1 /*
2  * @(#)QuaquaComboPopup.java  1.2.2  2006-11-02
3  *
4  * Copyright (c) 2004-2006 Werner Randelshofer
5  * Staldenmattweg 2, Immensee, CH-6405, Switzerland.
6  * All rights reserved.
7  *
8  * The copyright of this software is owned by Werner Randelshofer.
9  * You may not use, copy or modify this software, except in
10  * accordance with the license agreement you entered into with
11  * Werner Randelshofer. For details see accompanying license terms.
12  */
13 
14 package ch.randelshofer.quaqua;
15 
16 import java.lang.reflect.*;
17 import java.awt.*;
18 import java.awt.event.*;
19 import javax.swing.*;
20 import javax.swing.plaf.*;
21 import javax.swing.border.*;
22 import javax.swing.plaf.basic.*;
23 import java.io.Serializable;
24 import java.beans.*;
25 /**
26  * QuaquaComboPopup.
27  *
28  * @author  Werner Randelshofer
29  * @version 1.2.2 2006-11-02 XXX - Ensure that large combo boxes fit on screen.
30  * <br>1.2.1 2006-02-13 Fixed background color.
31  * <br>1.2 2005-06-21 PropertyChangeHandler which is responsible for detecting
32  * whether we are a table cell renderer changed, in order to detect cell rendering in Java 1.3.
33  * <br>1.1 2004-10-06 Popup menu width extends itself to accomodate
34  * the widest item.
35  * <br>1.0 April 11, 2004 Created.
36  */
37 public class QuaquaComboPopup extends BasicComboPopup {
38     private QuaquaComboBoxUI qqui;
39 
QuaquaComboPopup( JComboBox cBox, QuaquaComboBoxUI qqui)40     public QuaquaComboPopup( JComboBox cBox, QuaquaComboBoxUI qqui) {
41         super(cBox);
42         this.qqui = qqui;
43         updateCellRenderer(qqui.isTableCellEditor());
44     }
45 
46     /**
47      * Implementation of ComboPopup.show().
48      */
show()49     public void show() {
50 	setListSelection(comboBox.getSelectedIndex());
51 
52 	Point location = getPopupLocation();
53         show( comboBox, location.x, location.y );
54 
55         // This is required to properly render the selection, when the JComboBox
56         // is used as a table cell editor.
57         list.repaint();
58     }
59 
updateCellRenderer(boolean isTableCellEditor)60     private void updateCellRenderer(boolean isTableCellEditor) {
61         list.setCellRenderer(
62                 new QuaquaComboBoxCellRenderer(
63                 comboBox.getRenderer(), isTableCellEditor, comboBox.isEditable()
64                 ));
65     }
66     /**
67      * Creates a <code>PropertyChangeListener</code> which will be added to
68      * the combo box. If this method returns null then it will not
69      * be added to the combo box.
70      *
71      * @return an instance of a <code>PropertyChangeListener</code> or null
72      */
createPropertyChangeListener()73     protected PropertyChangeListener createPropertyChangeListener() {
74         return new BasicComboPopup.PropertyChangeHandler() {
75             public void propertyChange( PropertyChangeEvent e ) {
76                 super.propertyChange(e);
77                 String propertyName = e.getPropertyName();
78                 JComboBox comboBox = (JComboBox)e.getSource();
79 
80                 if ( propertyName.equals( "renderer" ) ||
81                         propertyName.equals(QuaquaComboBoxUI.IS_TABLE_CELL_EDITOR)) {
82                     updateCellRenderer(e.getNewValue().equals(Boolean.TRUE));
83                 } else if (propertyName.equals("JComboBox.lightweightKeyboardNavigation")) {
84                     // In Java 1.3 we have to use this property to guess whether we
85                     // are a table cell editor or not.
86                     updateCellRenderer(e.getNewValue() != null && e.getNewValue().equals("Lightweight"));
87                 } else if ( propertyName.equals( "editable" )) {
88                     updateCellRenderer(isTableCellEditor());
89                 }
90             }
91         };
92     }
93 
94     private int getMaximumRowCount() {
95         return (isEditable() || isTableCellEditor()) ?
96             comboBox.getMaximumRowCount() :
97             100;
98     }
99 
100     /**
101      * Calculates the upper left location of the Popup.
102      */
103     private Point getPopupLocation() {
104 	Dimension popupSize = comboBox.getSize();
105 	Insets insets = getInsets();
106 
107 	// reduce the width of the scrollpane by the insets so that the popup
108 	// is the same width as the combo box.
109 	popupSize.setSize(popupSize.width - (insets.right + insets.left),
110 			  getPopupHeightForRowCount( getMaximumRowCount()));
111 	Rectangle popupBounds = computePopupBounds( 0, comboBox.getBounds().height,
112                                                     popupSize.width, popupSize.height);
113 	Dimension scrollSize = popupBounds.getSize();
114 	Point popupLocation = popupBounds.getLocation();
115 
116 	scroller.setMaximumSize( scrollSize );
117 	scroller.setPreferredSize( scrollSize );
118 	scroller.setMinimumSize( scrollSize );
119 
120 	list.revalidate();
121 
122 	return popupLocation;
123     }
124     /**
125      * Sets the list selection index to the selectedIndex. This
126      * method is used to synchronize the list selection with the
127      * combo box selection.
128      *
129      * @param selectedIndex the index to set the list
130      */
131     private void setListSelection(int selectedIndex) {
132         if ( selectedIndex == -1 ) {
133             list.clearSelection();
134         }
135         else {
136             list.setSelectedIndex( selectedIndex );
137 	    list.ensureIndexIsVisible( selectedIndex );
138         }
139     }
140     /**
141      * Calculate the placement and size of the popup portion of the combo box based
142      * on the combo box location and the enclosing screen bounds. If
143      * no transformations are required, then the returned rectangle will
144      * have the same values as the parameters.
145      *
146      * @param px starting x location
147      * @param py starting y location
148      * @param pw starting width
149      * @param ph starting height
150      * @return a rectangle which represents the placement and size of the popup
151      */
152     protected Rectangle computePopupBounds(int px,int py,int pw,int ph) {
153 
154         Toolkit toolkit = Toolkit.getDefaultToolkit();
155         Rectangle screenBounds;
156         int listWidth = getList().getPreferredSize().width;
157         Insets margin = qqui.getMargin();
158         boolean isTableCellEditor = isTableCellEditor();
159         boolean hasScrollBars = hasScrollBars();
160         boolean isEditable = isEditable();
161         boolean isSmall = QuaquaUtilities.isSmallSizeVariant(comboBox);
162 
163 
164         if (isTableCellEditor) {
165             if (hasScrollBars) {
166                 pw = Math.max(pw, listWidth + 16);
167             } else {
168                 pw = Math.max(pw, listWidth);
169             }
170         } else {
171             if (hasScrollBars) {
172                 px += margin.left;
173                 pw = Math.max(pw - margin.left - margin.right, listWidth + 16);
174             } else {
175                 if (isEditable) {
176                     px += margin.left;
177                     pw = Math.max(pw - qqui.getArrowWidth() - margin.left, listWidth);
178                 } else {
179                     px += margin.left;
180                     pw = Math.max(pw - qqui.getArrowWidth() - margin.left, listWidth);
181                 }
182             }
183         }
184         // Calculate the desktop dimensions relative to the combo box.
185         GraphicsConfiguration gc = comboBox.getGraphicsConfiguration();
186         Point p = new Point();
187         SwingUtilities.convertPointFromScreen(p, comboBox);
188         if (gc != null) {
189             // Get the screen insets.
190             // This method will work with JDK 1.4 only. Since we want to stay
191             // compatible with JDk 1.3, we use the Reflection API to access it.
192             //Insets screenInsets = toolkit.getScreenInsets(gc);
193             Insets screenInsets;
194             try {
195                 screenInsets = (Insets)
196                 Toolkit.class.getMethod("getScreenInsets",  new Class[] {GraphicsConfiguration.class})
197                 .invoke(toolkit, new Object[] {gc});
198             } catch (Exception e) {
199                 //e.printStackTrace();
200                 screenInsets = new Insets(22,0,0,0);
201             }
202             // Note: We must create a new rectangle here, because method
203             // getBounds does not return a copy of a rectangle on J2SE 1.3.
204             screenBounds = new Rectangle(gc.getBounds());
205             screenBounds.width -= (screenInsets.left + screenInsets.right);
206             screenBounds.height -= (screenInsets.top + screenInsets.bottom);
207             screenBounds.x += screenInsets.left;
208             screenBounds.y += screenInsets.top;
209         } else {
210             screenBounds = new Rectangle(p, toolkit.getScreenSize());
211         }
212 
213         if (isDropDown()) {
214             if (! isTableCellEditor) {
215                 if (isEditable) {
216                     py -= margin.bottom + 2;
217                 } else {
218                     py -= margin.bottom;
219                 }
220             }
221         } else {
222             int yOffset;
223             if (isTableCellEditor) {
224                 yOffset = 7;
225             } else {
226                 yOffset = 3 - margin.top;
227             }
228             int selectedIndex = comboBox.getSelectedIndex();
229             if (selectedIndex <= 0) {
230                 py = -yOffset;
231             } else {
232                 py = -yOffset - list.getCellBounds(0, selectedIndex - 1).height;
233 
234             }
235         }
236 
237         // Compute the rectangle for the popup menu
238         Rectangle rect = new Rectangle(
239                 px,
240                 Math.max(py, p.y + screenBounds.y),
241                 Math.min(screenBounds.width, pw),
242                 Math.min(screenBounds.height - 40, ph)
243                 );
244 
245         // Add the preferred scroll bar width, if the popup does not fit
246         // on the available rectangle.
247         if (rect.height < ph) {
248             rect.width += 16;
249         }
250 
251         return rect;
252     }
253 
254     private boolean isDropDown() {
255         return comboBox.isEditable() || hasScrollBars();
256     }
257     private boolean hasScrollBars() {
258         return comboBox.getModel().getSize() > getMaximumRowCount();
259     }
260     private boolean isEditable() {
261         return comboBox.isEditable();
262     }
263     private boolean isTableCellEditor() {
264         return qqui.isTableCellEditor();
265     }
266 
267     /**
268      * Configures the popup portion of the combo box. This method is called
269      * when the UI class is created.
270      */
271     protected void configurePopup() {
272         super.configurePopup();
273         // FIXME - We need to convert the border into a non-UIResource object.
274         // An UIResourceObject will be removed from the popup.
275         //setBorder( new CompoundBorder(UIManager.getBorder("PopupMenu.border"), new EmptyBorder(0,0,0,0)));
276         setBorder(UIManager.getBorder("PopupMenu.border"));
277     }
278 
279     protected void configureList() {
280         super.configureList();
281         list.setBackground(UIManager.getColor("PopupMenu.background"));
282     }
283 
284 }
285