001 package com.softnetConsult.utils.swing; 002 003 import java.awt.BorderLayout; 004 import java.awt.Component; 005 import java.awt.Dimension; 006 import java.awt.FontMetrics; 007 import java.util.Vector; 008 009 import javax.accessibility.Accessible; 010 import javax.swing.ComboBoxModel; 011 import javax.swing.JComboBox; 012 import javax.swing.JScrollPane; 013 import javax.swing.plaf.ComboBoxUI; 014 import javax.swing.plaf.basic.BasicComboPopup; 015 016 017 /** 018 * Implements a JComboBox that dynamically adjusts the size of its selection drop-down 019 * based on the assumption that all items will be displayed using their toString method. 020 * 021 * <p style="font-size:smaller;">This product includes software developed by the 022 * <strong>SoftNet-Consult Java Utility Library</strong> project and its contributors.<br /> 023 * (<a href="http://java-tools.sourceforge.net" target="_blank">http://java-tools.sourceforge.net</a>)<br /> 024 * Copyright (c) 2007-2008 SoftNet-Consult.<br /> 025 * Copyright (c) 2007-2008 G. Paperin.<br /> 026 * All rights reserved. 027 * </p> 028 * <p style="font-size:smaller;">File: ResizablePopupComboBox.java<br /> 029 * Library API version: {@value com.softnetConsult.utils.APIProperties#apiVersion}<br /> 030 * Java compliance version: {@value com.softnetConsult.utils.APIProperties#javaComplianceVersion} 031 * </p> 032 * <p style="font-size:smaller;">Redistribution and use in source and binary forms, with or 033 * without modification, are permitted provided that the following terms and conditions are met: 034 * </p> 035 * <p style="font-size:smaller;">1. Redistributions of source code must retain the above 036 * acknowledgement of the SoftNet-Consult Java Utility Library project, the above copyright 037 * notice, this list of conditions and the following disclaimer.<br /> 038 * 2. Redistributions in binary form must reproduce the above acknowledgement of the 039 * SoftNet-Consult Java Utility Library project, the above copyright notice, this list of 040 * conditions and the following disclaimer in the documentation and/or other materials 041 * provided with the distribution.<br /> 042 * 3. All advertising materials mentioning features or use of this software or any derived 043 * software must display the following acknowledgement:<br /> 044 * <em>This product includes software developed by the SoftNet-Consult Java Utility Library 045 * project and its contributors.</em> 046 * </p> 047 * <p style="font-size:smaller;">THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY 048 * OF ANY KIND, EXPRESSED OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 049 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 050 * THE AUTHORS, CONTRIBUTORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 051 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 052 * IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 053 * </p> 054 * @author Greg Paperin (<a href="http://www.paperin.org" target="_blank">http://www.paperin.org</a>) 055 * @version {@value com.softnetConsult.utils.APIProperties#apiVersion} 056 * 057 */ 058 public class ResizablePopupComboBox extends JComboBox { 059 060 private Dimension minPopupSize = null; 061 private Dimension maxPopupSize = null; 062 private Dimension prefPopupSize = null; 063 064 /** 065 * Directly calls {@link JComboBox#JComboBox()}. 066 */ 067 public ResizablePopupComboBox() { 068 super(); 069 } 070 071 /** 072 * Directly calls {@link JComboBox#JComboBox(ComboBoxModel)}. 073 * @param model the {@code ComboBoxModel} that provides the displayed list of items. 074 */ 075 public ResizablePopupComboBox(ComboBoxModel model) { 076 super(model); 077 } 078 079 /** 080 * Directly calls {@link JComboBox#JComboBox(Object[])}. 081 * @param items an array of objects to insert into the combo box. 082 */ 083 public ResizablePopupComboBox(Object[] items) { 084 super(items); 085 } 086 087 /** 088 * Directly calls {@link JComboBox#JComboBox(Vector)}. 089 * @param items an vector of objects to insert into the combo box. 090 */ 091 public ResizablePopupComboBox(Vector<?> items) { 092 super(items); 093 } 094 095 /** 096 * Sets the minimum size for this combo box drop-down popup. 097 * Values that are smaller than {@code 0} will be interpreted as <em>unspecified</em> or <em>any</em>. 098 * 099 * @param width The minimum width of the popup. 100 * @param height The minimum height of the popup. 101 */ 102 public void setMinimumPopupSize(int width, int height) { 103 minPopupSize = new Dimension(width, height); 104 } 105 106 /** 107 * Sets the minimum size for this combo box drop-down popup. 108 * The value {@code null} or component values that are smaller than {@code 0} will be interpreted 109 * as <em>unspecified</em> or <em>any</em>. 110 * 111 * @param dim The minimum size for this combo box drop-down popup 112 */ 113 public void setMinimumPopupSize(Dimension dim) { 114 if (null == dim) 115 minPopupSize = null; 116 setMinimumPopupSize(dim.width, dim.height); 117 } 118 119 /** 120 * Gets the minimum size for this combo box drop-down popup. 121 * 122 * @return The minimum size for this combo box drop-down popup. 123 * The value {@code null} or component values that are smaller than {@code 0} will be interpreted 124 * as <em>unspecified</em> or <em>any</em>. 125 */ 126 public Dimension getMinimumPopupSize() { 127 return new Dimension(minPopupSize.width, minPopupSize.height); 128 } 129 130 /** 131 * Sets the maximum size for this combo box drop-down popup. 132 * Values that are smaller than {@code 0} will be interpreted as <em>unspecified</em> or <em>any</em>. 133 * 134 * @param width The maximum width of the popup. 135 * @param height The maximum height of the popup. 136 */ 137 public void setMaximumPopupSize(int width, int height) { 138 maxPopupSize = new Dimension(width, height); 139 } 140 141 /** 142 * Sets the maximum size for this combo box drop-down popup. 143 * The value {@code null} or component values that are smaller than {@code 0} will be interpreted 144 * as <em>unspecified</em> or <em>any</em>. 145 * 146 * @param dim The maximum size for this combo box drop-down popup 147 */ 148 public void setMaximumPopupSize(Dimension dim) { 149 if (null == dim) 150 maxPopupSize = null; 151 setMaximumPopupSize(dim.width, dim.height); 152 } 153 154 /** 155 * Gets the maximum size for this combo box drop-down popup. 156 * 157 * @return The maximum size for this combo box drop-down popup. 158 * The value {@code null} or component values that are smaller than {@code 0} will be interpreted 159 * as <em>unspecified</em> or <em>any</em>. 160 */ 161 public Dimension getMaximumPopupSize() { 162 return new Dimension(maxPopupSize.width, maxPopupSize.height); 163 } 164 165 /** 166 * Sets the preferred size for this combo box drop-down popup. 167 * Values that are smaller than {@code 0} will be interpreted as <em>unspecified</em> or <em>any</em>. 168 * 169 * @param width The preferred width of the popup. 170 * @param height The preferred height of the popup. 171 */ 172 public void setPreferredPopupSize(int width, int height) { 173 prefPopupSize = new Dimension(width, height); 174 } 175 176 /** 177 * Sets the preferred size for this combo box drop-down popup. 178 * The value {@code null} or component values that are smaller than {@code 0} will be interpreted 179 * as <em>unspecified</em> or <em>any</em>. 180 * 181 * @param dim The preferred size for this combo box drop-down popup 182 */ 183 public void setPreferredPopupSize(Dimension dim) { 184 if (null == dim) 185 prefPopupSize = null; 186 setPreferredPopupSize(dim.width, dim.height); 187 } 188 189 /** 190 * Gets the preferred size for this combo box drop-down popup. 191 * 192 * @return The preferred size for this combo box drop-down popup. 193 * The value {@code null} or component values that are smaller than {@code 0} will be interpreted 194 * as <em>unspecified</em> or <em>any</em>. 195 */ 196 public Dimension setPreferredPopupSize() { 197 return new Dimension(prefPopupSize.width, prefPopupSize.height); 198 } 199 200 @Override 201 public void firePopupMenuWillBecomeVisible() { 202 setPopupSize(); 203 super.firePopupMenuWillBecomeVisible(); 204 } 205 206 @Override 207 public void updateUI(){ 208 super.updateUI(); 209 setPopupSize(); 210 } 211 212 /** 213 * Gets the drop-down popup component for this combo box. 214 * 215 * @return The drop-down popup component for this combo box. 216 */ 217 private BasicComboPopup getComboPopup() { 218 219 ComboBoxUI ui = getUI(); 220 if (0 > ui.getAccessibleChildrenCount(this)) 221 return null; 222 223 Accessible accs = ui.getAccessibleChild(this, 0);; 224 if (! (accs instanceof BasicComboPopup)) 225 return null; 226 227 return (BasicComboPopup) accs; 228 } 229 230 /** 231 * Gets the actual size for the drop-down popup component of this combo box. 232 * If the size can be determined on the basis of the preferred, minimum and maximum 233 * popup size properties, than those values are used. If the size cannot be determined 234 * on the basis of those properties because they are set to {@code null} or because 235 * their {@code width} or {@code height} components are set to negative values, than 236 * the size is determined as follows:<br /> 237 * The height is copied from the default dehavious of the popup.<br /> 238 * The width is calculated under the assumption that all items of this combo box 239 * will be displayed using their {@code .toString()} methods. The width of the popup 240 * is set to the width of the longest item string under the current font metrics 241 * plus the width of the popup scroller it that is visible. 242 * 243 * @return The actual size for the drop-down popup component of this combo box. 244 */ 245 private Dimension calcPopupSize() { 246 247 Dimension prefSize = new Dimension(-1, -1); 248 249 // If there are enough preferences, return the size imediately: 250 251 if (null != prefPopupSize) { 252 prefSize.setSize(prefPopupSize.width, prefPopupSize.height); 253 254 if (null != minPopupSize) { 255 if (0 <= prefSize.width && 0 <= minPopupSize.width && prefSize.width < minPopupSize.width) 256 prefSize.width = minPopupSize.width; 257 if (0 <= prefSize.height && 0 <= minPopupSize.height && prefSize.height < minPopupSize.height) 258 prefSize.height = minPopupSize.height; 259 } 260 if (null != maxPopupSize) { 261 if (0 <= prefSize.width && 0 <= maxPopupSize.width && prefSize.width > maxPopupSize.width) 262 prefSize.width = maxPopupSize.width; 263 if (0 <= prefSize.height && 0 <= maxPopupSize.height && prefSize.height > maxPopupSize.height) 264 prefSize.height = maxPopupSize.height; 265 } 266 267 if (0 <= prefSize.width && 0 <= prefSize.height) 268 return prefSize; 269 } 270 271 // We need the popup for later calculatiopns: 272 273 BasicComboPopup popup = getComboPopup(); 274 275 276 // If preferred size is missing the width component, we need to calculate that: 277 278 if (0 > prefSize.width) { 279 280 // Get the max width of all popup items: 281 282 FontMetrics fm = this.getFontMetrics(getFont()); 283 284 prefSize.width = this.getPreferredSize().width; 285 for(int i = 0; i < getItemCount(); i++) { 286 287 Object item = getItemAt(i); 288 String itemStr = "null"; 289 if (null != item) 290 itemStr = item.toString(); 291 292 int strWidth = fm.stringWidth(itemStr); 293 if (prefSize.width < strWidth) 294 prefSize.width = strWidth; 295 } 296 297 // If there is a scroll bar, add its width: 298 299 if (null != popup && popup.getComponentCount() > 0) { 300 301 Component popupComp = popup.getComponent(0); 302 if (null != popupComp && popupComp instanceof JScrollPane) { 303 JScrollPane scrollPane = (JScrollPane) popupComp; 304 if (scrollPane.getVerticalScrollBar().isVisible()) 305 prefSize.width += scrollPane.getVerticalScrollBar().getWidth(); 306 } 307 } 308 } 309 310 // If preferred size is missing the height component, we need to calculate that: 311 312 if (0 > prefSize.height) { 313 if (null != popup) 314 prefSize.height = popup.getPreferredSize().height; 315 } 316 317 // Ensure bounds: 318 319 if (null != minPopupSize) { 320 if (0 <= minPopupSize.width && (prefSize.width < minPopupSize.width || prefSize.width < 0)) 321 prefSize.width = minPopupSize.width; 322 if (0 <= minPopupSize.height && (prefSize.height < minPopupSize.height || prefSize.height < 0)) 323 prefSize.height = minPopupSize.height; 324 } 325 326 if (null != maxPopupSize) { 327 if (0 <= maxPopupSize.width && (prefSize.width > maxPopupSize.width || prefSize.width < 0)) 328 prefSize.width = maxPopupSize.width; 329 if (0 <= maxPopupSize.height && (prefSize.height > maxPopupSize.height || prefSize.height < 0)) 330 prefSize.height = maxPopupSize.height; 331 } 332 333 return prefSize; 334 } 335 336 /** 337 * Calculates the size for the popup of this compo box using {@link #calcPopupSize()} and sets 338 * that as the new drop-down popup size. 339 */ 340 private void setPopupSize() { 341 342 Dimension size = calcPopupSize(); 343 if (null == size) 344 return; 345 346 BasicComboPopup popup = getComboPopup(); 347 if (null == popup) 348 return; 349 350 popup.setLayout(new BorderLayout()); 351 352 if (0 < popup.getComponentCount()) { 353 Component popupComp = popup.getComponent(0); 354 popup.remove(0); 355 popup.add(popupComp, BorderLayout.CENTER); 356 } 357 358 popup.setPreferredSize(size); 359 } 360 361 } // public class ResizablePopupComboBox