/******************************************************************************* * 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.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 */ 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 ( final 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 ( final NoSuchMethodException e ) { handleAlwaysOnTopException ( e ); } } } protected void setAlwaysOnTop ( final Window window, final 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 ( final IllegalAccessException e ) { handleAlwaysOnTopException ( e ); } catch ( final InvocationTargetException e ) { handleAlwaysOnTopException ( e ); } } protected boolean isAlwaysOnTop ( final Window window ) { assert isAlwaysOnTopMethod != null; try { if ( verboseModalityHandling ) { System.err.println ( "Calling isAlwaysOnTop() for " + window ); } final Object result = isAlwaysOnTopMethod.invoke ( window, new Object[] {} ); return ( (Boolean)result ).booleanValue (); } catch ( final IllegalAccessException e ) { handleAlwaysOnTopException ( e ); return false; } catch ( final InvocationTargetException e ) { handleAlwaysOnTopException ( e ); return false; } } protected void handleAlwaysOnTopException ( final Exception e ) { if ( verboseModalityHandling ) { e.printStackTrace (); } } private void handleRemovedDialog ( final Dialog awtDialog, final boolean removeListener ) { assert awtDialog != null; assert this.modalDialogs != null; assert this.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 ( this.modalDialogs.remove ( awtDialog ) ) { ThreadingHandler.getInstance ().asyncExec ( this.display, new Runnable () { public void run () { SwtInputBlocker.unblock (); } } ); } } private void handleAddedDialog ( final Dialog awtDialog ) { assert awtDialog != null; assert this.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 ( this.modalDialogs.contains ( awtDialog ) || !awtDialog.isModal () || !awtDialog.isVisible () ) { return; } this.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 ( this.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 AwtDialogListener.this.modalDialogs != null; final int size = AwtDialogListener.this.modalDialogs.size (); if ( size > 0 ) { final Dialog awtDialog = (Dialog)AwtDialogListener.this.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 ( final 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 ( final WindowEvent event ) { assert event != null; assert EventQueue.isDispatchThread (); // On AWT event thread final Window window = event.getWindow (); if ( window instanceof Dialog ) { handleAddedDialog ( (Dialog)window ); } } private void handleClosedWindow ( final WindowEvent event ) { assert event != null; assert EventQueue.isDispatchThread (); // On AWT event thread // Dispose-based close final Window window = event.getWindow (); if ( window instanceof Dialog ) { // Remove dialog and component listener handleRemovedDialog ( (Dialog)window, true ); } } private void handleClosingWindow ( final WindowEvent event ) { assert event != null; assert EventQueue.isDispatchThread (); // On AWT event thread // System-based close final 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 ( final 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 ( final ComponentEvent e ) { assert e != null; assert EventQueue.isDispatchThread (); // On AWT event thread if ( verboseModalityHandling ) { System.err.println ( "Component hidden" ); } final 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 ( final ComponentEvent e ) { assert e != null; assert EventQueue.isDispatchThread (); // On AWT event thread if ( verboseModalityHandling ) { System.err.println ( "Component shown" ); } final Object obj = e.getSource (); if ( obj instanceof Dialog ) { handleAddedDialog ( (Dialog)obj ); } } public void componentResized ( final ComponentEvent e ) { // Ignore event } public void componentMoved ( final 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 ( final WindowEvent e ) { assert USING_ALWAYS_ON_TOP; setAlwaysOnTop ( e.getWindow (), true ); } public void windowLostFocus ( final WindowEvent e ) { assert USING_ALWAYS_ON_TOP; setAlwaysOnTop ( e.getWindow (), false ); } }