Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[albireo-dev] Traversing out of embedded SwingControls on Windows

I committed an initial implementation for Windows to support traversing out of SwingControls.

AWT WEmbeddedFrames do not implement traversal out of the frame when tabbing past the first or last element. This is a problem when a SwingControl shares a view with SWT controls. Some details at:

http://wiki.eclipse.org/Albireo_Focus_Management_Test_Cases#Traverse_out_of_Swing_controls

The initial SAS contribution used a custom focus traversal policy to work around this problem, but it's cleaner to use an AWT key event dispatcher, and that's what I've done.

The tricky part is handling extra keystrokes between the first tab and the transfer of focus to the next control which involves a move to the SWT thread. I have a partial solution, but there are still a number of problems if you tab very quickly. (The SAS contribution did not handle this at all)

XEmbeddedFrame implements traverse out, so this implementation is not needed on GTK.
### Eclipse Workspace Patch 1.0
#P org.eclipse.albireo.core
Index: src/org/eclipse/albireo/core/AwtEnvironment.java
===================================================================
RCS file: /cvsroot/technology/org.eclipse.albireo/org.eclipse.albireo.core/src/org/eclipse/albireo/core/AwtEnvironment.java,v
retrieving revision 1.10
diff -u -r1.10 AwtEnvironment.java
--- src/org/eclipse/albireo/core/AwtEnvironment.java	18 Feb 2008 01:04:15 -0000	1.10
+++ src/org/eclipse/albireo/core/AwtEnvironment.java	20 Feb 2008 07:23:00 -0000
@@ -138,7 +138,7 @@
             EventQueue.invokeAndWait(new Runnable() {
                 public void run() {
                     setLookAndFeel();
-                    if (FocusHandler.verboseFocusEvents)
+                    if (FocusHandler.verboseKFHEvents)
                         FocusDebugging.enableKeyboardFocusManagerLogging();
                 }
             });
Index: src/org/eclipse/albireo/core/SwingControl.java
===================================================================
RCS file: /cvsroot/technology/org.eclipse.albireo/org.eclipse.albireo.core/src/org/eclipse/albireo/core/SwingControl.java,v
retrieving revision 1.36
diff -u -r1.36 SwingControl.java
--- src/org/eclipse/albireo/core/SwingControl.java	18 Feb 2008 01:04:15 -0000	1.36
+++ src/org/eclipse/albireo/core/SwingControl.java	20 Feb 2008 07:23:00 -0000
@@ -784,6 +784,7 @@
     }
 
     private void handleDispose() {
+        focusHandler.dispose();
         display.removeListener(SWT.Settings, settingsListener);
     }
 
Index: src/org/eclipse/albireo/internal/FocusDebugging.java
===================================================================
RCS file: /cvsroot/technology/org.eclipse.albireo/org.eclipse.albireo.core/src/org/eclipse/albireo/internal/FocusDebugging.java,v
retrieving revision 1.5
diff -u -r1.5 FocusDebugging.java
--- src/org/eclipse/albireo/internal/FocusDebugging.java	18 Feb 2008 21:54:01 -0000	1.5
+++ src/org/eclipse/albireo/internal/FocusDebugging.java	20 Feb 2008 07:23:00 -0000
@@ -183,7 +183,11 @@
      * from the AWT <code>KeyboardFocusManager</code> singleton.
      */
     public static void enableKeyboardFocusManagerLogging() {
-        Logger logger = Logger.getLogger("java.awt.focus.KeyboardFocusManager");
+        enableFinest("java.awt.focus.KeyboardFocusManager");
+        enableFinest("java.awt.focus.DefaultKeyboardFocusManager");
+    }
+    private static void enableFinest(String name) {
+        Logger logger = Logger.getLogger(name);
         logger.setLevel(Level.FINEST);
         ConsoleHandler handler = new ConsoleHandler();
         handler.setLevel(Level.FINEST);
Index: src/org/eclipse/albireo/internal/FocusHandler.java
===================================================================
RCS file: /cvsroot/technology/org.eclipse.albireo/org.eclipse.albireo.core/src/org/eclipse/albireo/internal/FocusHandler.java,v
retrieving revision 1.2
diff -u -r1.2 FocusHandler.java
--- src/org/eclipse/albireo/internal/FocusHandler.java	18 Feb 2008 19:52:49 -0000	1.2
+++ src/org/eclipse/albireo/internal/FocusHandler.java	20 Feb 2008 07:23:00 -0000
@@ -1,170 +1,313 @@
-package org.eclipse.albireo.internal;
-
-import java.awt.EventQueue;
-import java.awt.Frame;
-import java.awt.event.WindowEvent;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-
-import org.eclipse.albireo.core.SwingControl;
-import org.eclipse.albireo.core.ThreadingHandler;
-import org.eclipse.swt.SWT;
-import org.eclipse.swt.widgets.Display;
-import org.eclipse.swt.widgets.Event;
-import org.eclipse.swt.widgets.Listener;
-
-public class FocusHandler implements Listener {
-
-    private final Frame frame;
-    private final SwingControl swingControl;
-    private final Display display;
-    
-    // Whether to print debugging information regarding focus events.
-    public static final boolean verboseFocusEvents = false;
-    
-    static private boolean synthesizeMethodInitialized = false;
-    static private Method synthesizeMethod = null;
-
-    public FocusHandler(SwingControl swingControl, Frame frame) {
-        assert Display.getCurrent() != null;     // On SWT event thread
-
-        if (verboseFocusEvents)
-            FocusDebugging.addFocusDebugListeners(swingControl, frame);
-        
-        this.swingControl = swingControl;
-        this.frame = frame;
-        display = swingControl.getDisplay();
-        
-        getSynthesizeMethod(frame.getClass());
-
-        // TODO: optimize by adding just one pair of filters for all SwingControls
-        display.addFilter(SWT.Activate, this);
-        display.addFilter(SWT.Deactivate, this);
-    }
-    
-    // On Windows, when the embedded frame loses focus to an ancestor which then assigns focus to 
-    // the embedded composite, the composite receives only a Deactivate event and not an Activate
-    // event. This method schedules an async exec, meant to run after the SWT focus change is complete. 
-    // It sends the missing activation when necessary, so that the frame can get back focus.
-    //
-    // This solves the problem where clicking on an RCP view tab causes an embedded Swing control
-    // to lose focus. 
-    protected void postMissingActivation() {
-        assert Platform.isWin32();               // currently only implemented/needed for win32
-        
-        if (!display.isDisposed()) {
-            ThreadingHandler.getInstance().asyncExec(display, new Runnable() {
-                public void run() {
-                    if (!display.isDisposed() && (swingControl == display.getFocusControl())) {
-                        if (FocusHandler.verboseFocusEvents) {
-                            System.err.println("@" + System.currentTimeMillis() + 
-                                    " Manually reactivating " + swingControl);
-                        }
-                        synthesizeWindowActivation(true);
-                    }
-                }
-            });
-        }
-    }
-
-    // ==== 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
-
-        if (synthesizeMethod != null) {
-            // synthesizeWindowActivation() is available. Use it. Normally, this is on
-            // Java 1.5 and higher. 
-            EventQueue.invokeLater(new Runnable() {
-                public void run() {
-                    try {
-                        if (synthesizeMethod != null) {
-                            if (FocusHandler.verboseFocusEvents) {
-                                System.err.println("@" + System.currentTimeMillis() + 
-                                        " 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 (FocusHandler.verboseFocusEvents) {
-            e.printStackTrace();
-        }
-    }
-
-    // ======== Implementation of org.eclipse.swt.widgets.Listener
-    public void handleEvent(Event event) {
-    	if (event.widget != swingControl) {
-    		return;
-    	}
-        switch (event.type) {
-        case SWT.Activate:
-            if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
-                synthesizeWindowActivation(true);
-                if (FocusHandler.verboseFocusEvents) {
-                    System.err.println("@" + System.currentTimeMillis() + 
-                            " Consuming SWT.Activate event: " + event);
-                }
-            	event.type = SWT.None;
-            }
-            break;
-
-        case SWT.Deactivate:
-            if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
-                synthesizeWindowActivation(false);
-                if (FocusHandler.verboseFocusEvents) {
-                    System.err.println("@" + System.currentTimeMillis() + 
-                            " Consuming SWT.Deactivate event: " + event);
-                }
-            	event.type = SWT.None;
-            }
-            if (Platform.isWin32()) {
-                postMissingActivation();
-            }
-            break;
-        }
-    }
-}
+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.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.util.Set;
+
+import org.eclipse.albireo.core.SwingControl;
+import org.eclipse.albireo.core.ThreadingHandler;
+import org.eclipse.swt.SWT;
+import org.eclipse.swt.widgets.Control;
+import org.eclipse.swt.widgets.Display;
+import org.eclipse.swt.widgets.Event;
+import org.eclipse.swt.widgets.Listener;
+
+public class FocusHandler implements Listener, KeyEventDispatcher {
+
+    private final Frame frame;
+    private final SwingControl swingControl;
+    private final Display display;
+    private volatile AWTKeyStroke traverseKey = null;
+    private int extraTabCount;
+    
+    // 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;
+    
+    static private boolean synthesizeMethodInitialized = false;
+    static private Method synthesizeMethod = null;
+
+    public FocusHandler(SwingControl swingControl, final Frame frame) {
+        assert Display.getCurrent() != null;     // On SWT event thread
+
+        if (verboseFocusEvents)
+            FocusDebugging.addFocusDebugListeners(swingControl, frame);
+        
+        this.swingControl = swingControl;
+        this.frame = frame;
+        display = swingControl.getDisplay();
+        
+        getSynthesizeMethod(frame.getClass());
+
+        // TODO: optimize by adding just one pair of filters for all SwingControls
+        display.addFilter(SWT.Activate, this);
+        display.addFilter(SWT.Deactivate, this);
+        
+        KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(this);
+    }
+    
+    public void dispose() {
+        display.removeFilter(SWT.Activate, this);
+        display.removeFilter(SWT.Deactivate, this);
+        KeyboardFocusManager.getCurrentKeyboardFocusManager().removeKeyEventDispatcher(this);
+    }
+    
+    // =====
+    // 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 (traverseKey == null) {
+                // 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)
+                    traverseKey = key;
+                    swtTraverse(SWT.TRAVERSE_TAB_NEXT);
+                    if (verboseTraverseOut) {
+                        System.err.println("@" + System.currentTimeMillis() + " " + System.identityHashCode(FocusHandler.this) +  
+                                           " 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
+                    traverseKey = key;
+                    swtTraverse(SWT.TRAVERSE_TAB_PREVIOUS);
+                    if (verboseTraverseOut) {
+                        System.err.println("@" + System.currentTimeMillis() + " " + System.identityHashCode(FocusHandler.this) +  
+                                           " 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
+                synchronized (this) {
+                    if (traverseForwardKeys.contains(key)) {
+                        if (verboseTraverseOut) {
+                            System.err.println("@" + System.currentTimeMillis() + " " + System.identityHashCode(this) +  
+                                               " forward traversal typeahead");
+                        }
+                        extraTabCount++;
+                    } else if (traverseBackwardKeys.contains(key)) {
+                        if (verboseTraverseOut) {
+                            System.err.println("@" + System.currentTimeMillis() + " " + System.identityHashCode(this) +  
+                                               " backward traversal typeahead");
+                        }
+                        extraTabCount--;
+                    }
+                }
+                e.consume();
+                return true;
+            }
+        }
+        return false;
+    }
+    
+    protected void swtTraverse(final int direction) {
+        assert EventQueue.isDispatchThread();
+
+        ThreadingHandler.getInstance().asyncExec(display, new Runnable() {
+            public void run() {
+                if (verboseTraverseOut) {
+                    System.err.println("@" + System.currentTimeMillis() + " " + System.identityHashCode(FocusHandler.this) +  
+                                       " SWT: traversing out, control=" + swingControl + " direction= " + direction);
+                }
+                doTraverse(direction, swingControl);
+                
+                int increment = (direction == SWT.TRAVERSE_TAB_NEXT) ? -1 : 1;
+                synchronized (this) {
+                    while (extraTabCount != 0) {
+                        Control focusControl = display.getFocusControl();
+                        if (verboseTraverseOut) {
+                            System.err.println("@" + System.currentTimeMillis() + " " + System.identityHashCode(FocusHandler.this) +  
+                                               " handling typeahead, control=" + focusControl);
+                        }
+                        if (focusControl != null) {
+                            doTraverse(direction, focusControl);
+                        }
+                        extraTabCount += increment;
+                    }
+                }
+                traverseKey = null;
+            }
+
+        });
+    }
+
+    protected void doTraverse(final int direction, Control focusControl) {
+        assert Display.getCurrent() != null;
+        
+        boolean traverse;
+        traverse = focusControl.traverse(direction);
+        if (verboseTraverseOut && !traverse) {
+            System.err.println("@" + System.currentTimeMillis() + " " + System.identityHashCode(FocusHandler.this) +  
+                               " traverse failed, frome" + focusControl);
+        }
+    }
+    
+
+    // ==============
+    // On Windows, when the embedded frame loses focus to an ancestor which then assigns focus to 
+    // the embedded composite, the composite receives only a Deactivate event and not an Activate
+    // event. This method schedules an async exec, meant to run after the SWT focus change is complete. 
+    // It sends the missing activation when necessary, so that the frame can get back focus.
+    //
+    // This solves the problem where clicking on an RCP view tab causes an embedded Swing control
+    // to lose focus. 
+    protected void postMissingActivation() {
+        assert Platform.isWin32();               // currently only implemented/needed for win32
+        
+        if (!display.isDisposed()) {
+            ThreadingHandler.getInstance().asyncExec(display, new Runnable() {
+                public void run() {
+                    if (!display.isDisposed() && (swingControl == display.getFocusControl())) {
+                        if (FocusHandler.verboseFocusEvents) {
+                            System.err.println("@" + System.currentTimeMillis() + 
+                                    " Manually reactivating " + swingControl);
+                        }
+                        synthesizeWindowActivation(true);
+                    }
+                }
+            });
+        }
+    }
+
+    // ==== 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
+
+        if (synthesizeMethod != null) {
+            // synthesizeWindowActivation() is available. Use it. Normally, this is on
+            // Java 1.5 and higher. 
+            EventQueue.invokeLater(new Runnable() {
+                public void run() {
+                    try {
+                        if (synthesizeMethod != null) {
+                            if (FocusHandler.verboseFocusEvents) {
+                                System.err.println("@" + System.currentTimeMillis() + 
+                                        " 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 (FocusHandler.verboseFocusEvents) {
+            e.printStackTrace();
+        }
+    }
+
+    // ======== Implementation of org.eclipse.swt.widgets.Listener
+    public void handleEvent(Event event) {
+    	if (event.widget != swingControl) {
+    		return;
+    	}
+        switch (event.type) {
+        case SWT.Activate:
+            if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
+                synthesizeWindowActivation(true);
+                if (FocusHandler.verboseFocusEvents) {
+                    System.err.println("@" + System.currentTimeMillis() + 
+                            " Consuming SWT.Activate event: " + event);
+                }
+            	event.type = SWT.None;
+            }
+            break;
+
+        case SWT.Deactivate:
+            if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
+                synthesizeWindowActivation(false);
+                if (FocusHandler.verboseFocusEvents) {
+                    System.err.println("@" + System.currentTimeMillis() + 
+                            " Consuming SWT.Deactivate event: " + event);
+                }
+            	event.type = SWT.None;
+            }
+            if (Platform.isWin32()) {
+                postMissingActivation();
+            }
+            break;
+        }
+    }
+    
+    // ======== Implementation of KeyEventDispatcher
+    public boolean dispatchKeyEvent(KeyEvent e) {
+        boolean result = false;
+        
+        if (Platform.isWin32()) {
+            result = checkForTraverseOut(e);
+        }
+        return result;
+    }
+
+}

Back to the top