/* $Id: AwtEnvironment.java,v 1.27 2008/08/04 20:23:07 ghirsch Exp $ */ /******************************************************************************* * Copyright (c) 2007-2008 SAS Institute Inc., ILOG S.A. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * SAS Institute Inc. - initial API and implementation * ILOG S.A. - initial API and implementation *******************************************************************************/ package org.eclipse.albireo.core; import java.awt.Component; import java.awt.Container; import java.awt.EventQueue; import java.awt.Frame; import java.awt.KeyboardFocusManager; import java.awt.Window; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.lang.reflect.InvocationTargetException; import java.util.ArrayList; import java.util.HashMap; import java.util.Iterator; import java.util.List; import java.util.Map; import javax.swing.JPopupMenu; import javax.swing.UnsupportedLookAndFeelException; import org.eclipse.albireo.internal.AwtDialogListener; import org.eclipse.albireo.internal.FocusDebugging; import org.eclipse.albireo.internal.FocusHandler; import org.eclipse.albireo.internal.GlobalFocusHandler; import org.eclipse.albireo.internal.Platform; import org.eclipse.albireo.internal.SwtInputBlocker; import org.eclipse.swt.SWT; import org.eclipse.swt.SWTException; import org.eclipse.swt.awt.SWT_AWT; import org.eclipse.swt.graphics.Font; import org.eclipse.swt.graphics.FontData; import org.eclipse.swt.layout.FillLayout; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; /** * An environment to enable the proper display of AWT/Swing windows within a SWT or RCP * application. This class extends the base {@link org.eclipse.swt.awt.SWT_AWT Eclipse SWT/AWT integration} * support by * <ul> * <li>Using the platform-specific system Look and Feel. * <li>more later... * </ul> * <p> * This class is most helpful to applications which create new AWT/Swing windows (e.g. dialogs) rather * than those which embed AWT/Swing components in SWT windows. For support specific to embedding * AWT/Swing components see {@link SwingControl}. * <p> * There is at most one instance of this class per SWT * {@link org.eclipse.swt.widgets.Display Display}. In most applications * this means that there is exactly one instance for the entire application. * <p> * An instance of this class can be obtained with the static * {@link #getInstance(Display)} method. */ public final class AwtEnvironment { // ======================= Instances of this class ======================= // Map from Display to AwtEnvironment. // This does not need to be a WeakHashMap: Display instances don't go away // silently; they are disposed, and we install a Dispose listener. private static Map /* Display -> AwtEnvironment */environmentMap = new HashMap (); /** * Returns the single instance of AwtEnvironment for the given display. On * the first call to this method, the necessary initialization to allow * AWT/Swing code to run properly within an Eclipse application is done. * This initialization includes setting the approprite look and feel and * registering the necessary listeners to ensure proper behavior of modal * dialogs. * <p> * The first call to this method must occur before any AWT/Swing APIs are * called. * * @param display * the non-null SWT display * @return the AWT environment * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the display is null</li> * </ul> */ public static AwtEnvironment getInstance ( final Display display ) { // For now assume a single display. If necessary, this implementation // can be changed to create multiple environments for multiple display // applications. if ( display == null ) { SWT.error ( SWT.ERROR_NULL_ARGUMENT ); } synchronized ( environmentMap ) { AwtEnvironment instance = (AwtEnvironment)environmentMap.get ( display ); if ( instance == null ) { instance = new AwtEnvironment ( display ); environmentMap.put ( display, instance ); ThreadingHandler.getInstance ().asyncExec ( display, new Runnable () { public void run () { installDisposeHandler ( display ); } } ); } return instance; } } static private void installDisposeHandler ( final Display display ) { if ( !display.isDisposed () ) { display.addListener ( SWT.Dispose, new Listener () { public void handleEvent ( final Event event ) { removeInstance ( display ); } } ); } } static private void removeInstance ( final Display display ) { synchronized ( environmentMap ) { final AwtEnvironment instance = (AwtEnvironment)environmentMap.remove ( display ); if ( instance != null ) { instance.dispose (); } } } // ============================= Constructor ============================= private final Display display; private final AwtDialogListener dialogListener; private final GlobalFocusHandler globalFocusHandler; // Private constructor - clients use getInstance() to obtain instances private AwtEnvironment ( final Display display ) { assert display != null; this.display = display; /* * This property removes a large amount of flicker from embedded swing * components in JDK 1.4 and 1.5. Ideally it would be set lazily, * but since its value is read once and cached by AWT, it needs * to be set before any AWT/Swing APIs are called. * This setting is no longer needed in JDK 1.6. */ // TODO: this is effective only on Windows. System.setProperty ( "sun.awt.noerasebackground", "true" ); //$NON-NLS-1$//$NON-NLS-2$ /* * It's important to wait for the L&F to be set so that any subsequent calls * to SwingControl.createFrame() will be return a frame with the proper L&F (note * that createFrame() happens on the SWT thread). * * The calls to syncExec and invokeAndWait are safe because * the first call AwtEnvironment.getInstance should happen * before any (potential deadlocking) activity occurs on the * AWT thread. */ final Font[] initialFont = new Font[1]; display.syncExec ( new Runnable () { public void run () { initialFont[0] = display.getSystemFont (); } } ); final Font swtFont = initialFont[0]; final FontData[] swtFontData = swtFont.getFontData (); try { EventQueue.invokeAndWait ( new Runnable () { public void run () { setLookAndFeel (); LookAndFeelHandler.getInstance ().propagateSwtFont ( swtFont, swtFontData ); if ( FocusHandler.verboseKFHEvents ) { FocusDebugging.enableKeyboardFocusManagerLogging (); } } } ); } catch ( final InterruptedException e ) { SWT.error ( SWT.ERROR_FAILED_EXEC, e ); } catch ( final InvocationTargetException e ) { SWT.error ( SWT.ERROR_FAILED_EXEC, e.getTargetException () ); } // Listen for AWT modal dialogs to make them modal application-wide this.dialogListener = new AwtDialogListener ( display ); // Dismiss AWT popups when SWT menus are shown initSwingPopupsDismissal (); this.globalFocusHandler = new GlobalFocusHandler ( display ); } void dispose () { this.dialogListener.dispose (); if ( this.popupParent != null ) { this.popupParent.setVisible ( false ); this.popupParent.dispose (); } this.globalFocusHandler.dispose (); } // ======================= Look&Feel initialization ======================= // Mostly delegated to the LookAndFeelHandler. private static boolean isLookAndFeelInitialized = false; static private void setLookAndFeel () { assert EventQueue.isDispatchThread (); // On AWT event thread if ( !isLookAndFeelInitialized ) { isLookAndFeelInitialized = true; try { LookAndFeelHandler.getInstance ().setLookAndFeel (); } catch ( final ClassNotFoundException e ) { SWT.error ( SWT.ERROR_NOT_IMPLEMENTED, e ); } catch ( final InstantiationException e ) { SWT.error ( SWT.ERROR_NOT_IMPLEMENTED, e ); } catch ( final IllegalAccessException e ) { SWT.error ( SWT.ERROR_NOT_IMPLEMENTED, e ); } catch ( final UnsupportedLookAndFeelException e ) { SWT.error ( SWT.ERROR_NOT_IMPLEMENTED, e ); } } } //==================== Swing Popup Management ================================ // (Note there are no known problems with AWT popups (java.awt.PopupMenu), so this code // ignores them) /* * Dismiss AWT popups when SWT menus are shown (not needed in JDK1.6) */ private static final boolean HIDE_SWING_POPUPS_ON_SWT_MENU_OPEN = Platform.isGtk () && Platform.JAVA_VERSION < Platform.javaVersion ( 1, 6, 0 ) || // GTK: pre-Java1.6 Platform.isWin32 (); // Win32: all JDKs private void initSwingPopupsDismissal () { if ( HIDE_SWING_POPUPS_ON_SWT_MENU_OPEN ) { this.display.asyncExec ( new Runnable () { public void run () { AwtEnvironment.this.display.addFilter ( SWT.Show, AwtEnvironment.this.menuListener ); } } ); } } // This listener helps ensure that Swing popup menus are properly dismissed when // a menu item off the SWT main menu bar (or tool bar) is shown. private final Listener menuListener = new Listener () { public void handleEvent ( final Event event ) { EventQueue.invokeLater ( new Runnable () { public void run () { hidePopups (); } } ); } }; // Returns true if any popup has been hidden protected boolean hidePopups () { boolean result = false; final List popups = new ArrayList (); assert EventQueue.isDispatchThread (); // On AWT event thread final Window window = KeyboardFocusManager.getCurrentKeyboardFocusManager ().getFocusedWindow (); if ( window == null ) { return false; } // Look for popups inside the frame's component hierarchy. // Lightweight popups will be found here. findContainedPopups ( window, popups ); // Also look for popups in the frame's window hierachy. // Heavyweight popups will be found here. findOwnedPopups ( window, popups ); // System.err.println("Hiding popups, count=" + popups.size()); for ( final Iterator iter = popups.iterator (); iter.hasNext (); ) { final Component popup = (Component)iter.next (); if ( popup.isVisible () ) { result = true; popup.setVisible ( false ); } } return result; } protected void findOwnedPopups ( final Window window, final List popups ) { assert window != null; assert EventQueue.isDispatchThread (); // On AWT event thread final Window[] ownedWindows = window.getOwnedWindows (); for ( int i = 0; i < ownedWindows.length; i++ ) { findContainedPopups ( ownedWindows[i], popups ); findOwnedPopups ( ownedWindows[i], popups ); } } protected void findContainedPopups ( final Container container, final List popups ) { assert container != null; assert popups != null; assert EventQueue.isDispatchThread (); // On AWT event thread final Component[] components = container.getComponents (); for ( int i = 0; i < components.length; i++ ) { final Component c = components[i]; // JPopupMenu is a container, so check for it first if ( c instanceof JPopupMenu ) { popups.add ( c ); } else if ( c instanceof Container ) { findContainedPopups ( (Container)c, popups ); } } } // =========================== Other useful API =========================== // -------------------------- Modal AWT Dialogs -------------------------- /** * Invokes the given runnable in the AWT event thread while blocking user * input on the SWT event thread. The SWT event thread will remain blocked * until the runnable task completes, at which point this method will * return. * <p> * This method is useful for displayng modal AWT/Swing dialogs from the SWT * event thread. The modal AWT/Swing dialog will always block input across * the whole application, but not until it appears. By calling this method, * it is guaranteed that SWT input is blocked immediately, even before the * AWT/Swing dialog appears. * <p> * To avoid unnecessary flicker, AWT/Swing dialogs should have their parent * set to a frame returned by {@link #createDialogParentFrame()}. * <p> * This method must be called from the SWT event thread. * * @param runnable * the code to schedule on the AWT event thread * @exception IllegalArgumentException * <ul> * <li>ERROR_NULL_ARGUMENT - if the runnable is null</li> * </ul> * @exception SWTException * <ul> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * SWT event thread * </ul> */ public void invokeAndBlockSwt ( final Runnable runnable ) { assert this.display != null; /* * This code snippet is based on the following thread on * news.eclipse.platform.swt: * http://dev.eclipse.org/newslists/news.eclipse.platform.swt/msg24234.html */ if ( runnable == null ) { SWT.error ( SWT.ERROR_NULL_ARGUMENT ); } if ( !this.display.equals ( Display.getCurrent () ) ) { SWT.error ( SWT.ERROR_THREAD_INVALID_ACCESS ); } // Switch to the AWT thread... EventQueue.invokeLater ( new Runnable () { public void run () { try { // do swing work... runnable.run (); } finally { ThreadingHandler.getInstance ().asyncExec ( AwtEnvironment.this.display, new Runnable () { public void run () { // Unblock SWT SwtInputBlocker.unblock (); } } ); } } } ); // Prevent user input on SWT components SwtInputBlocker.block ( this.dialogListener ); } /** * Creates an AWT frame suitable as a parent for AWT/Swing dialogs. * <p> * This method must be called from the SWT event thread. There must be an active * shell associated with the environment's display. * <p> * The created frame is a non-visible child of the active shell and will be disposed when that shell * is disposed. * <p> * See {@link #createDialogParentFrame(Shell)} for more details. * * @return a {@link java.awt.Frame} to be used for parenting dialogs * @exception SWTException * <ul> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * SWT event thread * </ul> * @exception IllegalStateException * if the current display has no shells */ public Frame createDialogParentFrame () { if ( !this.display.equals ( Display.getCurrent () ) ) { SWT.error ( SWT.ERROR_THREAD_INVALID_ACCESS ); } final Shell parent = this.display.getActiveShell (); if ( parent == null ) { throw new IllegalStateException ( "No Active Shell" ); //$NON-NLS-1$ } return createDialogParentFrame ( parent ); } /** * Creates an AWT frame suitable as a parent for AWT/Swing dialogs. * <p> * This method must be called from the SWT event thread. There must be an active * shell associated with the environment's display. * <p> * The created frame is a non-visible child of the given shell and will be disposed when that shell * is disposed. * <p> * This method is useful for creating a frame to parent any AWT/Swing * dialogs created for use inside a SWT application. A modal AWT/Swing * dialogs will behave better if its parent is set to the returned frame * rather than to null or to an independently created {@link java.awt.Frame}. * <p> * The frame is positioned such that its child AWT dialogs are centered over the given * parent shell's position <i>when this method is called</i>. If the parent frame is * later moved, the child will no longer be properly positioned. For best results, * create a new frame with this method immediately before creating and displaying each * child AWT/Swing dialog. * <p> * As with any AWT window, the returned frame must be explicitly disposed. * * @param parent - the SWT parent shell of the shell that will contain the returned frame * @return a {@link java.awt.Frame} to be used for parenting dialogs * @exception SWTException * <ul> * <li>ERROR_THREAD_INVALID_ACCESS - if not called from the * SWT event thread * </ul> * @exception IllegalStateException * if the current display has no shells */ public Frame createDialogParentFrame ( final Shell parent ) { if ( parent == null ) { SWT.error ( SWT.ERROR_NULL_ARGUMENT ); } if ( !this.display.equals ( Display.getCurrent () ) ) { SWT.error ( SWT.ERROR_THREAD_INVALID_ACCESS ); } // SWT.ON_TOP worked great for AWT print/page setup dialogs, but not for // other dialogs, so it has been removed. final Shell shell = new Shell ( parent, SWT.NO_TRIM | SWT.APPLICATION_MODAL ); final Composite composite = new Composite ( shell, SWT.EMBEDDED ); final Frame frame = SWT_AWT.new_Frame ( composite ); // Position and size the shell and embedded composite. This ensures that // any child dialogs will be shown in the proper position, relative to the // parent shell. shell.setLocation ( parent.getLocation () ); // On Gtk, if the embedded frame is never made visible, its child dialog // will not be positioned correctly on the screen. (The frame's // getLocationOnScreen() method will always return 0,0). To work around // this problem, temporarily make the shell (and frame) visible. To // avoid flicker, temporarily set the size to 0. // (Note: the shell location must be correctly set before this will work) if ( Platform.isGtk () ) { shell.setSize ( 0, 0 ); shell.setVisible ( true ); shell.setVisible ( false ); } shell.setSize ( parent.getSize () ); shell.setLayout ( new FillLayout () ); shell.layout (); // Clean up the shell that was created above on dispose of the frame frame.addWindowListener ( new WindowAdapter () { @Override public void windowClosed ( final WindowEvent e ) { if ( !AwtEnvironment.this.display.isDisposed () ) { ThreadingHandler.getInstance ().asyncExec ( AwtEnvironment.this.display, new Runnable () { public void run () { shell.dispose (); } } ); } } } ); return frame; } // ------------------------ Displaying SWT popups ------------------------ // Lazily created holder for an SWT popup. private Shell popupParent; /** * Returns a suitable parent shell for a SWT menu attached to a Swing control. * Use the return value from this method to create any SWT menus that * are used in calls to * {@link SwtPopupRegistry#setMenu(Component, boolean, org.eclipse.swt.widgets.Menu)}. * Otherwise, the popup menu may not display on some platforms. * * @param control the SwingControl that owns the AWT component which will have * a menu attached. * @return */ public Shell getSwtPopupParent ( final SwingControl control ) { if ( Platform.isGtk () ) { if ( true && this.popupParent == null ) { // System.err.println("*** Creating separate popup parent shell"); this.popupParent = new Shell ( this.display, SWT.NO_TRIM | SWT.NO_FOCUS | SWT.ON_TOP ); this.popupParent.setSize ( 0, 0 ); } return this.popupParent; } else { return control.getShell (); } } // ----------------------- Focus Handling ------------------------------------------ protected GlobalFocusHandler getGlobalFocusHandler () { return this.globalFocusHandler; } }