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