Uploaded image for project: 'JDK'
  1. JDK
  2. JDK-4685768

A11y issue - Focus set to disabled component, can't Tab/Shift-Tab

XMLWordPrintable

    • b36
    • x86, sparc
    • solaris_8, windows_2000
    • Not verified

      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)
      ======================================================================

            ant Anton Tarasov (Inactive)
            jwarzech Joe Warzecha (Inactive)
            Votes:
            0 Vote for this issue
            Watchers:
            0 Start watching this issue

              Created:
              Updated:
              Resolved:
              Imported:
              Indexed: