Java Solaris Communities Sun Store Join SDN My Profile Why Join?
 
Bug Database
Bug Detail
Quick Lists
Top 25 Bugs
Top 25 RFE's
Recently Closed Bugs
Printable Page Printable Page


Bug Database
Bug ID: 4685768
Votes 16
Synopsis A11y issue - Focus set to disabled component, can't Tab/Shift-Tab
Category java:classes_awt
Reported Against 1.4.1 , 4_fcs , hopper-beta
Release Fixed 7(b36)
State 10-Fix Delivered, bug
Priority: 4-Low
Related Bugs 4686754 , 4801705 , 4886881
Submit Date 15-MAY-2002
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.
 





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 :
 customer  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)
======================================================================
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.
  xxxxx@xxxxx   2002-10-02
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.
  xxxxx@xxxxx   2002-05-15 

Since this is an accessibility issue, we should try to investigate this 
for mantis. 
  xxxxx@xxxxx   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. 
  xxxxx@xxxxx   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.
  xxxxx@xxxxx   2002-10-02

commit to Mustang. It's again too late and risky to fix this.
  xxxxx@xxxxx   2003-07-29
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.
Posted Date : 2005-09-27 14:06:57.0

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.
Posted Date : 2007-10-31 14:25:52.0
Comments
  
  Include a link with my name & email   

Submitted On 05-DEC-2002
sfriedberg
Making the decision on focus target significantly before the focus can be transferred leads to all kinds of serious problems, including total breakage of keyboard navigation.  I have also seen (transient, normally resolved by EventQueue processing) infinite loops in the focus cycle, which can hang applications altogether on an unfortunately-time keystroke.

The new Component.autoTransferFocus is addressing the right issue, but the implementation should be revised. Simply deferring the choice of focus target (as well as transfer of focus) can lead to "fights" between several components, each deciding where to do a deferred transferFocus, and an unpredictable result. I'd suggest that the need to autoTransferFocus be a flag, with one decision and one transfer taking place at the appropriate time.


Submitted On 29-JAN-2003
guest23
I wonder if this bug has anything to do with a problem I have.
I have not been able to point out the exact error, but it
seems that if severel focusrequests for a celleditor is
pending, while the contents of of a JTable changes (eg. the
celleditor is removed/stopped), the focus gets lost, and I
need to focus with the mouse to be able to type again.

The problem is very annoying, and I ended up writing a Fifo
for some events, and sorting other events, to for the ease
of my users, not to have to click in the application window.

I wonder if anyone else has same expirience as I, and maybe
know another bugnumber that applies better.


Submitted On 09-JUL-2004
pitzs
I received a defect against the product I work on that is a result of this bug. When is this scheduled to be fixed? What release is Mustang? Thanks.


Submitted On 19-OCT-2004
ragonsalves
My product has an issue which is directly related to this bug.  Fixing it would be most welcome.


Submitted On 02-JUN-2008
Since the problem is now understood, a fix is in hand, and this bug has been deferred from Mantis to Tiger to Mustang and deferred yet again, can we please get the fix in JDK7 before it becomes "too late and risky" for a FIFTH time?  Thanks.


Submitted On 17-JUN-2008
Yes, we can. The fix is in jdk7.



PLEASE NOTE: JDK6 is formerly known as Project Mustang