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 }