Skip to main content

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index] [List Home]
[albireo-dev] Participating more naturally in SWT tab order

I've committed some code to allow embedded Swing components to behave better with respect to the overall SWT tab order. This is an optional feature that is turned on by default. It can be controlled with SwingControl.setSwtTabOrderExtended.

If it is enabled, a forward SWT traverse into the embedded frame will cause focus to be set on the first control in the frame (as determined by the focus traversal policy). A backward SWT traverse into the embedded frame will cause focus to be set on the last control in the frame.

If this feature is not enabled, the previous behavior is retained and focus will be determined solely by the frame's focus traversal policy, which usually means the most recently focused Swing component gets the focus on any traverse or mouse click.

This change allows views like the TextFields view to have a more natural tab behavior. Now you can tab forward through the Swing fields, then out to the tab folder and back in to the first Swing field (instead of returning to the last one.)

Note: there is one quirk left under Gtk. If tabbing out of the last field, and there is no SWT control to receive focus, then focus remains on the last field rather than moving forward to the first field. This may seem rare, but it can happen relatively easily at the startup of bare-bones RCP applications. I was able to handle this case under Windows because we are forced to implement traverse-out logic ourselves since the Windows AWT implementation does not support it. We don't do our own Gtk traverse-out logic, so I have found nowhere to insert a fix (so far).

Along the way, I've fixed a couple of focus-related bugs:

* The case described in the note above needed some fixes under Windows.
* I've improved our tracking of the currently active Swing control. When the control is first created, no Activate is received, so I've added some code to work around that problem. This is especially important on Windows where we rely on this variable to help workaround issues with Display.getFocusControl().
### Eclipse Workspace Patch 1.0
#P org.eclipse.albireo.core
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.65
diff -u -r1.65 SwingControl.java
--- src/org/eclipse/albireo/core/SwingControl.java	22 Apr 2008 17:28:44 -0000	1.65
+++ src/org/eclipse/albireo/core/SwingControl.java	23 Apr 2008 20:31:05 -0000
@@ -15,6 +15,7 @@
 import java.awt.Container;
 import java.awt.Dimension;
 import java.awt.EventQueue;
+import java.awt.FocusTraversalPolicy;
 import java.awt.Frame;
 import java.awt.event.ComponentAdapter;
 import java.awt.event.ComponentEvent;
@@ -87,7 +88,7 @@
     private RootPaneContainer rootPaneContainer;
     private JComponent swingComponent;
     private boolean populated = false;
-
+    
     // ========================================================================
     // Constructors
 
@@ -1026,6 +1027,7 @@
     
     // ============================= Focus Management =============================
     private FocusHandler focusHandler;
+    private boolean isSwtTabOrderExtended = true;
     
     protected void initializeFocusManagement() {
         assert frame != null;
@@ -1035,6 +1037,39 @@
     }
     
     
+    /**
+     * Configures the SwingControl's participation in SWT traversals. See {@link #isSwtTabOrderExtended} 
+     * for more information. 
+     * 
+     * @param isSwtTabOrderExtended
+     */
+    public void setSwtTabOrderExtended(boolean isSwtTabOrderExtended) {
+        this.isSwtTabOrderExtended = isSwtTabOrderExtended;
+    }
+
+    /**
+     * Returns whether the SWT tab order is configured to extend to the 
+     * child Swing components inside this SwingControl. 
+     * <p>
+     * If this method returns true,
+     * then when traversing (e.g. with the tab key) forward into this SwingControl, 
+     * AWT focus will be set to the first component in the frame, as determined 
+     * by the AWT {@link FocusTraversalPolicy}. Similarly, 
+     * when traversing backward into this SwingControl, AWT focus will be set to
+     * the last component in the frame. 
+     * <p>
+     * If this method returns false, then the SwingControl participates in the tab order
+     * only as a single opaque element. Focus on a child component will then be determined 
+     * completely by the AWT focus subsystem, independent of any current SWT traversal state.
+     * This normally means that focus will move to the most recently focused 
+     * Swing component within the embedded frame.
+     *    
+     * @return true if the child componets are SWT traversal participants. false otherwise. 
+     */
+    public boolean isSwtTabOrderExtended() {
+        return isSwtTabOrderExtended;
+    }
+
     public boolean setFocus() {
         checkWidget();
         
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.10
diff -u -r1.10 FocusHandler.java
--- src/org/eclipse/albireo/internal/FocusHandler.java	22 Feb 2008 04:14:44 -0000	1.10
+++ src/org/eclipse/albireo/internal/FocusHandler.java	23 Apr 2008 20:31:05 -0000
@@ -31,7 +31,9 @@
     private final Frame frame;
     private final SwingControl swingControl;
     private final Display display;
-    private boolean pendingTraverse = false;
+    private boolean pendingTraverseOut = false;
+    private int pendingTraverseOutSeqNum = 0;
+    private int currentTraverseOutSeqNum = 0;
     private int extraTabCount = 0;
     private boolean isActiveSwt;
     private boolean isFocusedSwt;
@@ -66,6 +68,13 @@
         // TODO: optimize by adding just one pair of filters for all SwingControls
         display.addFilter(SWT.Activate, swtEventFilter);
         display.addFilter(SWT.Deactivate, swtEventFilter);
+        display.addFilter(SWT.Traverse, swtEventFilter);
+        
+        // We won't get an Activate event for the newly created control, so update
+        // the static tracking field here. 
+        if (display.getFocusControl() == swingControl) {
+            activeSwingControl = swingControl;
+        }
         
         frame.addWindowFocusListener(awtWindowFocusListener);
         KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(keyEventDispatcher);
@@ -113,15 +122,16 @@
             Set traverseBackwardKeys = frame.getFocusTraversalKeys(KeyboardFocusManager.BACKWARD_TRAVERSAL_KEYS);
             AWTKeyStroke key = AWTKeyStroke.getAWTKeyStrokeForEvent(e);
             
-            if (!pendingTraverse) {
+            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)
-                    pendingTraverse = true;
-                    swtTraverse(SWT.TRAVERSE_TAB_NEXT, 1);
+                    pendingTraverseOut = true;
+                    pendingTraverseOutSeqNum++;
+                    swtTraverse(SWT.TRAVERSE_TAB_NEXT, 1, false);
                     if (verboseTraverseOut) {
                         trace("AWT: traversing out (forward)");
                     }
@@ -132,8 +142,9 @@
                 limit = frame.getFocusTraversalPolicy().getFirstComponent(frame);
                 if (traverseBackwardKeys.contains(key) && (limit == e.getComponent() || limit == null)) {
                     // Tabbing backward from first component in frame
-                    pendingTraverse = true;
-                    swtTraverse(SWT.TRAVERSE_TAB_PREVIOUS, 1);
+                    pendingTraverseOut = true;
+                    pendingTraverseOutSeqNum++;
+                    swtTraverse(SWT.TRAVERSE_TAB_PREVIOUS, 1, false);
                     if (verboseTraverseOut) {
                         trace("AWT: traversing out (backward)");
                     }
@@ -165,32 +176,39 @@
         return false;
     }
     
-    protected void processTypeAheadKeys() {
-        if (pendingTraverse && extraTabCount != 0) {
+    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));
+            swtTraverse(direction, Math.abs(extraTabCount), true);
         }
-        pendingTraverse = false;
+        pendingTraverseOut = false;
+        currentTraverseOutSeqNum++;
         extraTabCount = 0;
     }
 
-    protected void swtTraverse(final int direction, final int count) {
+    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);
+                    doTraverse(direction, flushingTypeAhead, pendingTraverseOutSeqNum);
                 }
             }
 
         });
     }
 
-    protected void doTraverse(final int direction) {
+    protected void doTraverse(final int direction, boolean flushingTypeAhead, final int seqNum) {
         assert Display.getCurrent() != null;
 
         Control focusControl = display.getFocusControl();
@@ -205,6 +223,32 @@
         }
         if (focusControl != null) {
             boolean traverse = focusControl.traverse(direction);
+            
+            Control newFocusControl = display.getFocusControl();
+            if (traverse && (newFocusControl == focusControl) && (newFocusControl == activeSwingControl)) {
+                // 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);
             }
@@ -260,6 +304,7 @@
                         // Ideally, we would directly activate SwingControl here, but there seems to be 
                         // no way to do that, so activate the underlying AWT embedded frame
                         synthesizeWindowActivation(true);
+                        activeSwingControl = swingControl;
                     }
                 }
             });
@@ -334,43 +379,102 @@
     }
 
 
+    // ==== 
+    // 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);
+                }
+                component.requestFocus();                
+            }
+        });
+    }
+
+    
     // ========== Listener implementations
     
+
     protected class SwtEventFilter implements Listener {
         
+        private int currentSwtTraversal = SWT.TRAVERSE_NONE;
+
         public void handleEvent(Event event) {
-            if (event.widget != swingControl) {
+            if (event.type == SWT.Traverse) {
+                currentSwtTraversal = event.detail;
                 return;
             }
-            
-            switch (event.type) {
-            case SWT.Activate:
-                isActiveSwt = true;
-                activeSwingControl = swingControl;
-                if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
-                    synthesizeWindowActivation(true);
-                    if (verboseFocusEvents) {
-                        trace("Consuming SWT.Activate event: " + event);
+            if (event.widget == swingControl) {
+                switch (event.type) {
+                case SWT.Activate:
+                    isActiveSwt = true;
+                    activeSwingControl = swingControl;
+                    if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
+                        synthesizeWindowActivation(true);
+                        if (verboseFocusEvents) {
+                            trace("Consuming SWT.Activate event: " + event);
+                        }
+                        event.type = SWT.None;
                     }
-                    event.type = SWT.None;
-                }
-                
-                break;
 
-            case SWT.Deactivate:
-                isActiveSwt = false;
-                activeSwingControl = null;
-                if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
-                    synthesizeWindowActivation(false);
-                    if (verboseFocusEvents) {
-                        trace("Consuming SWT.Deactivate event: " + event);
+                    adjustFocusForSwtTraversal(currentSwtTraversal);
+                    
+                    break;
+
+                case SWT.Deactivate:
+                    isActiveSwt = false;
+                    activeSwingControl = null;
+                    if (Platform.isWin32() && (Platform.SWT_VERSION < Platform.SWT_34) && (synthesizeMethod != null)) {
+                        synthesizeWindowActivation(false);
+                        if (verboseFocusEvents) {
+                            trace("Consuming SWT.Deactivate event: " + event);
+                        }
+                        event.type = SWT.None;
                     }
-                    event.type = SWT.None;
-                }
-                if (Platform.isWin32()) {
-                    postMissingActivation();
+                    if (Platform.isWin32()) {
+                        postMissingActivation();
+                    }
+                    break;
+                    
                 }
-                break;
+            }
+            if (event.type == SWT.Activate) {
+                currentSwtTraversal   = SWT.TRAVERSE_NONE;
             }
         }
 
@@ -387,14 +491,14 @@
 
     protected class AwtWindowFocusListener implements WindowFocusListener {
         public void windowGainedFocus(WindowEvent e) {
-            assert !pendingTraverse;
+            assert !pendingTraverseOut;
             assert extraTabCount == 0;
         }
 
         public void windowLostFocus(WindowEvent e) {
             if (Platform.isWin32()) {
                 hideTextSelection();
-                processTypeAheadKeys();
+                processTypeAheadKeys(pendingTraverseOutSeqNum);
             }
         }
         

Back to the top