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