001 /*
002 * $Id: MacOSXPopupLocationFix.java 2379 2007-11-02 16:41:20Z kschaefe $
003 *
004 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
005 * Santa Clara, California 95054, U.S.A. All rights reserved.
006 *
007 * This library is free software; you can redistribute it and/or
008 * modify it under the terms of the GNU Lesser General Public
009 * License as published by the Free Software Foundation; either
010 * version 2.1 of the License, or (at your option) any later version.
011 *
012 * This library is distributed in the hope that it will be useful,
013 * but WITHOUT ANY WARRANTY; without even the implied warranty of
014 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
015 * Lesser General Public License for more details.
016 *
017 * You should have received a copy of the GNU Lesser General Public
018 * License along with this library; if not, write to the Free Software
019 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
020 */
021 package org.jdesktop.swingx.autocomplete.workarounds;
022
023 import java.awt.Component;
024 import java.awt.GraphicsConfiguration;
025 import java.awt.GraphicsDevice;
026 import java.awt.GraphicsEnvironment;
027 import java.awt.Insets;
028 import java.awt.Point;
029 import java.awt.Rectangle;
030 import java.awt.Toolkit;
031
032 import javax.swing.JComboBox;
033 import javax.swing.JComponent;
034 import javax.swing.JPopupMenu;
035 import javax.swing.UIManager;
036 import javax.swing.event.PopupMenuEvent;
037 import javax.swing.event.PopupMenuListener;
038
039 /**
040 * Fix a problem where the JComboBox's popup obscures its editor in the Mac OS X
041 * Aqua look and feel.
042 *
043 * <p>Installing this fix will resolve the problem for Aqua without having
044 * side-effects for other look-and-feels. It also supports dynamically changed
045 * look and feels.
046 *
047 * @see <a href="https://glazedlists.dev.java.net/issues/show_bug.cgi?id=332">Glazed Lists bug entry</a>
048 * @see <a href="https://swingx.dev.java.net/issues/show_bug.cgi?id=360">SwingX bug entry</a>
049 *
050 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
051 */
052 public final class MacOSXPopupLocationFix {
053
054 /** the components being fixed */
055 private final JComboBox comboBox;
056 private final JPopupMenu popupMenu;
057
058 /** the listener provides callbacks as necessary */
059 private final Listener listener = new Listener();
060
061 /**
062 * Private constructor so users use the more action-oriented
063 * {@link #install} method.
064 */
065 private MacOSXPopupLocationFix(JComboBox comboBox) {
066 this.comboBox = comboBox;
067 this.popupMenu = (JPopupMenu)comboBox.getUI().getAccessibleChild(comboBox, 0);
068
069 popupMenu.addPopupMenuListener(listener);
070 }
071
072 /**
073 * Install the fix for the specified combo box.
074 */
075 public static MacOSXPopupLocationFix install(JComboBox comboBox) {
076 if(comboBox == null) throw new IllegalArgumentException();
077 return new MacOSXPopupLocationFix(comboBox);
078 }
079
080 /**
081 * Uninstall the fix. Usually this is unnecessary since letting the combo
082 * box go out of scope is sufficient.
083 */
084 public void uninstall() {
085 popupMenu.removePopupMenuListener(listener);
086 }
087
088 /**
089 * Reposition the popup immediately before it is shown.
090 */
091 private class Listener implements PopupMenuListener {
092 public void popupMenuWillBecomeVisible(PopupMenuEvent e) {
093 final JComponent popupComponent = (JComponent) e.getSource();
094 fixPopupLocation(popupComponent);
095 }
096 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) {
097 // do nothing
098 }
099 public void popupMenuCanceled(PopupMenuEvent e) {
100 // do nothing
101 }
102 }
103
104 /**
105 * Do the adjustment on the specified popupComponent immediately before
106 * it is displayed.
107 */
108 private void fixPopupLocation(JComponent popupComponent) {
109 // we only need to fix Apple's aqua look and feel
110 if(popupComponent.getClass().getName().indexOf("apple.laf") != 0) {
111 return;
112 }
113
114 // put the popup right under the combo box so it looks like a
115 // normal Aqua combo box
116 Point comboLocationOnScreen = comboBox.getLocationOnScreen();
117 int comboHeight = comboBox.getHeight();
118 int popupY = comboLocationOnScreen.y + comboHeight;
119
120 // ...unless the popup overflows the screen, in which case we put it
121 // above the combobox
122 Rectangle screenBounds = new ScreenGeometry(comboBox).getScreenBounds();
123 int popupHeight = popupComponent.getPreferredSize().height;
124 if(comboLocationOnScreen.y + comboHeight + popupHeight > screenBounds.x + screenBounds.height) {
125 popupY = comboLocationOnScreen.y - popupHeight;
126 }
127
128 popupComponent.setLocation(comboLocationOnScreen.x, popupY);
129 }
130
131 /**
132 * Figure out the dimensions of our screen.
133 *
134 * <p>This code is inspired by similar in
135 * <code>JPopupMenu.adjustPopupLocationToFitScreen()</code>.
136 *
137 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a>
138 */
139 private final static class ScreenGeometry {
140
141 final GraphicsConfiguration graphicsConfiguration;
142 final boolean aqua;
143
144 public ScreenGeometry(JComponent component) {
145 this.aqua = UIManager.getLookAndFeel().getName().indexOf("Aqua") != -1;
146 this.graphicsConfiguration = graphicsConfigurationForComponent(component);
147 }
148
149 /**
150 * Get the best graphics configuration for the specified point and component.
151 */
152 private GraphicsConfiguration graphicsConfigurationForComponent(Component component) {
153 Point point = component.getLocationOnScreen();
154
155 // try to find the graphics configuration for our point of interest
156 GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment();
157 GraphicsDevice[] gd = ge.getScreenDevices();
158 for(int i = 0; i < gd.length; i++) {
159 if(gd[i].getType() != GraphicsDevice.TYPE_RASTER_SCREEN) continue;
160 GraphicsConfiguration defaultGraphicsConfiguration = gd[i].getDefaultConfiguration();
161 if(!defaultGraphicsConfiguration.getBounds().contains(point)) continue;
162 return defaultGraphicsConfiguration;
163 }
164
165 // we couldn't find a graphics configuration, use the component's
166 return component.getGraphicsConfiguration();
167 }
168
169 /**
170 * Get the bounds of where we can put a popup.
171 */
172 public Rectangle getScreenBounds() {
173 Rectangle screenSize = getScreenSize();
174 Insets screenInsets = getScreenInsets();
175
176 return new Rectangle(
177 screenSize.x + screenInsets.left,
178 screenSize.y + screenInsets.top,
179 screenSize.width - screenInsets.left - screenInsets.right,
180 screenSize.height - screenInsets.top - screenInsets.bottom
181 );
182 }
183
184 /**
185 * Get the bounds of the screen currently displaying the component.
186 */
187 public Rectangle getScreenSize() {
188 // get the screen bounds and insets via the graphics configuration
189 if(graphicsConfiguration != null) {
190 return graphicsConfiguration.getBounds();
191 }
192
193 // just use the toolkit bounds, it's less awesome but sufficient
194 return new Rectangle(Toolkit.getDefaultToolkit().getScreenSize());
195 }
196
197 /**
198 * Fetch the screen insets, the off limits areas around the screen such
199 * as menu bar, dock or start bar.
200 */
201 public Insets getScreenInsets() {
202 Insets screenInsets;
203 if(graphicsConfiguration != null) {
204 screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration);
205 } else {
206 screenInsets = new Insets(0, 0, 0, 0);
207 }
208
209 // tweak the insets for aqua, they're reported incorrectly there
210 if(aqua) {
211 int aquaBottomInsets = 21; // unreported insets, shown in screenshot, https://glazedlists.dev.java.net/issues/show_bug.cgi?id=332
212 int aquaTopInsets = 22; // for Apple menu bar, found via debugger
213
214 screenInsets.bottom = Math.max(screenInsets.bottom, aquaBottomInsets);
215 screenInsets.top = Math.max(screenInsets.top, aquaTopInsets);
216 }
217
218 return screenInsets;
219 }
220 }
221 }