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 }