/* $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.LookAndFeel;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import org.dyno.visual.swing.base.ISyncUITask;
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.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.
*/
@SuppressWarnings("unchecked")
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;
}
}
public static synchronized Object runWithLnf(LookAndFeel lnf,
ISyncUITask task) throws Throwable {
UIManager.setLookAndFeel(lnf);
return task.doTask();
}
static private void installDisposeHandler(final Display display) {
if (!display.isDisposed()) {
display.addListener(SWT.Dispose, new Listener() {
public void handleEvent(Event event) {
removeInstance(display);
}
});
}
}
static private void removeInstance(final Display display) {
synchronized (environmentMap) {
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 (InterruptedException e) {
SWT.error(SWT.ERROR_FAILED_EXEC, e);
} catch (InvocationTargetException e) {
SWT.error(SWT.ERROR_FAILED_EXEC, e.getTargetException());
}
// Listen for AWT modal dialogs to make them modal application-wide
dialogListener = new AwtDialogListener(display);
// Dismiss AWT popups when SWT menus are shown
initSwingPopupsDismissal();
globalFocusHandler = new GlobalFocusHandler(display);
}
void dispose() {
dialogListener.dispose();
if (popupParent != null) {
popupParent.setVisible(false);
popupParent.dispose();
}
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 (ClassNotFoundException e) {
SWT.error(SWT.ERROR_NOT_IMPLEMENTED, e);
} catch (InstantiationException e) {
SWT.error(SWT.ERROR_NOT_IMPLEMENTED, e);
} catch (IllegalAccessException e) {
SWT.error(SWT.ERROR_NOT_IMPLEMENTED, e);
} catch (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) {
display.asyncExec(new Runnable() {
public void run() {
display.addFilter(SWT.Show, 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(Event event) {
EventQueue.invokeLater(new Runnable() {
public void run() {
hidePopups();
}
});
}
};
// Returns true if any popup has been hidden
protected boolean hidePopups() {
boolean result = false;
List popups = new ArrayList();
assert EventQueue.isDispatchThread(); // On AWT event thread
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 (Iterator iter = popups.iterator(); iter.hasNext();) {
Component popup = (Component)iter.next();
if (popup.isVisible()) {
result = true;
//popup.setVisible(false);
}
}
return result;
}
protected void findOwnedPopups(Window window, List popups) {
assert window != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
Window[] ownedWindows = window.getOwnedWindows();
for (int i = 0; i < ownedWindows.length; i++) {
findContainedPopups(ownedWindows[i], popups);
findOwnedPopups(ownedWindows[i], popups);
}
}
protected void findContainedPopups(Container container, List popups) {
assert container != null;
assert popups != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
Component[] components = container.getComponents();
for (int i = 0; i < components.length; i++) {
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 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 (!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(display, new Runnable() {
public void run() {
// Unblock SWT
SwtInputBlocker.unblock();
}
});
}
}
});
// Prevent user input on SWT components
SwtInputBlocker.block(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 (!display.equals(Display.getCurrent())) {
SWT.error(SWT.ERROR_THREAD_INVALID_ACCESS);
}
Shell parent = 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(Shell parent) {
if (parent == null) {
SWT.error(SWT.ERROR_NULL_ARGUMENT);
}
if (!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);
Composite composite = new Composite(shell, SWT.EMBEDDED);
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() {
public void windowClosed(WindowEvent e) {
if (!display.isDisposed()) {
ThreadingHandler.getInstance().asyncExec(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(SwingControl control) {
if (Platform.isGtk()) {
if (true && (popupParent == null)) {
// System.err.println("*** Creating separate popup parent shell");
popupParent = new Shell(display, SWT.NO_TRIM | SWT.NO_FOCUS | SWT.ON_TOP);
popupParent.setSize(0, 0);
}
return popupParent;
} else {
return control.getShell();
}
}
// ----------------------- Focus Handling ------------------------------------------
protected GlobalFocusHandler getGlobalFocusHandler() {
return globalFocusHandler;
}
}