Search code examples
javaswingdialogfocus

Default Button doesn't cause JFormattedTextField to commit current edit


I was wondering whether the way I expect the JFormattedTextField to work in combination with a default button is correct. When editing the value of a JFormattedTextField, you usually want to commit the value and then use it, this usually happens on focusLost or sometimes when the ActionListener is manually triggered. Now, if such a text field is inside of a JDialog which has a DefaultButton defined, triggering the default button with Ctrl+Enter will not cause the currently focused JFormattedTextField to trigger it's focusLost event.

Now, I see two solutions for this:

  1. Manually make sure all inupt fields are in a comitted (or reverted) state
  2. Use a SwingUtilities.InvokeLater in the ActionListener of the default button

However, both of these seem dirty, especially the first one, as it's not very easy to extend said dialog without forgetting to handle the components. The second one is rather undesirable imo because more invokeLaters usually mean more code that's harder to debug.

Is there a better way of solving this?

EXAMPLE:

import java.awt.BorderLayout;

import javax.swing.JButton;
import javax.swing.JFormattedTextField;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;

public class DefaultButton
{
  public static void main( String[] args )
  {
    JFrame frame = new JFrame();

    JFormattedTextField field = new JFormattedTextField();
    field.setValue( "HELLO" );

    JButton defaultButton = new JButton( "Default" );
    defaultButton.addActionListener( __ ->
    {
      JOptionPane.showMessageDialog( frame, "You've chosen: " + field.getValue() );
    } );
    frame.getRootPane().setDefaultButton( defaultButton );

    JPanel panel = new JPanel( new BorderLayout() );
    panel.add( field, BorderLayout.NORTH );
    panel.add( defaultButton, BorderLayout.SOUTH );
    frame.add( panel );

    frame.pack();
    frame.setLocationRelativeTo( null );
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE );
    frame.setVisible( true );
  }
}

Solution

  • Yes, when the JFormattedTextField has input focus and you activate the defaultButton, the JFormattedTextField does not lose focus and therefore the value is not committed.

    Refer to How to Use Formatted Text Fields.
    Here is a quote:

    A formatted text field's text and its value are two different properties, and the value often lags behind the text.
    The text property is defined by the JTextField class. This property always reflects what the field displays. The value property, defined by the JFormattedTextField class, might not reflect the latest text displayed in the field. While the user is typing, the text property changes, but the value property does not change until the changes are committed.

    I see two possible solutions (although I imagine there are more).
    In the actionPerformed() method, either call getText() rather than getValue(), or call commitEdit() before calling getValue(), i.e.

    JOptionPane.showMessageDialog( frame, "You've chosen: " + field.getText() );
    

    or

    try {
        field.commitEdit();
    }
    catch (java.text.ParseException x) {
        x.printStackTrace();
    }
    JOptionPane.showMessageDialog(frame, "You've chosen: " + field.getValue());
    

    EDIT

    I believe this is what you are looking for.

    import java.awt.BorderLayout;
    import java.awt.EventQueue;
    import java.awt.KeyboardFocusManager;
    
    import javax.swing.JButton;
    import javax.swing.JFormattedTextField;
    import javax.swing.JFrame;
    import javax.swing.JOptionPane;
    import javax.swing.JPanel;
    
    public class DefaultButton
    {
      public static void main( String[] args )
      {
        JFrame frame = new JFrame();
    
        JFormattedTextField field = new JFormattedTextField();
        field.setValue( "HELLO" );
    
        JButton defaultButton = new JButton( "Default" );
        defaultButton.addActionListener( __ ->
        {
            KeyboardFocusManager kfm = KeyboardFocusManager.getCurrentKeyboardFocusManager();
            if (kfm.getFocusOwner() == field) {
                kfm.focusNextComponent();            
            }
            EventQueue.invokeLater(() -> JOptionPane.showMessageDialog( frame, "You've chosen: " + field.getValue() ));
        } );
        frame.getRootPane().setDefaultButton( defaultButton );
    
        JPanel panel = new JPanel( new BorderLayout() );
        panel.add( field, BorderLayout.NORTH );
        panel.add( defaultButton, BorderLayout.SOUTH );
        frame.add( panel );
    
        frame.pack();
        frame.setLocationRelativeTo( null );
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE );
        frame.setVisible( true );
      }
    }
    

    Concentrate on method actionPerformed(). If the JFormattedTextField currently has the focus then force the focus to move to the next component. Regardless of which component has the keyboard focus, wrap the displaying of the JOptionPane in a invokeLater() call.