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 }