/*******************************************************************************
* 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.internal;
import java.awt.AWTEvent;
import java.awt.Component;
import java.awt.Dialog;
import java.awt.EventQueue;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.AWTEventListener;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.WindowEvent;
import java.awt.event.WindowFocusListener;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import org.eclipse.albireo.core.Platform;
import org.eclipse.albireo.core.SwingControl;
import org.eclipse.albireo.core.ThreadingHandler;
import org.eclipse.swt.widgets.Display;
/**
* A listener that insures the proper modal behavior of Swing dialogs when running
* within a SWT environment. When initialized, it blocks and unblocks SWT input
* as modal Swing dialogs are shown and hidden.
*
* @see SwtInputBlocker
*/
@SuppressWarnings("unchecked")
public class AwtDialogListener implements AWTEventListener, ComponentListener, WindowFocusListener {
private static boolean verboseModalityHandling = false;
protected static boolean USING_ALWAYS_ON_TOP = Platform.isGtk() &&
(Platform.JAVA_VERSION >= Platform.javaVersion(1, 5, 0));
private static boolean alwaysOnTopMethodsInitialized = false;
private static Method setAlwaysOnTopMethod = null;
private static Method isAlwaysOnTopMethod = null;
// modalDialogs should be accessed only from the AWT thread, so no
// synchronization is needed.
private final List modalDialogs = new ArrayList();
private final Display display;
/**
* Registers this object as an AWT event listener so that Swing dialogs have the
* proper modal behavior in the containing SWT environment. This is called automatically
* when you construct a {@link SwingControl}, and it
* need not be called separately in that case.
* @param shell
*/
public AwtDialogListener(Display display) {
assert display != null;
// In some cases, we use Window.setAlwaysOnTop to keep modal AWT dialogs visible
if (USING_ALWAYS_ON_TOP) {
getAlwaysOnTopMethods();
}
this.display = display;
Toolkit.getDefaultToolkit().addAWTEventListener(this, AWTEvent.WINDOW_EVENT_MASK);
}
protected void getAlwaysOnTopMethods() {
// These methods is only needed for Gtk, Reflection is used to allow compilation
// against JDK 1.4
if (!alwaysOnTopMethodsInitialized) {
alwaysOnTopMethodsInitialized = true;
try {
setAlwaysOnTopMethod = Window.class.getMethod("setAlwaysOnTop", new Class[]{boolean.class});
isAlwaysOnTopMethod = Window.class.getMethod("isAlwaysOnTop", new Class[]{});
} catch (NoSuchMethodException e) {
handleAlwaysOnTopException(e);
}
}
}
protected void setAlwaysOnTop(Window window, boolean onTop) {
assert setAlwaysOnTopMethod != null;
try {
if (verboseModalityHandling) {
System.err.println("Calling setAlwaysOnTop(" + onTop + ") for " + window);
}
setAlwaysOnTopMethod.invoke(window, new Object[] { new Boolean(onTop) });
} catch (IllegalAccessException e) {
handleAlwaysOnTopException(e);
} catch (InvocationTargetException e) {
handleAlwaysOnTopException(e);
}
}
protected boolean isAlwaysOnTop(Window window) {
assert isAlwaysOnTopMethod != null;
try {
if (verboseModalityHandling) {
System.err.println("Calling isAlwaysOnTop() for " + window);
}
Object result = isAlwaysOnTopMethod.invoke(window, new Object[] { });
return ((Boolean)result).booleanValue();
} catch (IllegalAccessException e) {
handleAlwaysOnTopException(e);
return false;
} catch (InvocationTargetException e) {
handleAlwaysOnTopException(e);
return false;
}
}
protected void handleAlwaysOnTopException(Exception e) {
if (verboseModalityHandling) {
e.printStackTrace();
}
}
private void handleRemovedDialog(Dialog awtDialog, boolean removeListener) {
assert awtDialog != null;
assert modalDialogs != null;
assert display != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
if (verboseModalityHandling) {
System.err.println("Remove dialog: " + awtDialog);
}
if (removeListener) {
awtDialog.removeComponentListener(this);
if (USING_ALWAYS_ON_TOP) {
awtDialog.removeWindowFocusListener(this);
}
}
// Note: there is no isModal() check here because the dialog might
// have been changed from modal to non-modal after it was opened. In this case
// the currently visible dialog would still act modal and we'd need to unblock
// SWT here when it goes away.
if (modalDialogs.remove(awtDialog)) {
ThreadingHandler.getInstance().asyncExec(display, new Runnable() {
public void run() {
SwtInputBlocker.unblock();
}
});
}
}
private void handleAddedDialog(final Dialog awtDialog) {
assert awtDialog != null;
assert modalDialogs != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
if (verboseModalityHandling) {
System.err.println("Add dialog: " + awtDialog);
}
// Don't block if
// 1) the the dialog has already triggered a block
// 2) the dialog is not modal, or
// 3) the dialog is not (yet?) visible
// It's not clear why/when case 3 would happen, but it has been reported and the consequences
// are severe if the check is not in place.
if (modalDialogs.contains(awtDialog) || !awtDialog.isModal() || !awtDialog.isVisible()) {
return;
}
modalDialogs.add(awtDialog);
awtDialog.addComponentListener(this);
// In some cases (e.g. GTK), we need to use the Window.setAlwaysOnTop
// method to force modal AWT dialogs in front of any SWT shells. Otherwise, they
// are easily hidden when clicking on the parent shell. It might be possible to
// remove this code if we could successfully move the SWT shell back in the z-order,
// but there is an open bug (on GTK) on Shell.moveBelow.
// See note in SwtInputBlocker.activateListener
// We use a listener to keep the always-on-top behavior enabled only while the
// dialog has focus. If the dialog is already always-on-top, we don't add a listener.
// TODO: we don't handle the case where always-on-top status is changed while the dialog
// is visible.
if (USING_ALWAYS_ON_TOP && !isAlwaysOnTop(awtDialog)) {
awtDialog.addWindowFocusListener(this);
}
ThreadingHandler.getInstance().asyncExec(display, new Runnable() {
public void run() {
SwtInputBlocker.block(AwtDialogListener.this);
}
});
}
void requestFocus() {
// TODO: in early testing this did not always bring the dialog to the top
// under some Linux desktops/window managers (e.g. metacity under GNOME).
// Re-test with recent changes
EventQueue.invokeLater(new Runnable() {
public void run() {
assert modalDialogs != null;
int size = modalDialogs.size();
if (size > 0) {
final Dialog awtDialog = (Dialog)modalDialogs.get(size - 1);
Component focusOwner = awtDialog.getMostRecentFocusOwner();
if (verboseModalityHandling) {
System.err.println("Bringing to front, focusOwner=" + focusOwner);
}
if (focusOwner == null) {
focusOwner = awtDialog; // try the dialog itself in this case
}
try {
// In one case, a call to requestFocus() alone does not
// bring the AWT dialog to the top. This happens if the
// dialog is given a null parent frame. When opened, the dialog
// can be hidden by the SWT window even when it obtains focus.
// Calling toFront() solves the problem.
focusOwner.requestFocus();
awtDialog.toFront();
} catch (NullPointerException e) {
// Some dialogs (e.g. Windows page setup and print dialogs on JDK 1.5+) throw an NPE on
// requestFocus(). There's no way to check ahead of time, so just swallow the NPE here.
}
}
}
});
}
private void handleOpenedWindow(WindowEvent event) {
assert event != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
Window window = event.getWindow();
if (window instanceof Dialog) {
handleAddedDialog((Dialog)window);
}
}
private void handleClosedWindow(WindowEvent event) {
assert event != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
// Dispose-based close
Window window = event.getWindow();
if (window instanceof Dialog) {
// Remove dialog and component listener
handleRemovedDialog((Dialog)window, true);
}
}
private void handleClosingWindow(WindowEvent event) {
assert event != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
// System-based close
Window window = event.getWindow();
if (window instanceof Dialog) {
final Dialog dialog = (Dialog) window;
// Defer until later. Bad things happen if
// handleRemovedDialog() is called directly from
// this event handler. The Swing dialog does not close
// properly and its modality remains in effect.
EventQueue.invokeLater(new Runnable() {
public void run() {
// Remove dialog and component listener
handleRemovedDialog(dialog, true);
}
});
}
}
public void dispose() {
Toolkit.getDefaultToolkit().removeAWTEventListener(this);
}
// ================== Implementation of AWTEventListener ==================
// This listener is permanently attached to the AWT Toolkit.
public void eventDispatched(AWTEvent event) {
assert event != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
switch (event.getID()) {
case WindowEvent.WINDOW_OPENED:
handleOpenedWindow((WindowEvent)event);
break;
case WindowEvent.WINDOW_CLOSED:
handleClosedWindow((WindowEvent)event);
break;
case WindowEvent.WINDOW_CLOSING:
handleClosingWindow((WindowEvent)event);
break;
default:
break;
}
}
// ================= Implementation of ComponentListener =================
// This listener is attached to modal Dialog instances while they are
// shown.
public void componentHidden(ComponentEvent e) {
assert e != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
if (verboseModalityHandling) {
System.err.println("Component hidden");
}
Object obj = e.getSource();
if (obj instanceof Dialog) {
// Remove dialog but keep listener in place so that we know if/when it is set visible
handleRemovedDialog((Dialog)obj, false);
}
}
public void componentShown(ComponentEvent e) {
assert e != null;
assert EventQueue.isDispatchThread(); // On AWT event thread
if (verboseModalityHandling) {
System.err.println("Component shown");
}
Object obj = e.getSource();
if (obj instanceof Dialog) {
handleAddedDialog((Dialog)obj);
}
}
public void componentResized(ComponentEvent e) {
// Ignore event
}
public void componentMoved(ComponentEvent e) {
// Ignore event
}
// ================ Implementation of WindowFocusListener ================
// This listener is attached to modal Dialog instances that are not already
// set to AlwaysOnTop, while they are shown, if USING_ALWAYS_ON_TOP is true.
public void windowGainedFocus(WindowEvent e) {
assert USING_ALWAYS_ON_TOP;
setAlwaysOnTop(e.getWindow(), true);
}
public void windowLostFocus(WindowEvent e) {
assert USING_ALWAYS_ON_TOP;
setAlwaysOnTop(e.getWindow(), false);
}
}