1

I have a Swing program that does a search based on the contents of some text fields and settings of a pair of radio buttons (in a button group). The program will automatically search when certain of the text fields lose focus. The problem comes in when the lose focus event is triggered by a click on one of the radio buttons. The lost focus event on the text field is getting processed before the radio button isSelected() values have changed, so the search is done with the "wrong" (i.e. old) parameters, instead of the parameters based on the new setting of the radio buttons.

I tried invoking the search using my own invokeWhenIdle method (shown below) to run the search after the event queue had settled down, but it still is using the old setting of the radio buttons.

My only working solution is to delay for 250 milliseconds in the lost focus event before running the search, so that the radio buttons have time to change. This works, but it makes the UI seem sluggish.

Any better ideas?

public static void invokeWhenIdle(final int a_max_retry, final Runnable a_runnable) {
  if (a_max_retry <= 0) {
    throw new IllegalStateException("invokeWhenIdle: Could not run " + a_runnable);
  }

  // get the next event on the queue
  EventQueue l_queue = Toolkit.getDefaultToolkit().getSystemEventQueue();
  AWTEvent l_evt = l_queue.peekEvent();
  if (l_evt == null) {
    // nothing left on the queue (but us), we can do it
    SwingUtilities.invokeLater(a_runnable);
  } else {
    // still something in the queue, try again
    SwingUtilities.invokeLater(new Runnable() {
      public void run() {
        invokeWhenIdle(a_max_retry - 1, a_runnable);
      }
    });
  }
}
Mitch
  • 899
  • 9
  • 23
  • Well there's no real way to know when you should run the search or when you should wait because the user might click on a radio button. You'll either need to use a timer to run the search after a certain amount of time since the user's last keystroke, or not do this and let the user start the search by clicking the 'Search' button. You shouldn't need to play around with the `EventQueue` like this - check out [How to Use Swing Timers](http://docs.oracle.com/javase/tutorial/uiswing/misc/timer.html). When you detect a keystroke, restart the timer – Nate W. Dec 02 '11 at 18:37
  • My workaround for this was to make the radio buttons non focusable (setFocusable(false)) so that they don't cause the lost focus event on the text fields. Still curious if there is a solution the other way. – Mitch Dec 02 '11 at 18:40
  • You don't need to do that either, you can use a timer connected to the keystrokes on the search field. – Nate W. Dec 02 '11 at 18:41
  • But it's not the keystrokes in the search field that trigger a search, but when the field loses focus. – Mitch Dec 02 '11 at 19:13
  • 1
    I think that perhaps you shouldn't start your search on focus lost since it's obviously not working well for you. Perhaps better to let the user set up the fields as desired and then press a "submit" button. – Hovercraft Full Of Eels Dec 02 '11 at 19:15
  • Searching on lost focus is a business requirement. Nothing I can do about that, and I wouldn't change it if I could. If makes sense in the context in which it is used. – Mitch Dec 02 '11 at 19:55
  • But it needs an unreliable kludge to be fixed. Could you disable this feature if the `ButtonGroup#getSelection()` returns `null`, in other words, if no JRadioButtons are selected? – Hovercraft Full Of Eels Dec 02 '11 at 20:36
  • I could, but the people that pay the bills want one of them to be always selected as a default. I'm fine with my workaround the way it is. It might even be better from the user perspective than my original design. At this point, the question is simply one of curiosity. – Mitch Dec 02 '11 at 21:11

1 Answers1

1

Not an answer, but an explanation about what is happening. Maybe it will spark an idea...

The problem is that a mousePressed arms the button model and the mouseReleased actually changes the selected value of the model.

When you execute the FocusListener code the radio button the model is in an undefined state. Even if you add the FocusListener code to the end of the EDT by using invokeLater the code will still execute before the mouseReleased event is generated.

The following shows how you might code the listener to handle this. It assumes the state of the button is about to change:

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;

public class FocusSSCCE extends JPanel
{
    public FocusSSCCE()
    {
        final JRadioButton radio = new JRadioButton("Radio");
        add( radio );
        radio.setMnemonic('R');

        JTextField textField = new JTextField(10);
        add( textField );

        JButton button = new JButton("Button");
        add( button );

        textField.addFocusListener( new FocusAdapter()
        {
            public void focusLost(FocusEvent e)
            {
                boolean isSelected = radio.isSelected();

                //  Assumes selected state will change

                if (radio.getModel().isArmed())
                    isSelected = !isSelected;

                System.out.println( isSelected );
            }
        });
    }

    private static void createAndShowUI()
    {
        JFrame frame = new JFrame("FocusSSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add( new FocusSSCCE() );
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setVisible( true );
    }

    public static void main(String[] args)
    {
        EventQueue.invokeLater(new Runnable()
        {
            public void run()
            {
                createAndShowUI();
            }
        });
    }
}

However, even this approach can't be guaranteed to work. If for some reason the user generates the mousePressed event on the radio button and them moves the mouse away from the radio button before releasing the mouse, then the selected state of the radio button is not changed.

Similiarly, even your original implementation to sleep for 250ms can not be guaranteed to work because the user could theoretically hold down the mouse for more than 250ms which would also generate the wrong value.

My workaround for this was to make the radio buttons non focusable

I can't think of any better approach.

Edit:

I just thought of a wild solution.

textField.addFocusListener( new FocusAdapter()
{
    public void focusLost(FocusEvent e)
    {
        if (e.getOppositeComponent() instanceof JRadioButton)
        {
            final JRadioButton radio = (JRadioButton)e.getOppositeComponent();

            MouseListener ml = new MouseAdapter()
            {
                public void mouseReleased(MouseEvent e)
                {
                    System.out.println( radio.isSelected() );
                    radio.removeMouseListener(this);
                }
            };

            radio.addMouseListener( ml );
        }

        else
            System.out.println( radio.isSelected() );
    }
});

Basically your processing code won't execute until the mouse has been released when you click on the radio button.

camickr
  • 308,339
  • 18
  • 152
  • 272
  • thanks for brutte-force idea, since I would be going to implements Timer directly, your hack really works, but by dealying by Timer throught whole and current JVM instance +1 – mKorbel Dec 03 '11 at 23:02