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