001/* 002 * $Id: MacOSXPopupLocationFix.java 4019 2011-05-11 16:52:30Z 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 */ 021package org.jdesktop.swingx.autocomplete.workarounds; 022 023import java.awt.Component; 024import java.awt.GraphicsConfiguration; 025import java.awt.GraphicsDevice; 026import java.awt.GraphicsEnvironment; 027import java.awt.Insets; 028import java.awt.Point; 029import java.awt.Rectangle; 030import java.awt.Toolkit; 031 032import javax.swing.JComboBox; 033import javax.swing.JComponent; 034import javax.swing.JPopupMenu; 035import javax.swing.UIManager; 036import javax.swing.event.PopupMenuEvent; 037import 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 */ 052public 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 @Override 093 public void popupMenuWillBecomeVisible(PopupMenuEvent e) { 094 final JComponent popupComponent = (JComponent) e.getSource(); 095 fixPopupLocation(popupComponent); 096 } 097 @Override 098 public void popupMenuWillBecomeInvisible(PopupMenuEvent e) { 099 // do nothing 100 } 101 @Override 102 public void popupMenuCanceled(PopupMenuEvent e) { 103 // do nothing 104 } 105 } 106 107 /** 108 * Do the adjustment on the specified popupComponent immediately before 109 * it is displayed. 110 */ 111 private void fixPopupLocation(JComponent popupComponent) { 112 // we only need to fix Apple's aqua look and feel 113 if(popupComponent.getClass().getName().indexOf("apple.laf") != 0) { 114 return; 115 } 116 117 // put the popup right under the combo box so it looks like a 118 // normal Aqua combo box 119 Point comboLocationOnScreen = comboBox.getLocationOnScreen(); 120 int comboHeight = comboBox.getHeight(); 121 int popupY = comboLocationOnScreen.y + comboHeight; 122 123 // ...unless the popup overflows the screen, in which case we put it 124 // above the combobox 125 Rectangle screenBounds = new ScreenGeometry(comboBox).getScreenBounds(); 126 int popupHeight = popupComponent.getPreferredSize().height; 127 if(comboLocationOnScreen.y + comboHeight + popupHeight > screenBounds.x + screenBounds.height) { 128 popupY = comboLocationOnScreen.y - popupHeight; 129 } 130 131 popupComponent.setLocation(comboLocationOnScreen.x, popupY); 132 } 133 134 /** 135 * Figure out the dimensions of our screen. 136 * 137 * <p>This code is inspired by similar in 138 * <code>JPopupMenu.adjustPopupLocationToFitScreen()</code>. 139 * 140 * @author <a href="mailto:jesse@swank.ca">Jesse Wilson</a> 141 */ 142 private final static class ScreenGeometry { 143 144 final GraphicsConfiguration graphicsConfiguration; 145 final boolean aqua; 146 147 public ScreenGeometry(JComponent component) { 148 this.aqua = UIManager.getLookAndFeel().getName().indexOf("Aqua") != -1; 149 this.graphicsConfiguration = graphicsConfigurationForComponent(component); 150 } 151 152 /** 153 * Get the best graphics configuration for the specified point and component. 154 */ 155 private GraphicsConfiguration graphicsConfigurationForComponent(Component component) { 156 Point point = component.getLocationOnScreen(); 157 158 // try to find the graphics configuration for our point of interest 159 GraphicsEnvironment ge = GraphicsEnvironment.getLocalGraphicsEnvironment(); 160 GraphicsDevice[] gd = ge.getScreenDevices(); 161 for(int i = 0; i < gd.length; i++) { 162 if(gd[i].getType() != GraphicsDevice.TYPE_RASTER_SCREEN) continue; 163 GraphicsConfiguration defaultGraphicsConfiguration = gd[i].getDefaultConfiguration(); 164 if(!defaultGraphicsConfiguration.getBounds().contains(point)) continue; 165 return defaultGraphicsConfiguration; 166 } 167 168 // we couldn't find a graphics configuration, use the component's 169 return component.getGraphicsConfiguration(); 170 } 171 172 /** 173 * Get the bounds of where we can put a popup. 174 */ 175 public Rectangle getScreenBounds() { 176 Rectangle screenSize = getScreenSize(); 177 Insets screenInsets = getScreenInsets(); 178 179 return new Rectangle( 180 screenSize.x + screenInsets.left, 181 screenSize.y + screenInsets.top, 182 screenSize.width - screenInsets.left - screenInsets.right, 183 screenSize.height - screenInsets.top - screenInsets.bottom 184 ); 185 } 186 187 /** 188 * Get the bounds of the screen currently displaying the component. 189 */ 190 public Rectangle getScreenSize() { 191 // get the screen bounds and insets via the graphics configuration 192 if(graphicsConfiguration != null) { 193 return graphicsConfiguration.getBounds(); 194 } 195 196 // just use the toolkit bounds, it's less awesome but sufficient 197 return new Rectangle(Toolkit.getDefaultToolkit().getScreenSize()); 198 } 199 200 /** 201 * Fetch the screen insets, the off limits areas around the screen such 202 * as menu bar, dock or start bar. 203 */ 204 public Insets getScreenInsets() { 205 Insets screenInsets; 206 if(graphicsConfiguration != null) { 207 screenInsets = Toolkit.getDefaultToolkit().getScreenInsets(graphicsConfiguration); 208 } else { 209 screenInsets = new Insets(0, 0, 0, 0); 210 } 211 212 // tweak the insets for aqua, they're reported incorrectly there 213 if(aqua) { 214 int aquaBottomInsets = 21; // unreported insets, shown in screenshot, https://glazedlists.dev.java.net/issues/show_bug.cgi?id=332 215 int aquaTopInsets = 22; // for Apple menu bar, found via debugger 216 217 screenInsets.bottom = Math.max(screenInsets.bottom, aquaBottomInsets); 218 screenInsets.top = Math.max(screenInsets.top, aquaTopInsets); 219 } 220 221 return screenInsets; 222 } 223 } 224}