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