JDK-4685768 : A11y issue - Focus set to disabled component, can't Tab/Shift-Tab
  • Type: Bug
  • Component: client-libs
  • Sub-Component: java.awt
  • Affected Version: 6
  • Priority: P4
  • Status: Closed
  • Resolution: Fixed
  • OS: solaris_8,windows_2000
  • CPU: x86,sparc
  • Submitted: 2002-05-15
  • Updated: 2010-10-07
  • Resolved: 2011-05-18
The Version table provides details related to the release that this issue/RFE will be addressed.

Unresolved : Release in which this issue/RFE will be addressed.
Resolved: Release in which this issue/RFE has been resolved.
Fixed : Release in which this issue/RFE has been fixed. The release containing this fix may be available for download as an Early Access Release or a General Availability Release.

To download the current JDK release, click here.
JDK 7
7 b36Fixed
Related Reports
Relates :  
Relates :  
Description
I have a frame on which I have 3 buttons. Selecting one of
the buttons causes 2 of them to get disabled. When this happens,
it appears that focus is transferred to one of the disabled
buttons. After this occurs, trying to use Tab or Shift-Tab
to move between components does not work.

To reproduce:
* Compile the attachment (FocusBug.java).
* Run it.
* Focus is initially in the text field.
* Use tab to move from component to component.
* Move focus to the "A Remove Button" and hit the space-bar.
* The text field will update with new text ("Remove button selected")
  and the "Remove" and "Edit" buttons will get disabled.
* Note at this point that it appears that the "Edit" button
  has focus ("focus" box is drawn around button label),
  but since its disabled, you can't do anything.. hitting
  space-bar does nothing, hitting Tab or Shift-Tab doesn't
  do anything either.

I would think that focus should get transferred to the "next"
enabled component, ie. the text field.

I'm using Hopper (jdk1.4.1), build 11 on Solaris 8.
 


Name: jk109818			Date: 08/14/2003


FULL PRODUCT VERSION :
java version "1.4.1_01"
Java(TM) 2 Runtime Environment, Standard Edition (build 1.4.1_01-
b01)
Java HotSpot(TM) Client VM (build 1.4.1_01-b01, mixed mode)

FULL OPERATING SYSTEM VERSION :
Microsoft Windows 2000 [Version
5.00.2195]

A DESCRIPTION OF THE PROBLEM :
Found several, probably related, symptoms
  1) Focus ends up on a disabled component, keyboard navigation doesn't work
  2) Focus ends up on a non-visible component, keyboard navigation doesn't work
  3) Infinite loop in focus cycle

STEPS TO FOLLOW TO REPRODUCE THE PROBLEM :
Use the attached source to demonstrate the problems.

The anonymous package Bug class constructs three panels and switches
between two states.  In state START, the JToggleButtons in the first
panel are enabled, the second panel is made visible, and the third
panel is made not-visible.  In state TOGGLED, the buttons in the first
panel are disabled, the second panel is made not-visible, and the
third panel is made visible.

The problems arise when the action attached to a JToggleButton changes
the state to TOGGLED (and disables the button).
 
-- java Bug
Verify the basic behavior by activating the Toggle and Reset buttons
at the bottom of the UI.  The focus cycle and enable/visible changes
are logged to System.out.

Reset the state to START and activate Button 0. Focus ends up in
Button 1, which is not enabled and not in the focus cycle.  TAB does
not work to break back into the focus cycle.  Also notice that the
focus cycle changes slightly between "immediately" and "invokeLater"
after changing the state. The focus does not actually move until
"invokeLater".

Reset the state to START and activate Button 2. Focus ends up in the
text field of panel START, which is not visible and not in the focus
cycle. TAB does not work to break back into the focus cycle.  Also
notice that text field START was not even in the focus cycle immediately
after changing the state.  It appears the target of the focus change
is computed significantly earlier than the change takes place.

-----
Let's make things nicer by adding another component that doesn't
change enable or visibility state.

-- java Bug unchanging
This adds a JButton ("Dummy") beneath the JToggleButtons, which doesn't
change state.

Activate the Toggle button.  There is an infinite loop in the
focus cycle!  Fortunately, this loop resolves itself invokeLater.
Same thing happens for Buttons 0, 1 and 2.

-----
Let's demonstrate that the infinite loop is at least partially due
to the LayoutComparator behavior.

-- java Bug unchanging comparator
This alters the AlignmentX of each button, thereby changing their
X coordinates.

Activate the Toggle button.  Looks sane now.  Again, notice that
the focus cycle has a slightly dubious order "immediately"
after changing the state, which resolves itself "invokeLater".

Reset the state to START and activate Button 0.  No change in
behavior from original demonstration.

Reset the state to START and activate Button 2.  Adding the
unchanging component has made this behave correctly.

-----
Let's see if we can use transferFocus to break back into the
correct focus cycle.

-- java Bug snatch
This sets up an invokeLater transferFocus whenever a textpanel
is made visible.  By accident of implementation, this invokeLater
is set up before the invokeLater to show the focus cycle.  So,
we will invokeLater one MORE dump of the focus cycle to try to be
the last thing to run.

Activate Button 0.  Ahah, transferFocus does work where TAB does
not.

Reset the state to START and activate Button 2.  Take a close
look at the sequence of focus owners and compare activating Button 0
and Button 2.  Button0,Button1,Button1,textTOGGLED and
Button0,textTOGGLED, textTOGGLED,textTOGGLED respectively.  So,
the transferFocus is not a rock solid workaround for this problem.  (In
fact, in the original application, transferFocus does not work
reliably to get around this.)






EXPECTED VERSUS ACTUAL BEHAVIOR :
Tthe focus owner should be a member of the focus cycle. Instead, the focus
owner ended up a disabled or non-visible
component and keyboard
navigation was broken.

REPRODUCIBILITY :
This bug can be reproduced always.

---------- BEGIN SOURCE ----------
import java.awt.Container;
import java.awt.Dimension;
import
java.awt.Insets;
import java.awt.event.ActionEvent;
import
java.awt.event.KeyEvent;
import java.awt.event.WindowAdapter;
import
java.awt.event.WindowEvent;
import java.beans.PropertyChangeEvent;
import
java.beans.PropertyChangeListener;
import javax.swing.AbstractAction;
import
javax.swing.Action;
import javax.swing.BorderFactory;
import
javax.swing.BoxLayout;
import javax.swing.JButton;
import
javax.swing.JComponent;
import javax.swing.JFrame;
import
javax.swing.JPanel;
import javax.swing.JTextField;
import
javax.swing.JToggleButton;

import java.awt.Component;
import
java.awt.FocusTraversalPolicy;
import java.awt.KeyboardFocusManager;

class Bug
  
extends JPanel
{
  static final String State = "Bug.state";

  int state = START;
  static
final int START = 0;
  static final int TOGGLED = 1;

  Action toggleAction;
  Action
resetAction;

  boolean shiftAlign = false;
  boolean useDummy = false;
  boolean
snatchFocus = false;

  public Bug(String [] args) {
    for (int i = 0; i < args.length; i++) {
      if
(args[i].equals("comparator")) {
        shiftAlign = true;
      }
      if
(args[i].equals("unchanging")) {
        useDummy = true;
      }
      if (args[i].equals("snatch"))
{
        snatchFocus = true;
      }
    }

    String panelname = "Bug panel";
     
    setName(panelname);
    
setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
    
setBorder(BorderFactory.createTitledBorder(panelname));

    toggleAction = new
AbstractAction() {
      {
        putValue(Action.NAME, "toggleAction");
        
putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_T));
      }
      public void
actionPerformed(ActionEvent e) {

        java.awt.EventQueue.invokeLater(new Runnable() {
          
public void run() {
	    dumpFocusCycle("before changing state to TOGGLED");
	    
setState(TOGGLED);
	    dumpFocusCycle("immediately after changing state to TOGGLED");

            
java.awt.EventQueue.invokeLater(new Runnable() {
              public void run() {
	        
dumpFocusCycle("invokeLater after changing state to TOGGLED");
              }
            });
          }
        });

        //
Performing the setState inline here or delayed via invokeLater
        // does not seem to have a
significant impact.  This demonstrator
        // uses invokeLater to avoid any question about
interactions between
        // the JToggleButton action firing and the consequences of setState.
      
}
    };

    resetAction = new AbstractAction() {
      {
        putValue(Action.NAME,
"resetAction");
        putValue(Action.MNEMONIC_KEY, new Integer(KeyEvent.VK_R));
      }
      
public void actionPerformed(ActionEvent e) {
        setState(START);
      }
    };

    add(new
togglepanel());
    add(new textpanel("Lower panel START", START));
    add(new
textpanel("Lower panel TOGGLED", TOGGLED));

    JButton reset = new
JButton(resetAction);
    reset.setName("Reset button in " + panelname);
    
reset.setText("Reset");
    if (shiftAlign) {
      
reset.setAlignmentX((float)4/(float)5.0);
    }
    add(reset);

    JButton toggle = new
JButton(toggleAction);
    toggle.setName("Toggle button in " + panelname);
    
toggle.setText("Toggle");
    if (shiftAlign) {
      
toggle.setAlignmentX((float)5/(float)5.0);
    }
    add(toggle);
  }

  int getState() {
    
return state;
  }

  void setState(int newState) {
    int oldState = state;

    if (oldState ==
newState)
      return;

    state = newState;

    firePropertyChange(State, oldState,
newState);
  }

  public static void main(String[] args) {
    JFrame frame = new JFrame();
    
Container pane = frame.getContentPane();

    pane.add(new Bug(args));

    
frame.invalidate();
    frame.setVisible(true);

        // Set the frame size to make the content
pane
    // fill the window.
    Dimension d = pane.getLayout().preferredLayoutSize(pane);
    
Insets    i = frame.getInsets();
    d.height += i.bottom + i.top;
    d.width  += i.left   + i.right;
    
frame.setSize(d);

    frame.addWindowListener(new WindowAdapter() {
        public void
windowClosing(WindowEvent e) {
          System.exit(0);
        }
      } );
    frame.validate();
  }


  
class togglepanel
    extends JPanel
  {
    togglepanel() {
      String panelname = "Toggle
panel";

      setName(panelname);
      setLayout(new BoxLayout(this, BoxLayout.Y_AXIS));
      
setBorder(BorderFactory.createTitledBorder(panelname));

      for (int i = 0; i < 3; i++) {
        
JToggleButton but = new toggle("Button " + i, panelname);
        if (shiftAlign) {
	  
but.setAlignmentX((float)i/(float)5.0);
        }
        add(but);
      }

      if (useDummy)
{
	JButton dummy = new JButton("Dummy");
	dummy.setName("Dummy button in " +
panelname);
	if (shiftAlign) {
	  
dummy.setAlignmentX((float)3/(float)5.0);
	}
	add(dummy);
      }
    }
  }

  class
toggle
    extends JToggleButton
    implements PropertyChangeListener
  {
    toggle(String
togglename, String panelname) {
      super(toggleAction);
      setText(togglename);
      
setName(togglename + " in " + panelname);

      Bug.this.addPropertyChangeListener(State,
this);
    }

    public void propertyChange(PropertyChangeEvent e) {
      switch (getState())
{
      case START:
        setEnabled(true);
        setSelected(false);
        break;

      case TOGGLED:
        
System.out.println(">>> disabling " + getName());
        setEnabled(false);
        break;
      }
    }
  
}

  class textpanel
    extends JPanel
    implements PropertyChangeListener
  {
    private
int myState;

    textpanel(String panelname, int visibleState) {
      myState =
visibleState;

      setName(panelname);
      setLayout(new BoxLayout(this,
BoxLayout.X_AXIS));
      setBorder(BorderFactory.createTitledBorder(panelname));

      
JComponent text = new JTextField(20);
      text.setName("Text field in " + panelname);

      
add(text);

      Bug.this.addPropertyChangeListener(State, this);

      updateVis();
    
}

    private void updateVis() {
      boolean isVisible = (getState() == myState);

      String vis
= (isVisible ? "visible" : "non-visible");

      System.out.println(">>> making " + getName() + "
" + vis);
      setVisible(isVisible);

      invalidate();
      validate();

      if (isVisible &&
snatchFocus) {
        java.awt.EventQueue.invokeLater(new Runnable() {
          public void run() {
            
textpanel.this.transferFocus();
            dumpFocusCycle("immediately after transferFocus to
textpanel");
            java.awt.EventQueue.invokeLater(new Runnable() {
              public void run() {
                
dumpFocusCycle("invokeLater after transferFocus to textpanel");
              }
            });
          }
        });
      }
    
}

    public void propertyChange(PropertyChangeEvent e) {
      updateVis();
    }
  }

  void
dumpFocusCycle(String where) {
    KeyboardFocusManager km
      =
KeyboardFocusManager.getCurrentKeyboardFocusManager();
    FocusTraversalPolicy pol =
km.getDefaultFocusTraversalPolicy();
    Container root =
km.getCurrentFocusCycleRoot();

    if (null == root) {
      return;
    }

    Component first =
pol.getFirstComponent(root);
    Component comp  = first;

    System.out.println("*** Focus
cycle " + where);
    int i = 0;
    do {
      if (null != comp) {
        
System.out.println(comp.getName());
        comp = pol.getComponentAfter(root, comp);
      }
      
i++;
    } while (null != comp && first != comp && 15 > i);
    if (15 == i) {
      System.out.println("  -
infinite loop-");
    }

    System.out.println("*** Current focus owner");
    comp =
km.getFocusOwner();
    if (null == comp) {
      System.out.println(" -none-");
    } else {
      
System.out.println(comp.getName());
    }

    System.out.println("***");
  }
}




---------- END SOURCE ----------

CUSTOMER WORKAROUND :
Sometimes transferFocus can be used to snatch focus away
from the
disabled or non-visible component and back into
the real focus cycle.
However, this is not always reliable.
(Review ID: 178749)
======================================================================

Comments
SUGGESTED FIX http://sa.sfbay.sun.com/projects/awt_data/7/4685768
17-06-2008

EVALUATION Currently, with the fix for 4726458 (JDK7.0 b17) the test would work fine if only another bug would not exist. That another bug is as follows. The fix for 4726458 is to restore focus on dispatching FOCUS_GAINED on a unfocusable/invisible/disabled component. However, in the restore-focus procedure we allow to request focus on a disabled component. This results in the following: 1. "Remove Button" is pressed. 2. Focus is auto transfered to "Edit Button" 3. When FOCUS_GAINED on "Edit Button" is being dispatched, it's rejected and focus is restored to the most recent focus owner. The latter is "Remove Button". It's disabled but nevetheless focus is requested to it. After that everything gets into an endless loop. The details of the loop don't matter as the true reason of the problem is that focus is restored to a disabled component.
31-10-2007

EVALUATION Some changes (6180261) were made to improve the situation with disable and other ineligible-for-input components. The changes made remain valid in the frames of the current specification. No other changes are planned for Mustang. Additional API is planned for Dolphin.
27-09-2005

CONVERTED DATA BugTraq+ Release Management Values COMMIT TO FIX: mustang
17-09-2004

WORK AROUND Except for the workaround suggested in the test itself (which isn't always easy to apply, since you have to think carefully about the order in which you disable components), it should be possible to request focus to the desired component after all other procedures are completed. For example, the listener that is registered with jb2 might look like this: jb2.addActionListener (new ActionListener () { public void actionPerformed (ActionEvent evt) { tf.setText ("Remove Button selected"); /* * If you switch the order here, then the problem doesn't * occur. */ jb2.setEnabled (false); jb3.setEnabled (false); tf.requestFocusInWindow(); <= This is the key line here } }); After that, the test starts working as desired. ###@###.### 2002-10-02
02-10-2002

EVALUATION The test has two buttons (at least), when one of the buttons is clicked it disables that component and disables the second button. When the first button is disabled focus moves to the second button, but because the request is asynchronous the second button dosen't think it has focus and therefore setEnabled(false) doesn't transfer focus and we end up with focus on a disabled component. This appears to be an awt issue, and now swing specific. ###@###.### 2002-05-15 Since this is an accessibility issue, we should try to investigate this for mantis. ###@###.### 2002-05-15 To add a little to Scott's evaluation above: awt only checks the eligibility of a Component to have focus (i.e. that the component is enabled, visible etc.) at the moment of focus request being made. This is not the same moment as the focus actually arrives to the component. So the following scenario happens: 1. when jb2 is disabled, an attemps to auto-transfer focus to jb3 is done. jb3 is enabled, so it can accept focus, and the request is posted to the queue. 2. when jb3 is disabled, it is not yet a focus owner (i.e. the request posted in step 1 is not yet processed). So no attempt to auto-transfer focus is made. 3. The request gets processed, and focus is assigned to jb3. Unfortunately, it is not as easy as just checking if the component is enabled at the moment of focus assignment. Some redesign of request focus processing might be necessary to fix this. ###@###.### 2002-10-02 I have discussed this issue with Joe Warzecha, and it appears the issue can be worked around for now. As the changes that are needed to fix this are probably too risky for Mantis, I am committing this to Tiger instead. ###@###.### 2002-10-02 commit to Mustang. It's again too late and risky to fix this. ###@###.### 2003-07-29
02-10-2002