/******************************************************************************* * 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.AWTKeyStroke; import java.awt.Component; import java.awt.EventQueue; import java.awt.Frame; import java.awt.KeyEventDispatcher; import java.awt.KeyboardFocusManager; import java.awt.event.KeyEvent; import java.awt.event.WindowEvent; import java.awt.event.WindowFocusListener; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.Set; import javax.swing.text.Caret; import javax.swing.text.JTextComponent; import org.eclipse.albireo.core.SwingControl; import org.eclipse.albireo.core.ThreadingHandler; import org.eclipse.swt.SWT; import org.eclipse.swt.events.FocusEvent; import org.eclipse.swt.events.FocusListener; import org.eclipse.swt.widgets.Composite; import org.eclipse.swt.widgets.Control; import org.eclipse.swt.widgets.Display; import org.eclipse.swt.widgets.Event; import org.eclipse.swt.widgets.Listener; import org.eclipse.swt.widgets.Shell; public class FocusHandler { // =========================== Static variables =========================== // Whether to print debugging information regarding focus events. public static final boolean verboseFocusEvents = false; public static final boolean verboseKFHEvents = false; public static final boolean verboseTraverseOut = false; // synthesizeWindowActivation method on the frame's class (Win32 only, // JRE >= 1.5 only). // See <https://bugs.eclipse.org/bugs/show_bug.cgi?id=216431> // and <http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4922092>. private static boolean synthesizeMethodInitialized = false; private static Method synthesizeMethod = null; // ======================================================================== private final Frame frame; private final Composite borderless; // the Control corresponding to the frame private final SwingControl swingControl; // either borderless or its parent private final Display display; private final GlobalFocusHandler globalHandler; private boolean pendingTraverseOut = false; private int pendingTraverseOutSeqNum = 0; private int currentTraverseOutSeqNum = 0; private int extraTabCount = 0; private boolean isFocusedSwt; private boolean pendingDeactivate = false; // Listeners private final KeyEventDispatcher keyEventDispatcher = new AwtKeyDispatcher (); private final WindowFocusListener awtWindowFocusListener = new AwtWindowFocusListener (); private final FocusListener swtFocusListener = new SwtFocusListener (); private final Listener swtEventFilter = new SwtEventFilter (); public FocusHandler ( final SwingControl swingControl, final GlobalFocusHandler globalHandler, final Composite borderless, final Frame frame ) { this.globalHandler = globalHandler; assert Display.getCurrent () != null; // On SWT event thread if ( verboseFocusEvents ) { FocusDebugging.addFocusDebugListeners ( swingControl, frame ); } this.swingControl = swingControl; this.borderless = borderless; this.frame = frame; this.display = swingControl.getDisplay (); getSynthesizeMethod ( frame.getClass () ); globalHandler.addEventFilter ( this.swtEventFilter ); frame.addWindowFocusListener ( this.awtWindowFocusListener ); KeyboardFocusManager.getCurrentKeyboardFocusManager ().addKeyEventDispatcher ( this.keyEventDispatcher ); borderless.addFocusListener ( this.swtFocusListener ); } public void dispose () { this.globalHandler.removeEventFilter ( this.swtEventFilter ); this.frame.removeWindowFocusListener ( this.awtWindowFocusListener ); KeyboardFocusManager.getCurrentKeyboardFocusManager ().removeKeyEventDispatcher ( this.keyEventDispatcher ); this.borderless.removeFocusListener ( this.swtFocusListener ); } // ================ // On a normal change of focus, Swing will turn off any selection // in a text field to help indicate focus is lost. This won't happen // automatically when transferring to SWT, so turn off the selection // manually. protected void hideTextSelection () { assert EventQueue.isDispatchThread (); final Component focusOwner = this.frame.getMostRecentFocusOwner (); if ( focusOwner instanceof JTextComponent ) { final Caret caret = ( (JTextComponent)focusOwner ).getCaret (); if ( caret != null ) { caret.setSelectionVisible ( false ); } } } // ===== // Embedded frames in win32 do not support traverse out. For seamless embedding, we // check for the need to traverse out here and generate the necessary SWT traversal(s). // TODO: this should be optional protected boolean checkForTraverseOut ( final KeyEvent e ) { assert EventQueue.isDispatchThread (); // Ignore events outside this frame if ( this.frame.isFocused () ) { final Set traverseForwardKeys = this.frame.getFocusTraversalKeys ( KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS ); final Set traverseBackwardKeys = this.frame.getFocusTraversalKeys ( KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS ); final AWTKeyStroke key = AWTKeyStroke.getAWTKeyStrokeForEvent ( e ); if ( !this.pendingTraverseOut ) { // We haven't started to traverse out yet. Check to see if the traversal key has been // hit and that we are at the last/first compoent in the traversal group. Component limit = this.frame.getFocusTraversalPolicy ().getLastComponent ( this.frame ); if ( traverseForwardKeys.contains ( key ) && ( limit == e.getComponent () || limit == null ) ) { // Tabbing forward from last component in frame (or empty frame) this.pendingTraverseOut = true; this.pendingTraverseOutSeqNum++; swtTraverse ( SWT.TRAVERSE_TAB_NEXT, 1, false ); if ( verboseTraverseOut ) { trace ( "AWT: traversing out (forward)" ); } e.consume (); return true; } limit = this.frame.getFocusTraversalPolicy ().getFirstComponent ( this.frame ); if ( traverseBackwardKeys.contains ( key ) && ( limit == e.getComponent () || limit == null ) ) { // Tabbing backward from first component in frame this.pendingTraverseOut = true; this.pendingTraverseOutSeqNum++; swtTraverse ( SWT.TRAVERSE_TAB_PREVIOUS, 1, false ); if ( verboseTraverseOut ) { trace ( "AWT: traversing out (backward)" ); } e.consume (); return true; } } else { // We received a keystroke while the traverse out is pending. Record any tabs so that the // right number of SWT traversals can be done in the SWT thread. This prevents us from losing // tabs when typing them very fast. // TODO: this needs to be generalized to all keystrokes, not just traversals if ( traverseForwardKeys.contains ( key ) ) { if ( verboseTraverseOut ) { trace ( "forward traversal typeahead" ); } this.extraTabCount++; e.consume (); return true; } else if ( traverseBackwardKeys.contains ( key ) ) { if ( verboseTraverseOut ) { trace ( "backward traversal typeahead" ); } this.extraTabCount--; e.consume (); return true; } } } return false; } protected void processTypeAheadKeys ( final int callerSeqNum ) { if ( this.pendingTraverseOut && this.extraTabCount != 0 ) { if ( callerSeqNum != this.currentTraverseOutSeqNum + 1 ) { if ( verboseTraverseOut ) { trace ( "Discarding processTypeAhead request, sequence number out of sync " + callerSeqNum + "!=" + ( this.currentTraverseOutSeqNum + 1 ) + ", extraTabCount=" + this.extraTabCount ); } return; } if ( verboseTraverseOut ) { trace ( "Processing typeahead traversals, count=" + this.extraTabCount ); } final int direction = this.extraTabCount > 0 ? SWT.TRAVERSE_TAB_NEXT : SWT.TRAVERSE_TAB_PREVIOUS; swtTraverse ( direction, Math.abs ( this.extraTabCount ), true ); } this.pendingTraverseOut = false; this.currentTraverseOutSeqNum++; this.extraTabCount = 0; } protected void swtTraverse ( final int direction, final int count, final boolean flushingTypeAhead ) { assert EventQueue.isDispatchThread (); ThreadingHandler.getInstance ().asyncExec ( this.display, new Runnable () { public void run () { for ( int i = 0; i < count; i++ ) { doTraverse ( direction, flushingTypeAhead, FocusHandler.this.pendingTraverseOutSeqNum ); } } } ); } protected void doTraverse ( final int direction, final boolean flushingTypeAhead, final int seqNum ) { assert Display.getCurrent () != null; Control focusControl = this.display.getFocusControl (); if ( verboseTraverseOut ) { trace ( "SWT: traversing, control=" + focusControl ); } final SwingControl activeBorderless = this.globalHandler.getActiveEmbedded (); if ( focusControl == null && activeBorderless != null ) { focusControl = activeBorderless; if ( verboseTraverseOut ) { trace ( "SWT: current focus control is null; using=" + focusControl ); } } if ( focusControl != null ) { final boolean traverse = focusControl.traverse ( direction ); final Control newFocusControl = this.display.getFocusControl (); if ( traverse && newFocusControl == focusControl && newFocusControl == activeBorderless ) { // We were unable to traverse anywhere else. if ( verboseTraverseOut ) { trace ( "no-op traverse out, control=" + focusControl ); } // Queue up a request to empty the typeahead buffer. Normally this // happens when the AWT frame loses focus, but that won't happen here since // we did not traverse to a different SWT control if ( !flushingTypeAhead ) { EventQueue.invokeLater ( new Runnable () { public void run () { processTypeAheadKeys ( seqNum ); } } ); } // Traverse to the right component // inside the embedded frame, as if coming back from SWT // TODO: this case is not covered under Gtk because we rely on Swing's traverse out code // so none of this code is ever executed. adjustFocusForSwtTraversal ( direction ); } if ( verboseTraverseOut && !traverse ) { trace ( "traverse failed, from=" + focusControl ); } } } public boolean handleForceFocus ( boolean result ) { if ( Platform.isWin32 () ) { // On Windows, focus queries are unreliable while traversing SwingControls // In some cases this causes forceFocus() to incorrectly return false, causing // the SwingControl to be skipped while tabbing. Try to fix it here by resetting // the forceFocus result back to true when SWT events indicate we really do have // focus. if ( !result && this.globalHandler.getActiveWidget () == this.borderless && this.isFocusedSwt ) { // Force focus should have returned true result = true; if ( verboseTraverseOut ) { trace ( " resetting forceFocus return code to true" ); } } } return result; } // ==== Fix Eclipse bug 216431 on pre-3.4 // XEmbeddedFrame does not implement the synthesize method, and the Eclipse bug was fixed only for // win32, so we do the same in the methods below /** * This method duplicates the behavior of recent versions of Windows SWT_AWT * when the embedded Composite is activated or deactivated. It is used here to * workaround bugs in earlier (pre-3.4) versions of SWT, and to handle cases * where the Composite is not properly activated/deactivated, even today. See * the callers of this method for more information. * * @param activate <code>true</code> if the embedded frame whould be activated; * <code>false</code> otherwise * @return */ protected void synthesizeWindowActivation ( final boolean activate ) { assert Display.getCurrent () != null; // On SWT event thread assert Platform.isWin32 (); // Only done on Windows EventQueue.invokeLater ( new Runnable () { public void run () { if ( synthesizeMethod != null ) { // synthesizeWindowActivation() is available. Use it. Normally, this is on // Java 1.5 and higher. try { if ( synthesizeMethod != null ) { if ( verboseFocusEvents ) { trace ( "Calling synthesizeWindowActivation(" + activate + ")" ); } synthesizeMethod.invoke ( FocusHandler.this.frame, new Object[] { new Boolean ( activate ) } ); } } catch ( final IllegalAccessException e ) { handleSynthesizeException ( e ); } catch ( final InvocationTargetException e ) { handleSynthesizeException ( e ); } } else { if ( activate ) { FocusHandler.this.frame.dispatchEvent ( new WindowEvent ( FocusHandler.this.frame, WindowEvent.WINDOW_ACTIVATED ) ); FocusHandler.this.frame.dispatchEvent ( new WindowEvent ( FocusHandler.this.frame, WindowEvent.WINDOW_GAINED_FOCUS ) ); } else { FocusHandler.this.frame.dispatchEvent ( new WindowEvent ( FocusHandler.this.frame, WindowEvent.WINDOW_LOST_FOCUS ) ); FocusHandler.this.frame.dispatchEvent ( new WindowEvent ( FocusHandler.this.frame, WindowEvent.WINDOW_DEACTIVATED ) ); } } } } ); } private void getSynthesizeMethod ( final Class clazz ) { if ( Platform.isWin32 () && !synthesizeMethodInitialized ) { synthesizeMethodInitialized = true; try { synthesizeMethod = clazz.getMethod ( "synthesizeWindowActivation", new Class[] { boolean.class } ); } catch ( final NoSuchMethodException e ) { handleSynthesizeException ( e ); } } } private void handleSynthesizeException ( final Exception e ) { if ( verboseFocusEvents ) { e.printStackTrace (); } } // ==== // When the embedded frame is activated for any reason, focus will return to the most // recently focused component. However, if the activation was a result of a SWT traversal // operation, it will make more sense to reset the focus to the first or last component. // This is an optional behavior, controlled through SwingControl.setSwtTabOrderExtended protected void adjustFocusForSwtTraversal ( final int currentSwtTraversal ) { assert Display.getCurrent () != null; if ( !this.swingControl.isSwtTabOrderExtended () ) { return; } switch ( currentSwtTraversal ) { case SWT.TRAVERSE_TAB_NEXT: case SWT.TRAVERSE_ARROW_NEXT: case SWT.TRAVERSE_PAGE_NEXT: setInitialTraversalFocus ( true ); break; case SWT.TRAVERSE_TAB_PREVIOUS: case SWT.TRAVERSE_ARROW_PREVIOUS: case SWT.TRAVERSE_PAGE_PREVIOUS: setInitialTraversalFocus ( false ); break; } } protected void setInitialTraversalFocus ( final boolean forward ) { assert Display.getCurrent () != null; EventQueue.invokeLater ( new Runnable () { public void run () { Component component; if ( forward ) { component = FocusHandler.this.frame.getFocusTraversalPolicy ().getFirstComponent ( FocusHandler.this.frame ); } else { component = FocusHandler.this.frame.getFocusTraversalPolicy ().getLastComponent ( FocusHandler.this.frame ); } if ( verboseFocusEvents ) { trace ( "Setting AWT focus on SWT traversal, forward=" + forward + ", component=" + component ); } if ( component != null ) { component.requestFocus (); } } } ); } // ==== /** * Activates the embedded AWT frame, as long as the parent SWT composite has focus and is part of * the active SWT shell. */ public void activateEmbeddedFrame () { assert Display.getCurrent () != null; final Shell activeShell = this.globalHandler.getActiveShell (); final SwingControl activeBorderless = this.globalHandler.getActiveEmbedded (); if ( !this.borderless.isDisposed () && activeShell == this.borderless.getShell () && this.borderless == this.display.getFocusControl () && ( activeBorderless == null || activeBorderless == this.borderless ) ) { EventQueue.invokeLater ( new Runnable () { public void run () { // Ideally, we would use frame.toFront() here, but that method // is a no-op for sun.awt.EmbeddedFrame. The next best thing // is to request focus on the result of getMostRecentFocusOwner // which will preserve any existing focus, or otherwise use the initial // component. final Component component = FocusHandler.this.frame.getMostRecentFocusOwner (); if ( component != null ) { if ( verboseFocusEvents ) { trace ( "Manually activating: " + FocusHandler.this.frame + ", focus component=" + component ); } component.requestFocus (); } else { // Nothing can take focus, no point activating the frame. if ( verboseFocusEvents ) { trace ( "Ignoring manual activation; no focusable components in " + FocusHandler.this.frame ); } } } } ); } } protected void doActivation ( final int swtTraversal ) { if ( this.swingControl.isDisposed () ) { return; } if ( !this.swingControl.isFocusControl () ) { // We've lost focus, don't activate the underlying AWT window if ( verboseFocusEvents ) { trace ( "Focus lost before activating AWT window" ); } return; } // Process deferred de-activation first (Windows only). // If a deactivation has been deferred (see SWT.Deactivate case below), handle it here. // Ideally we would have already deactivated as soon as some other control gets focus, but // the deactivation code does nothing in that case. On the other hand, if we just ignore // the deactivation altogether, the subsequent Activate triggered by this event, does // nothing and the embedded frame never gets focus. So we do the deactivate right here, // just before the activation. if ( Platform.isWin32 () && this.pendingDeactivate ) { synthesizeWindowActivation ( false ); this.pendingDeactivate = false; } if ( Platform.isWin32 () && synthesizeMethod != null ) { // Activate the window now synthesizeWindowActivation ( true ); } adjustFocusForSwtTraversal ( swtTraversal ); } // ========== Listener implementations protected class SwtEventFilter implements Listener { public void handleEvent ( final Event event ) { // Handle activation of the SWT.EMBEDDED composite. Track the currently active one. if ( event.widget == FocusHandler.this.borderless ) { switch ( event.type ) { case SWT.Activate: // The lastSwtTraversal may change before it is used. Save its value for the asyncExecs final int swtTraversal = FocusHandler.this.globalHandler.getCurrentSwtTraversal (); // We use asyncExec to defer the activation and focus setting in the underlying AWT frame. // This allows proper handling of the case where focus is briefly // set to the Swing control and immediately moved to a SWT component. (The deferred // handling will abort if focus has been lost on the Swing control) // // This case is common when navigating among tabs in an RCP view stack with the // left and right arrow keys. Focus is briefly given to the main view component and // then it is returned to the view tab for further navigation. If we did not defer // the activation, then focus cannot be restored to the view tab. FocusHandler.this.display.asyncExec ( new Runnable () { public void run () { doActivation ( swtTraversal ); } } ); // On windows, the actual activation needs to be deferred until doActivation() to // prevent the problem described above, so veto the activation normally done by SWT_AWT. if ( Platform.isWin32 () && synthesizeMethod != null ) { if ( verboseFocusEvents ) { trace ( "Consuming SWT.Activate event: " + event ); } event.type = SWT.None; } break; case SWT.Deactivate: // On Windows, when the SwingControl temporarily loses focus to an ancestor, and // that ancestor then assigns focus right back to it, the SwingControl receives only a // Deactivate event and not a subsequent Activate event. This causes the embedded // Swing component to lose focus when, for example, clicking on its parent RCP view tab. // // To work around this problem, we defer the deactivation // of the embedded frame here. See the SWT.Activate case above for processing of the // deferred event. if ( Platform.isWin32 () && synthesizeMethod != null ) { FocusHandler.this.pendingDeactivate = true; // Prevent the SWT_AWT-installed listener from running (and deactivating the frame). if ( verboseFocusEvents ) { trace ( "Consuming SWT.Activate event: " + event ); } event.type = SWT.None; break; } } } } } protected class SwtFocusListener implements FocusListener { public void focusGained ( final FocusEvent e ) { FocusHandler.this.isFocusedSwt = true; } public void focusLost ( final FocusEvent e ) { FocusHandler.this.isFocusedSwt = false; } } protected class AwtWindowFocusListener implements WindowFocusListener { public void windowGainedFocus ( final WindowEvent e ) { assert !FocusHandler.this.pendingTraverseOut; assert FocusHandler.this.extraTabCount == 0; } public void windowLostFocus ( final WindowEvent e ) { if ( Platform.isWin32 () ) { hideTextSelection (); processTypeAheadKeys ( FocusHandler.this.pendingTraverseOutSeqNum ); } } } protected class AwtKeyDispatcher implements KeyEventDispatcher { public boolean dispatchKeyEvent ( final KeyEvent e ) { boolean result = false; if ( Platform.isWin32 () ) { result = checkForTraverseOut ( e ); } return result; } } private void trace ( final String msg ) { System.err.println ( header () + ' ' + msg ); } private String header () { return "@" + System.currentTimeMillis () + " " + System.identityHashCode ( this ); } }