Path: blob/master/src/java.desktop/macosx/classes/com/apple/laf/AquaComboBoxPopup.java
41154 views
/*1* Copyright (c) 2011, 2017, Oracle and/or its affiliates. All rights reserved.2* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.3*4* This code is free software; you can redistribute it and/or modify it5* under the terms of the GNU General Public License version 2 only, as6* published by the Free Software Foundation. Oracle designates this7* particular file as subject to the "Classpath" exception as provided8* by Oracle in the LICENSE file that accompanied this code.9*10* This code is distributed in the hope that it will be useful, but WITHOUT11* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or12* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License13* version 2 for more details (a copy is included in the LICENSE file that14* accompanied this code).15*16* You should have received a copy of the GNU General Public License version17* 2 along with this work; if not, write to the Free Software Foundation,18* Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.19*20* Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA21* or visit www.oracle.com if you need additional information or have any22* questions.23*/2425package com.apple.laf;2627import java.awt.Component;28import java.awt.Dimension;29import java.awt.GraphicsConfiguration;30import java.awt.GraphicsDevice;31import java.awt.GraphicsEnvironment;32import java.awt.Insets;33import java.awt.Point;34import java.awt.Rectangle;35import java.awt.Toolkit;36import java.awt.event.InputEvent;37import java.awt.event.MouseEvent;3839import javax.swing.Box;40import javax.swing.JComboBox;41import javax.swing.JList;42import javax.swing.ListCellRenderer;43import javax.swing.SwingUtilities;44import javax.swing.plaf.basic.BasicComboPopup;4546import sun.lwawt.macosx.CPlatformWindow;4748@SuppressWarnings("serial") // Superclass is not serializable across versions49final class AquaComboBoxPopup extends BasicComboPopup {50static final int FOCUS_RING_PAD_LEFT = 6;51static final int FOCUS_RING_PAD_RIGHT = 6;52static final int FOCUS_RING_PAD_BOTTOM = 5;5354protected Component topStrut;55protected Component bottomStrut;56protected boolean isPopDown = false;5758public AquaComboBoxPopup(final JComboBox<Object> cBox) {59super(cBox);60}6162@Override63protected void configurePopup() {64super.configurePopup();6566setBorderPainted(false);67setBorder(null);68updateContents(false);6970// TODO: CPlatformWindow?71putClientProperty(CPlatformWindow.WINDOW_FADE_OUT, Integer.valueOf(150));72}7374public void updateContents(final boolean remove) {75// for more background on this issue, see AquaMenuBorder.getBorderInsets()7677isPopDown = isPopdown();78if (isPopDown) {79if (remove) {80if (topStrut != null) {81this.remove(topStrut);82}83if (bottomStrut != null) {84this.remove(bottomStrut);85}86} else {87add(scroller);88}89} else {90if (topStrut == null) {91topStrut = Box.createVerticalStrut(4);92bottomStrut = Box.createVerticalStrut(4);93}9495if (remove) remove(scroller);9697this.add(topStrut);98this.add(scroller);99this.add(bottomStrut);100}101}102103protected Dimension getBestPopupSizeForRowCount(final int maxRowCount) {104final int currentElementCount = comboBox.getModel().getSize();105final int rowCount = Math.min(maxRowCount, currentElementCount);106107final Dimension popupSize = new Dimension();108final ListCellRenderer<Object> renderer = list.getCellRenderer();109110for (int i = 0; i < rowCount; i++) {111final Object value = list.getModel().getElementAt(i);112final Component c = renderer.getListCellRendererComponent(list, value, i, false, false);113114final Dimension prefSize = c.getPreferredSize();115popupSize.height += prefSize.height;116popupSize.width = Math.max(prefSize.width, popupSize.width);117}118119popupSize.width += 10;120121return popupSize;122}123124protected boolean shouldScroll() {125return comboBox.getItemCount() > comboBox.getMaximumRowCount();126}127128protected boolean isPopdown() {129return shouldScroll() || AquaComboBoxUI.isPopdown(comboBox);130}131132@Override133public void show() {134final int startItemCount = comboBox.getItemCount();135136final Rectangle popupBounds = adjustPopupAndGetBounds();137if (popupBounds == null) return; // null means don't show138139comboBox.firePopupMenuWillBecomeVisible();140show(comboBox, popupBounds.x, popupBounds.y);141142// hack for <rdar://problem/4905531> JComboBox does not fire popupWillBecomeVisible if item count is 0143final int afterShowItemCount = comboBox.getItemCount();144if (afterShowItemCount == 0) {145hide();146return;147}148149if (startItemCount != afterShowItemCount) {150final Rectangle newBounds = adjustPopupAndGetBounds();151list.setSize(newBounds.width, newBounds.height);152pack();153154final Point newLoc = comboBox.getLocationOnScreen();155setLocation(newLoc.x + newBounds.x, newLoc.y + newBounds.y);156}157// end hack158159list.requestFocusInWindow();160}161162@Override163@SuppressWarnings("serial") // anonymous class164protected JList<Object> createList() {165return new JList<Object>(comboBox.getModel()) {166@Override167@SuppressWarnings("deprecation")168public void processMouseEvent(MouseEvent e) {169if (e.isMetaDown()) {170e = new MouseEvent((Component) e.getSource(), e.getID(),171e.getWhen(),172e.getModifiers() ^ InputEvent.META_MASK,173e.getX(), e.getY(), e.getXOnScreen(),174e.getYOnScreen(), e.getClickCount(),175e.isPopupTrigger(), MouseEvent.NOBUTTON);176}177super.processMouseEvent(e);178}179};180}181182protected Rectangle adjustPopupAndGetBounds() {183if (isPopDown != isPopdown()) {184updateContents(true);185}186187final Dimension popupSize = getBestPopupSizeForRowCount(comboBox.getMaximumRowCount());188final Rectangle popupBounds = computePopupBounds(0, comboBox.getBounds().height, popupSize.width, popupSize.height);189if (popupBounds == null) return null; // returning null means don't show anything190191final Dimension realPopupSize = popupBounds.getSize();192scroller.setMaximumSize(realPopupSize);193scroller.setPreferredSize(realPopupSize);194scroller.setMinimumSize(realPopupSize);195list.invalidate();196197final int selectedIndex = comboBox.getSelectedIndex();198if (selectedIndex == -1) {199list.clearSelection();200} else {201list.setSelectedIndex(selectedIndex);202}203list.ensureIndexIsVisible(list.getSelectedIndex());204205return popupBounds;206}207208// Get the bounds of the screen where the menu should appear209// p is the origin of the combo box in screen bounds210Rectangle getBestScreenBounds(final Point p) {211//System.err.println("GetBestScreenBounds p: "+ p.x + ", " + p.y);212final GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();213final GraphicsDevice[] gs = ge.getScreenDevices();214for (final GraphicsDevice gd : gs) {215final GraphicsConfiguration[] gc = gd.getConfigurations();216for (final GraphicsConfiguration element0 : gc) {217final Rectangle gcBounds = element0.getBounds();218if (gcBounds.contains(p)) {219return getAvailableScreenArea(gcBounds, element0);220}221}222}223224// Hmm. Origin's off screen, but is any part on?225final Rectangle comboBoxBounds = comboBox.getBounds();226comboBoxBounds.setLocation(p);227for (final GraphicsDevice gd : gs) {228final GraphicsConfiguration[] gc = gd.getConfigurations();229for (final GraphicsConfiguration element0 : gc) {230final Rectangle gcBounds = element0.getBounds();231if (gcBounds.intersects(comboBoxBounds)) {232return getAvailableScreenArea(gcBounds, element0);233}234}235}236237return null;238}239240private Rectangle getAvailableScreenArea(Rectangle bounds,241GraphicsConfiguration gc) {242Insets insets = Toolkit.getDefaultToolkit().getScreenInsets(gc);243return new Rectangle(bounds.x + insets.left, bounds.y + insets.top,244bounds.width - insets.left - insets.right,245bounds.height - insets.top - insets.bottom);246}247248private int getComboBoxEdge(int py, boolean bottom) {249int offset = bottom ? 9 : -9;250// if py is less than new y we have a clipped combo, so leave it alone.251return Math.min((py / 2) + offset, py);252}253254@Override255protected Rectangle computePopupBounds(int px, int py, int pw, int ph) {256final int itemCount = comboBox.getModel().getSize();257final boolean isPopdown = isPopdown();258final boolean isTableCellEditor = AquaComboBoxUI.isTableCellEditor(comboBox);259if (isPopdown && !isTableCellEditor) {260// place the popup just below the button, which is261// near the center of a large combo box262py = getComboBoxEdge(py, true);263}264265// px & py are relative to the combo box266267// **** Common calculation - applies to the scrolling and menu-style ****268final Point p = new Point(0, 0);269SwingUtilities.convertPointToScreen(p, comboBox);270//System.err.println("First Converting from point to screen: 0,0 is now " + p.x + ", " + p.y);271final Rectangle scrBounds = getBestScreenBounds(p);272//System.err.println("BestScreenBounds is " + scrBounds);273274// If the combo box is totally off screen, do whatever super does275if (scrBounds == null) return super.computePopupBounds(px, py, pw, ph);276277// line up with the bottom of the text field/button (or top, if we have to go above it)278// and left edge if left-to-right, right edge if right-to-left279final Insets comboBoxInsets = comboBox.getInsets();280final Rectangle comboBoxBounds = comboBox.getBounds();281282if (shouldScroll()) {283pw += 15;284}285286if (isPopdown) {287pw += 4;288}289290// the popup should be wide enough for the items but not wider than the screen it's on291final int minWidth = comboBoxBounds.width - (comboBoxInsets.left + comboBoxInsets.right);292pw = Math.max(minWidth, pw);293294final boolean leftToRight = AquaUtils.isLeftToRight(comboBox);295if (leftToRight) {296px += comboBoxInsets.left;297if (!isPopDown) px -= FOCUS_RING_PAD_LEFT;298} else {299px = comboBoxBounds.width - pw - comboBoxInsets.right;300if (!isPopDown) px += FOCUS_RING_PAD_RIGHT;301}302py -= (comboBoxInsets.bottom); //sja fix was +kInset303304// Make sure it's all on the screen - shift it by the amount it's off305p.x += px;306p.y += py; // Screen location of px & py307if (p.x < scrBounds.x) {308px = px + (scrBounds.x - p.x);309}310if (p.y < scrBounds.y) {311py = py + (scrBounds.y - p.y);312}313314final Point top = new Point(0, 0);315SwingUtilities.convertPointFromScreen(top, comboBox);316//System.err.println("Converting from point to screen: 0,0 is now " + top.x + ", " + top.y);317318// Since the popup is at zero in this coord space, the maxWidth == the X coord of the screen right edge319// (it might be wider than the screen, if the combo is off the left edge)320final int maxWidth = Math.min(scrBounds.width, top.x + scrBounds.x + scrBounds.width) - 2; // subtract some buffer space321322pw = Math.min(maxWidth, pw);323if (pw < minWidth) {324px -= (minWidth - pw);325pw = minWidth;326}327328// this is a popup window, and will continue calculations below329if (!isPopdown) {330// popup windows are slightly inset from the combo end-cap331pw -= 6;332return computePopupBoundsForMenu(px, py, pw, ph, itemCount, scrBounds);333}334335// don't attempt to inset table cell editors336if (!isTableCellEditor) {337pw -= (FOCUS_RING_PAD_LEFT + FOCUS_RING_PAD_RIGHT);338if (leftToRight) {339px += FOCUS_RING_PAD_LEFT;340}341}342343final Rectangle r = new Rectangle(px, py, pw, ph);344if (r.y + r.height < top.y + scrBounds.y + scrBounds.height) {345return r;346}347// Check whether it goes below the bottom of the screen, if so flip it348int newY = getComboBoxEdge(comboBoxBounds.height, false) - ph - comboBoxInsets.top;349if (newY > top.y + scrBounds.y) {350return new Rectangle(px, newY, r.width, r.height);351} else {352// There are no place at top, move popup to the center of the screen353r.y = top.y + scrBounds.y + Math.max(0, (scrBounds.height - ph) / 2 );354r.height = Math.min(scrBounds.height, ph);355}356return r;357}358359// The one to use when itemCount <= maxRowCount. Size never adjusts for arrows360// We want it positioned so the selected item is right above the combo box361protected Rectangle computePopupBoundsForMenu(final int px, final int py,362final int pw, final int ph,363final int itemCount,364final Rectangle scrBounds) {365//System.err.println("computePopupBoundsForMenu: " + px + "," + py + " " + pw + "," + ph);366//System.err.println("itemCount: " +itemCount +" src: "+ scrBounds);367int elementSize = 0; //kDefaultItemSize;368if (list != null && itemCount > 0) {369final Rectangle cellBounds = list.getCellBounds(0, 0);370if (cellBounds != null) elementSize = cellBounds.height;371}372373int offsetIndex = comboBox.getSelectedIndex();374if (offsetIndex < 0) offsetIndex = 0;375list.setSelectedIndex(offsetIndex);376377final int selectedLocation = elementSize * offsetIndex;378379final Point top = new Point(0, scrBounds.y);380final Point bottom = new Point(0, scrBounds.y + scrBounds.height - 20); // Allow some slack381SwingUtilities.convertPointFromScreen(top, comboBox);382SwingUtilities.convertPointFromScreen(bottom, comboBox);383384final Rectangle popupBounds = new Rectangle(px, py, pw, ph);// Relative to comboBox385386final int theRest = ph - selectedLocation;387388// If the popup fits on the screen and the selection appears under the mouse w/o scrolling, cool!389// If the popup won't fit on the screen, adjust its position but not its size390// and rewrite this to support arrows - JLists always move the contents so they all show391392// Test to see if it extends off the screen393final boolean extendsOffscreenAtTop = selectedLocation > -top.y;394final boolean extendsOffscreenAtBottom = theRest > bottom.y;395396if (extendsOffscreenAtTop) {397popupBounds.y = top.y + 1;398// Round it so the selection lines up with the combobox399popupBounds.y = (popupBounds.y / elementSize) * elementSize;400} else if (extendsOffscreenAtBottom) {401// Provide blank space at top for off-screen stuff to scroll into402popupBounds.y = bottom.y - popupBounds.height; // popupBounds.height has already been adjusted to fit403} else { // fits - position it so the selectedLocation is under the mouse404popupBounds.y = -selectedLocation;405}406407// Center the selected item on the combobox408final int height = comboBox.getHeight();409final Insets insets = comboBox.getInsets();410final int buttonSize = height - (insets.top + insets.bottom);411final int diff = (buttonSize - elementSize) / 2 + insets.top;412popupBounds.y += diff - FOCUS_RING_PAD_BOTTOM;413414return popupBounds;415}416}417418419