1 /* 2 * Copyright (c) 2011, 2014, Oracle and/or its affiliates. All rights reserved. 3 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER. 4 * 5 * This code is free software; you can redistribute it and/or modify it 6 * under the terms of the GNU General Public License version 2 only, as 7 * published by the Free Software Foundation. Oracle designates this 8 * particular file as subject to the "Classpath" exception as provided 9 * by Oracle in the LICENSE file that accompanied this code. 10 * 11 * This code is distributed in the hope that it will be useful, but WITHOUT 12 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or 13 * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License 14 * version 2 for more details (a copy is included in the LICENSE file that 15 * accompanied this code). 16 * 17 * You should have received a copy of the GNU General Public License version 18 * 2 along with this work; if not, write to the Free Software Foundation, 19 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA. 20 * 21 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA 22 * or visit www.oracle.com if you need additional information or have any 23 * questions. 24 */ 25 26 package com.apple.laf; 27 28 import java.awt.*; 29 import java.awt.event.*; 30 31 import javax.swing.*; 32 import javax.swing.plaf.basic.BasicComboPopup; 33 34 import sun.lwawt.macosx.CPlatformWindow; 35 36 @SuppressWarnings("serial") // Superclass is not serializable across versions 37 class AquaComboBoxPopup extends BasicComboPopup { 38 static final int FOCUS_RING_PAD_LEFT = 6; 39 static final int FOCUS_RING_PAD_RIGHT = 6; 40 static final int FOCUS_RING_PAD_BOTTOM = 5; 41 42 protected Component topStrut; 43 protected Component bottomStrut; 44 protected boolean isPopDown = false; 45 46 public AquaComboBoxPopup(final JComboBox cBox) { 47 super(cBox); 48 } 49 50 @Override 51 protected void configurePopup() { 52 super.configurePopup(); 53 54 setBorderPainted(false); 55 setBorder(null); 56 updateContents(false); 57 58 // TODO: CPlatformWindow? 59 putClientProperty(CPlatformWindow.WINDOW_FADE_OUT, new Integer(150)); 60 } 61 62 public void updateContents(final boolean remove) { 63 // for more background on this issue, see AquaMenuBorder.getBorderInsets() 64 65 isPopDown = isPopdown(); 66 if (isPopDown) { 67 if (remove) { 68 if (topStrut != null) { 69 this.remove(topStrut); 70 } 71 if (bottomStrut != null) { 72 this.remove(bottomStrut); 73 } 74 } else { 75 add(scroller); 76 } 77 } else { 78 if (topStrut == null) { 79 topStrut = Box.createVerticalStrut(4); 80 bottomStrut = Box.createVerticalStrut(4); 81 } 82 83 if (remove) remove(scroller); 84 85 this.add(topStrut); 86 this.add(scroller); 87 this.add(bottomStrut); 88 } 89 } 90 91 protected Dimension getBestPopupSizeForRowCount(final int maxRowCount) { 92 final int currentElementCount = comboBox.getModel().getSize(); 93 final int rowCount = Math.min(maxRowCount, currentElementCount); 94 95 final Dimension popupSize = new Dimension(); 96 final ListCellRenderer renderer = list.getCellRenderer(); 97 98 for (int i = 0; i < rowCount; i++) { 99 final Object value = list.getModel().getElementAt(i); 100 final Component c = renderer.getListCellRendererComponent(list, value, i, false, false); 101 102 final Dimension prefSize = c.getPreferredSize(); 103 popupSize.height += prefSize.height; 104 popupSize.width = Math.max(prefSize.width, popupSize.width); 105 } 106 107 popupSize.width += 10; 108 109 return popupSize; 110 } 111 112 protected boolean shouldScroll() { 113 return comboBox.getItemCount() > comboBox.getMaximumRowCount(); 114 } 115 116 protected boolean isPopdown() { 117 return shouldScroll() || AquaComboBoxUI.isPopdown(comboBox); 118 } 119 120 @Override 121 public void show() { 122 final int startItemCount = comboBox.getItemCount(); 123 124 final Rectangle popupBounds = adjustPopupAndGetBounds(); 125 if (popupBounds == null) return; // null means don't show 126 127 comboBox.firePopupMenuWillBecomeVisible(); 128 show(comboBox, popupBounds.x, popupBounds.y); 129 130 // hack for <rdar://problem/4905531> JComboBox does not fire popupWillBecomeVisible if item count is 0 131 final int afterShowItemCount = comboBox.getItemCount(); 132 if (afterShowItemCount == 0) { 133 hide(); 134 return; 135 } 136 137 if (startItemCount != afterShowItemCount) { 138 final Rectangle newBounds = adjustPopupAndGetBounds(); 139 list.setSize(newBounds.width, newBounds.height); 140 pack(); 141 142 final Point newLoc = comboBox.getLocationOnScreen(); 143 setLocation(newLoc.x + newBounds.x, newLoc.y + newBounds.y); 144 } 145 // end hack 146 147 list.requestFocusInWindow(); 148 } 149 150 @Override 151 @SuppressWarnings("serial") // anonymous class 152 protected JList createList() { 153 return new JList(comboBox.getModel()) { 154 @Override 155 public void processMouseEvent(MouseEvent e) { 156 if (e.isMetaDown()) { 157 e = new MouseEvent((Component)e.getSource(), e.getID(), e.getWhen(), e.getModifiers() ^ InputEvent.META_MASK, e.getX(), e.getY(), e.getXOnScreen(), e.getYOnScreen(), e.getClickCount(), e.isPopupTrigger(), MouseEvent.NOBUTTON); 158 } 159 super.processMouseEvent(e); 160 } 161 }; 162 } 163 164 protected Rectangle adjustPopupAndGetBounds() { 165 if (isPopDown != isPopdown()) { 166 updateContents(true); 167 } 168 169 final Dimension popupSize = getBestPopupSizeForRowCount(comboBox.getMaximumRowCount()); 170 final Rectangle popupBounds = computePopupBounds(0, comboBox.getBounds().height, popupSize.width, popupSize.height); 171 if (popupBounds == null) return null; // returning null means don't show anything 172 173 final Dimension realPopupSize = popupBounds.getSize(); 174 scroller.setMaximumSize(realPopupSize); 175 scroller.setPreferredSize(realPopupSize); 176 scroller.setMinimumSize(realPopupSize); 177 list.invalidate(); 178 179 final int selectedIndex = comboBox.getSelectedIndex(); 180 if (selectedIndex == -1) { 181 list.clearSelection(); 182 } else { 183 list.setSelectedIndex(selectedIndex); 184 } 185 list.ensureIndexIsVisible(list.getSelectedIndex()); 186 187 return popupBounds; 188 } 189 190 // Get the bounds of the screen where the menu should appear 191 // p is the origin of the combo box in screen bounds 192 Rectangle getBestScreenBounds(final Point p) { 193 //System.err.println("GetBestScreenBounds p: "+ p.x + ", " + p.y); 194 final GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 195 final GraphicsDevice[] gs = ge.getScreenDevices(); 196 //System.err.println(" gs.length = " + gs.length); 197 final Rectangle comboBoxBounds = comboBox.getBounds(); 198 if (gs.length == 1) { 199 final Dimension scrSize = Toolkit.getDefaultToolkit().getScreenSize(); 200 201 //System.err.println(" scrSize: "+ scrSize); 202 203 // If the combo box is totally off screen, don't show a popup 204 if ((p.x + comboBoxBounds.width < 0) || (p.y + comboBoxBounds.height < 0) || (p.x > scrSize.width) || (p.y > scrSize.height)) { 205 return null; 206 } 207 return new Rectangle(0, 22, scrSize.width, scrSize.height - 22); 208 } 209 210 for (final GraphicsDevice gd : gs) { 211 final GraphicsConfiguration[] gc = gd.getConfigurations(); 212 for (final GraphicsConfiguration element0 : gc) { 213 final Rectangle gcBounds = element0.getBounds(); 214 if (gcBounds.contains(p)) return gcBounds; 215 } 216 } 217 218 // Hmm. Origin's off screen, but is any part on? 219 comboBoxBounds.setLocation(p); 220 for (final GraphicsDevice gd : gs) { 221 final GraphicsConfiguration[] gc = gd.getConfigurations(); 222 for (final GraphicsConfiguration element0 : gc) { 223 final Rectangle gcBounds = element0.getBounds(); 224 if (gcBounds.intersects(comboBoxBounds)) return gcBounds; 225 } 226 } 227 228 return null; 229 } 230 231 @Override 232 protected Rectangle computePopupBounds(int px, int py, int pw, int ph) { 233 final int itemCount = comboBox.getModel().getSize(); 234 final boolean isPopdown = isPopdown(); 235 final boolean isTableCellEditor = AquaComboBoxUI.isTableCellEditor(comboBox); 236 if (isPopdown && !isTableCellEditor) { 237 // place the popup just below the button, which is 238 // near the center of a large combo box 239 py = Math.min((py / 2) + 9, py); // if py is less than new y we have a clipped combo, so leave it alone. 240 } 241 242 // px & py are relative to the combo box 243 244 // **** Common calculation - applies to the scrolling and menu-style **** 245 final Point p = new Point(0, 0); 246 SwingUtilities.convertPointToScreen(p, comboBox); 247 //System.err.println("First Converting from point to screen: 0,0 is now " + p.x + ", " + p.y); 248 final Rectangle scrBounds = getBestScreenBounds(p); 249 //System.err.println("BestScreenBounds is " + scrBounds); 250 251 // If the combo box is totally off screen, do whatever super does 252 if (scrBounds == null) return super.computePopupBounds(px, py, pw, ph); 253 254 // line up with the bottom of the text field/button (or top, if we have to go above it) 255 // and left edge if left-to-right, right edge if right-to-left 256 final Insets comboBoxInsets = comboBox.getInsets(); 257 final Rectangle comboBoxBounds = comboBox.getBounds(); 258 259 if (shouldScroll()) { 260 pw += 15; 261 } 262 263 if (isPopdown) { 264 pw += 4; 265 } 266 267 // the popup should be wide enough for the items but not wider than the screen it's on 268 final int minWidth = comboBoxBounds.width - (comboBoxInsets.left + comboBoxInsets.right); 269 pw = Math.max(minWidth, pw); 270 271 final boolean leftToRight = AquaUtils.isLeftToRight(comboBox); 272 if (leftToRight) { 273 px += comboBoxInsets.left; 274 if (!isPopDown) px -= FOCUS_RING_PAD_LEFT; 275 } else { 276 px = comboBoxBounds.width - pw - comboBoxInsets.right; 277 if (!isPopDown) px += FOCUS_RING_PAD_RIGHT; 278 } 279 py -= (comboBoxInsets.bottom); //sja fix was +kInset 280 281 // Make sure it's all on the screen - shift it by the amount it's off 282 p.x += px; 283 p.y += py; // Screen location of px & py 284 if (p.x < scrBounds.x) px -= (p.x + scrBounds.x); 285 if (p.y < scrBounds.y) py -= (p.y + scrBounds.y); 286 287 final Point top = new Point(0, 0); 288 SwingUtilities.convertPointFromScreen(top, comboBox); 289 //System.err.println("Converting from point to screen: 0,0 is now " + top.x + ", " + top.y); 290 291 // Since the popup is at zero in this coord space, the maxWidth == the X coord of the screen right edge 292 // (it might be wider than the screen, if the combo is off the left edge) 293 final int maxWidth = Math.min(scrBounds.width, top.x + scrBounds.x + scrBounds.width) - 2; // subtract some buffer space 294 295 pw = Math.min(maxWidth, pw); 296 if (pw < minWidth) { 297 px -= (minWidth - pw); 298 pw = minWidth; 299 } 300 301 // this is a popup window, and will continue calculations below 302 if (!isPopdown) { 303 // popup windows are slightly inset from the combo end-cap 304 pw -= 6; 305 return computePopupBoundsForMenu(px, py, pw, ph, itemCount, scrBounds); 306 } 307 308 // don't attempt to inset table cell editors 309 if (!isTableCellEditor) { 310 pw -= (FOCUS_RING_PAD_LEFT + FOCUS_RING_PAD_RIGHT); 311 if (leftToRight) { 312 px += FOCUS_RING_PAD_LEFT; 313 } 314 } 315 316 final Rectangle r = new Rectangle(px, py, pw, ph); 317 // Check whether it goes below the bottom of the screen, if so flip it 318 if (r.y + r.height < top.y + scrBounds.y + scrBounds.height) return r; 319 320 return new Rectangle(px, -r.height + comboBoxInsets.top, r.width, r.height); 321 } 322 323 // The one to use when itemCount <= maxRowCount. Size never adjusts for arrows 324 // We want it positioned so the selected item is right above the combo box 325 protected Rectangle computePopupBoundsForMenu(final int px, final int py, final int pw, final int ph, final int itemCount, final Rectangle scrBounds) { 326 //System.err.println("computePopupBoundsForMenu: " + px + "," + py + " " + pw + "," + ph); 327 //System.err.println("itemCount: " +itemCount +" src: "+ scrBounds); 328 int elementSize = 0; //kDefaultItemSize; 329 if (list != null && itemCount > 0) { 330 final Rectangle cellBounds = list.getCellBounds(0, 0); 331 if (cellBounds != null) elementSize = cellBounds.height; 332 } 333 334 int offsetIndex = comboBox.getSelectedIndex(); 335 if (offsetIndex < 0) offsetIndex = 0; 336 list.setSelectedIndex(offsetIndex); 337 338 final int selectedLocation = elementSize * offsetIndex; 339 340 final Point top = new Point(0, scrBounds.y); 341 final Point bottom = new Point(0, scrBounds.y + scrBounds.height - 20); // Allow some slack 342 SwingUtilities.convertPointFromScreen(top, comboBox); 343 SwingUtilities.convertPointFromScreen(bottom, comboBox); 344 345 final Rectangle popupBounds = new Rectangle(px, py, pw, ph);// Relative to comboBox 346 347 final int theRest = ph - selectedLocation; 348 349 // If the popup fits on the screen and the selection appears under the mouse w/o scrolling, cool! 350 // If the popup won't fit on the screen, adjust its position but not its size 351 // and rewrite this to support arrows - JLists always move the contents so they all show 352 353 // Test to see if it extends off the screen 354 final boolean extendsOffscreenAtTop = selectedLocation > -top.y; 355 final boolean extendsOffscreenAtBottom = theRest > bottom.y; 356 357 if (extendsOffscreenAtTop) { 358 popupBounds.y = top.y + 1; 359 // Round it so the selection lines up with the combobox 360 popupBounds.y = (popupBounds.y / elementSize) * elementSize; 361 } else if (extendsOffscreenAtBottom) { 362 // Provide blank space at top for off-screen stuff to scroll into 363 popupBounds.y = bottom.y - popupBounds.height; // popupBounds.height has already been adjusted to fit 364 } else { // fits - position it so the selectedLocation is under the mouse 365 popupBounds.y = -selectedLocation; 366 } 367 368 // Center the selected item on the combobox 369 final int height = comboBox.getHeight(); 370 final Insets insets = comboBox.getInsets(); 371 final int buttonSize = height - (insets.top + insets.bottom); 372 final int diff = (buttonSize - elementSize) / 2 + insets.top; 373 popupBounds.y += diff - FOCUS_RING_PAD_BOTTOM; 374 375 return popupBounds; 376 } 377 }