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 &quot;AS IS&quot;, 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