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 @SuppressWarnings("deprecation") 158 public void processMouseEvent(MouseEvent e) { 159 if (e.isMetaDown()) { 160 e = new MouseEvent((Component) e.getSource(), e.getID(), 161 e.getWhen(), 162 e.getModifiers() ^ InputEvent.META_MASK, 163 e.getX(), e.getY(), e.getXOnScreen(), 164 e.getYOnScreen(), e.getClickCount(), 165 e.isPopupTrigger(), MouseEvent.NOBUTTON); 166 } 167 super.processMouseEvent(e); 168 } 169 }; 170 } 171 172 protected Rectangle adjustPopupAndGetBounds() { 173 if (isPopDown != isPopdown()) { 174 updateContents(true); 175 } 176 177 final Dimension popupSize = getBestPopupSizeForRowCount(comboBox.getMaximumRowCount()); 178 final Rectangle popupBounds = computePopupBounds(0, comboBox.getBounds().height, popupSize.width, popupSize.height); 179 if (popupBounds == null) return null; // returning null means don't show anything 180 181 final Dimension realPopupSize = popupBounds.getSize(); 182 scroller.setMaximumSize(realPopupSize); 183 scroller.setPreferredSize(realPopupSize); 184 scroller.setMinimumSize(realPopupSize); 185 list.invalidate(); 186 187 final int selectedIndex = comboBox.getSelectedIndex(); 188 if (selectedIndex == -1) { 189 list.clearSelection(); 190 } else { 191 list.setSelectedIndex(selectedIndex); 192 } 193 list.ensureIndexIsVisible(list.getSelectedIndex()); 194 195 return popupBounds; 196 } 197 198 // Get the bounds of the screen where the menu should appear 199 // p is the origin of the combo box in screen bounds 200 Rectangle getBestScreenBounds(final Point p) { 201 //System.err.println("GetBestScreenBounds p: "+ p.x + ", " + p.y); 202 final GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 203 final GraphicsDevice[] gs = ge.getScreenDevices(); 204 //System.err.println(" gs.length = " + gs.length); 205 final Rectangle comboBoxBounds = comboBox.getBounds(); 206 207 for (final GraphicsDevice gd : gs) { 208 final GraphicsConfiguration[] gc = gd.getConfigurations(); 209 for (final GraphicsConfiguration element0 : gc) { 210 final Rectangle gcBounds = element0.getBounds(); 211 if (gcBounds.contains(p)) { 212 return getAvailableScreenArea(gcBounds, element0); 213 } 214 } 215 } 216 217 // Hmm. Origin's off screen, but is any part on? 218 comboBoxBounds.setLocation(p); 219 for (final GraphicsDevice gd : gs) { 220 final GraphicsConfiguration[] gc = gd.getConfigurations(); 221 for (final GraphicsConfiguration element0 : gc) { 222 final Rectangle gcBounds = element0.getBounds(); 223 if (gcBounds.intersects(comboBoxBounds)) { 224 if (gcBounds.contains(p)) { 225 return getAvailableScreenArea(gcBounds, element0); 226 } 227 } 228 } 229 } 230 231 return null; 232 } 233 234 private Rectangle getAvailableScreenArea(Rectangle bounds, 235 GraphicsConfiguration gc) { 236 Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(gc); 237 return new Rectangle(0, insets.top, bounds.width, 238 bounds.height - insets.top); 239 } 240 241 @Override 242 protected Rectangle computePopupBounds(int px, int py, int pw, int ph) { 243 final int itemCount = comboBox.getModel().getSize(); 244 final boolean isPopdown = isPopdown(); 245 final boolean isTableCellEditor = AquaComboBoxUI.isTableCellEditor(comboBox); 246 if (isPopdown && !isTableCellEditor) { 247 // place the popup just below the button, which is 248 // near the center of a large combo box 249 py = Math.min((py / 2) + 9, py); // if py is less than new y we have a clipped combo, so leave it alone. 250 } 251 252 // px & py are relative to the combo box 253 254 // **** Common calculation - applies to the scrolling and menu-style **** 255 final Point p = new Point(0, 0); 256 SwingUtilities.convertPointToScreen(p, comboBox); 257 //System.err.println("First Converting from point to screen: 0,0 is now " + p.x + ", " + p.y); 258 final Rectangle scrBounds = getBestScreenBounds(p); 259 //System.err.println("BestScreenBounds is " + scrBounds); 260 261 // If the combo box is totally off screen, do whatever super does 262 if (scrBounds == null) return super.computePopupBounds(px, py, pw, ph); 263 264 // line up with the bottom of the text field/button (or top, if we have to go above it) 265 // and left edge if left-to-right, right edge if right-to-left 266 final Insets comboBoxInsets = comboBox.getInsets(); 267 final Rectangle comboBoxBounds = comboBox.getBounds(); 268 269 if (shouldScroll()) { 270 pw += 15; 271 } 272 273 if (isPopdown) { 274 pw += 4; 275 } 276 277 // the popup should be wide enough for the items but not wider than the screen it's on 278 final int minWidth = comboBoxBounds.width - (comboBoxInsets.left + comboBoxInsets.right); 279 pw = Math.max(minWidth, pw); 280 281 final boolean leftToRight = AquaUtils.isLeftToRight(comboBox); 282 if (leftToRight) { 283 px += comboBoxInsets.left; 284 if (!isPopDown) px -= FOCUS_RING_PAD_LEFT; 285 } else { 286 px = comboBoxBounds.width - pw - comboBoxInsets.right; 287 if (!isPopDown) px += FOCUS_RING_PAD_RIGHT; 288 } 289 py -= (comboBoxInsets.bottom); //sja fix was +kInset 290 291 // Make sure it's all on the screen - shift it by the amount it's off 292 p.x += px; 293 p.y += py; // Screen location of px & py 294 if (p.x < scrBounds.x) px -= (p.x + scrBounds.x); 295 if (p.y < scrBounds.y) py -= (p.y + scrBounds.y); 296 297 final Point top = new Point(0, 0); 298 SwingUtilities.convertPointFromScreen(top, comboBox); 299 //System.err.println("Converting from point to screen: 0,0 is now " + top.x + ", " + top.y); 300 301 // Since the popup is at zero in this coord space, the maxWidth == the X coord of the screen right edge 302 // (it might be wider than the screen, if the combo is off the left edge) 303 final int maxWidth = Math.min(scrBounds.width, top.x + scrBounds.x + scrBounds.width) - 2; // subtract some buffer space 304 305 pw = Math.min(maxWidth, pw); 306 if (pw < minWidth) { 307 px -= (minWidth - pw); 308 pw = minWidth; 309 } 310 311 // this is a popup window, and will continue calculations below 312 if (!isPopdown) { 313 // popup windows are slightly inset from the combo end-cap 314 pw -= 6; 315 return computePopupBoundsForMenu(px, py, pw, ph, itemCount, scrBounds); 316 } 317 318 // don't attempt to inset table cell editors 319 if (!isTableCellEditor) { 320 pw -= (FOCUS_RING_PAD_LEFT + FOCUS_RING_PAD_RIGHT); 321 if (leftToRight) { 322 px += FOCUS_RING_PAD_LEFT; 323 } 324 } 325 326 final Rectangle r = new Rectangle(px, py, pw, ph); 327 if (py + ph > scrBounds.y + scrBounds.height) { 328 if (ph <= -scrBounds.y ) { 329 // popup goes above 330 r.y = -ph ; 331 } else { 332 // a full screen height popup 333 r.y = scrBounds.y + Math.max(0, (scrBounds.height - ph) / 2 ); 334 r.height = Math.min(scrBounds.height, ph); 335 } 336 } 337 return r; 338 } 339 340 // The one to use when itemCount <= maxRowCount. Size never adjusts for arrows 341 // We want it positioned so the selected item is right above the combo box 342 protected Rectangle computePopupBoundsForMenu(final int px, final int py, final int pw, final int ph, final int itemCount, final Rectangle scrBounds) { 343 //System.err.println("computePopupBoundsForMenu: " + px + "," + py + " " + pw + "," + ph); 344 //System.err.println("itemCount: " +itemCount +" src: "+ scrBounds); 345 int elementSize = 0; //kDefaultItemSize; 346 if (list != null && itemCount > 0) { 347 final Rectangle cellBounds = list.getCellBounds(0, 0); 348 if (cellBounds != null) elementSize = cellBounds.height; 349 } 350 351 int offsetIndex = comboBox.getSelectedIndex(); 352 if (offsetIndex < 0) offsetIndex = 0; 353 list.setSelectedIndex(offsetIndex); 354 355 final int selectedLocation = elementSize * offsetIndex; 356 357 final Point top = new Point(0, scrBounds.y); 358 final Point bottom = new Point(0, scrBounds.y + scrBounds.height - 20); // Allow some slack 359 SwingUtilities.convertPointFromScreen(top, comboBox); 360 SwingUtilities.convertPointFromScreen(bottom, comboBox); 361 362 final Rectangle popupBounds = new Rectangle(px, py, pw, ph);// Relative to comboBox 363 364 final int theRest = ph - selectedLocation; 365 366 // If the popup fits on the screen and the selection appears under the mouse w/o scrolling, cool! 367 // If the popup won't fit on the screen, adjust its position but not its size 368 // and rewrite this to support arrows - JLists always move the contents so they all show 369 370 // Test to see if it extends off the screen 371 final boolean extendsOffscreenAtTop = selectedLocation > -top.y; 372 final boolean extendsOffscreenAtBottom = theRest > bottom.y; 373 374 if (extendsOffscreenAtTop) { 375 popupBounds.y = top.y + 1; 376 // Round it so the selection lines up with the combobox 377 popupBounds.y = (popupBounds.y / elementSize) * elementSize; 378 } else if (extendsOffscreenAtBottom) { 379 // Provide blank space at top for off-screen stuff to scroll into 380 popupBounds.y = bottom.y - popupBounds.height; // popupBounds.height has already been adjusted to fit 381 } else { // fits - position it so the selectedLocation is under the mouse 382 popupBounds.y = -selectedLocation; 383 } 384 385 // Center the selected item on the combobox 386 final int height = comboBox.getHeight(); 387 final Insets insets = comboBox.getInsets(); 388 final int buttonSize = height - (insets.top + insets.bottom); 389 final int diff = (buttonSize - elementSize) / 2 + insets.top; 390 popupBounds.y += diff - FOCUS_RING_PAD_BOTTOM; 391 392 return popupBounds; 393 } 394 }