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