/*******************************************************************************
* 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.Platform;
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;
@SuppressWarnings("unchecked")
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 KeyEventDispatcher keyEventDispatcher = new AwtKeyDispatcher();
private WindowFocusListener awtWindowFocusListener = new AwtWindowFocusListener();
private FocusListener swtFocusListener = new SwtFocusListener();
private Listener swtEventFilter = new SwtEventFilter();
public FocusHandler(final SwingControl swingControl, 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;
display = swingControl.getDisplay();
getSynthesizeMethod(frame.getClass());
globalHandler.addEventFilter(swtEventFilter);
frame.addWindowFocusListener(awtWindowFocusListener);
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(keyEventDispatcher);
borderless.addFocusListener(swtFocusListener);
}
public void dispose() {
globalHandler.removeEventFilter(swtEventFilter);
frame.removeWindowFocusListener(awtWindowFocusListener);
KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(keyEventDispatcher);
borderless.removeFocusListener(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();
Component focusOwner = frame.getMostRecentFocusOwner();
if (focusOwner instanceof JTextComponent) {
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(KeyEvent e) {
assert EventQueue.isDispatchThread();
// Ignore events outside this frame
if (frame.isFocused()) {
Set traverseForwardKeys = frame.getFocusTraversalKeys(KeyboardFocusManager.FORWARD_TRAVERSAL_KEYS);
Set traverseBackwardKeys = frame.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS);
AWTKeyStroke key = AWTKeyStroke.getAWTKeyStrokeForEvent(e);
if (!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 = frame.getFocusTraversalPolicy().getLastComponent(frame);
if (traverseForwardKeys.contains(key) && (limit == e.getComponent() || limit == null)) {
// Tabbing forward from last component in frame (or empty frame)
pendingTraverseOut = true;
pendingTraverseOutSeqNum++;
swtTraverse(SWT.TRAVERSE_TAB_NEXT, 1, false);
if (verboseTraverseOut) {
trace("AWT: traversing out (forward)");
}
e.consume();
return true;
}
limit = frame.getFocusTraversalPolicy().getFirstComponent(frame);
if (traverseBackwardKeys.contains(key) && (limit == e.getComponent() || limit == null)) {
// Tabbing backward from first component in frame
pendingTraverseOut = true;
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");
}
extraTabCount++;
e.consume();
return true;
} else if (traverseBackwardKeys.contains(key)) {
if (verboseTraverseOut) {
trace("backward traversal typeahead");
}
extraTabCount--;
e.consume();
return true;
}
}
}
return false;
}
protected void processTypeAheadKeys(int callerSeqNum) {
if (pendingTraverseOut && extraTabCount != 0) {
if (callerSeqNum != currentTraverseOutSeqNum + 1) {
if (verboseTraverseOut) {
trace("Discarding processTypeAhead request, sequence number out of sync " + callerSeqNum + "!=" + (currentTraverseOutSeqNum+1) + ", extraTabCount=" + extraTabCount);
}
return;
}
if (verboseTraverseOut) {
trace("Processing typeahead traversals, count=" + extraTabCount);
}
int direction = (extraTabCount > 0) ? SWT.TRAVERSE_TAB_NEXT : SWT.TRAVERSE_TAB_PREVIOUS;
swtTraverse(direction, Math.abs(extraTabCount), true);
}
pendingTraverseOut = false;
currentTraverseOutSeqNum++;
extraTabCount = 0;
}
protected void swtTraverse(final int direction, final int count, final boolean flushingTypeAhead) {
assert EventQueue.isDispatchThread();
ThreadingHandler.getInstance().asyncExec(display, new Runnable() {
public void run() {
for(int i = 0; i < count; i++) {
doTraverse(direction, flushingTypeAhead, pendingTraverseOutSeqNum);
}
}
});
}
protected void doTraverse(final int direction, boolean flushingTypeAhead, final int seqNum) {
assert Display.getCurrent() != null;
Control focusControl = display.getFocusControl();
if (verboseTraverseOut) {
trace("SWT: traversing, control=" + focusControl);
}
SwingControl activeBorderless = globalHandler.getActiveEmbedded();
if ((focusControl == null) && (activeBorderless != null)) {
focusControl = activeBorderless;
if (verboseTraverseOut) {
trace("SWT: current focus control is null; using=" + focusControl);
}
}
if (focusControl != null) {
boolean traverse = focusControl.traverse(direction);
Control newFocusControl = 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 && (globalHandler.getActiveWidget() == borderless) && 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(frame,
new Object[] { new Boolean(activate) });
}
} catch (IllegalAccessException e) {
handleSynthesizeException(e);
} catch (InvocationTargetException e) {
handleSynthesizeException(e);
}
} else {
if (activate) {
frame.dispatchEvent (new WindowEvent (frame, WindowEvent.WINDOW_ACTIVATED));
frame.dispatchEvent (new WindowEvent (frame, WindowEvent.WINDOW_GAINED_FOCUS));
} else {
frame.dispatchEvent (new WindowEvent (frame, WindowEvent.WINDOW_LOST_FOCUS));
frame.dispatchEvent (new WindowEvent (frame, WindowEvent.WINDOW_DEACTIVATED));
}
}
}
});
}
private void getSynthesizeMethod(Class clazz) {
if (Platform.isWin32() && !synthesizeMethodInitialized) {
synthesizeMethodInitialized = true;
try {
synthesizeMethod = clazz.getMethod("synthesizeWindowActivation", new Class[]{boolean.class});
} catch (NoSuchMethodException e) {
handleSynthesizeException(e);
}
}
}
private void handleSynthesizeException(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(int currentSwtTraversal) {
assert Display.getCurrent() != null;
if (!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 = frame.getFocusTraversalPolicy().getFirstComponent(frame);
} else {
component = frame.getFocusTraversalPolicy().getLastComponent(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;
Shell activeShell = globalHandler.getActiveShell();
SwingControl activeBorderless = globalHandler.getActiveEmbedded();
if (!borderless.isDisposed() &&
// Make sure that this control is in the active shell, so focus is not stolen from other windows.
// (Note: display.getActiveShell() is not always accurate here, so we use the static instead)
(activeShell == borderless.getShell()) &&
// Check that this control currently the focus control
// BUT... Display.getFocusControl is unreliable when another embedded AWT window has recently
// become active, so to be safe, make sure that no other Swing control has been activated
// (otherwise we will steal focus from the other active SwingControl)
(borderless == display.getFocusControl() &&
((activeBorderless == null) || (activeBorderless == 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.
Component component = frame.getMostRecentFocusOwner();
if (component != null) {
if (verboseFocusEvents) {
trace("Manually activating: " + 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 " + frame);
}
}
}
});
}
}
protected void doActivation(int swtTraversal) {
if (swingControl.isDisposed()) {
return;
}
if (!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() && pendingDeactivate) {
synthesizeWindowActivation(false);
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(Event event) {
// Handle activation of the SWT.EMBEDDED composite. Track the currently active one.
if (event.widget == borderless) {
switch (event.type) {
case SWT.Activate:
// The lastSwtTraversal may change before it is used. Save its value for the asyncExecs
final int swtTraversal = 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.
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)) {
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(FocusEvent e) {
isFocusedSwt = true;
}
public void focusLost(FocusEvent e) {
isFocusedSwt = false;
}
}
protected class AwtWindowFocusListener implements WindowFocusListener {
public void windowGainedFocus(WindowEvent e) {
assert !pendingTraverseOut;
assert extraTabCount == 0;
}
public void windowLostFocus(WindowEvent e) {
if (Platform.isWin32()) {
hideTextSelection();
processTypeAheadKeys(pendingTraverseOutSeqNum);
}
}
}
protected class AwtKeyDispatcher implements KeyEventDispatcher {
public boolean dispatchKeyEvent(KeyEvent e) {
boolean result = false;
if (Platform.isWin32()) {
result = checkForTraverseOut(e);
}
return result;
}
}
private void trace(String msg) {
System.err.println(header() + ' ' + msg);
}
private String header() {
return "@" + System.currentTimeMillis() + " " + System.identityHashCode(this);
}
}