Chupo_cro Chupo_cro - 3 months ago 11
Java Question

JButton does not gain focus immediately after setEnabled(true)

I am trying to make an intuitive user interface where the user would enter the numeric values into the

JTextFields
, advance with the TAB key and finally activate the button to start processing the input.

At the beginning the button is disabled and it should be enabled only when all of the data is entered into the text fields.

I am using
javax.swing.InputVerifier
to restrict entering only positive numbers up to 4 decimal places and that works fine.

There are 3 focusable objects, two text fields and the button. Pressing the TAB key after typing the (valid) number into the text field, and if all the text fields contain valid inputs, enables the button. That works fine too.

The problem is:

After typing the valid data into the second text field when the first text field already contains valid data and pressing the TAB, the button does not gain the focus as it should. Instead, the focus is transfered to the next focusable object in a row which is (again) the first text field.

I tried to use two different approaches:


  1. The button's
    enabled
    property is changed via
    FocusListener
    inside overriden
    focusLost()
    method

  2. The button's
    enabled
    property is changed inside overriden
    shouldYieldFocus()
    method



In both cases the focus skips the button immediately after enabling the button. However, if we then continue to change the focus using TAB and SHIFT+TAB keys, the button gains focus as it should - right after the second text field.

It seems to me as the
opposite
component has been predetermined before enabling the button so the button does not gain the focus even after it gets enabled.

I even tried to force the button to gain the focus using
requestFocusInWindow()
after enabling the button but that didnt'n work either so the question is how to force the
LayoutFocusTraversalPolicy
to re-evaluate the Layout so it can immediately take into account the newly introduced button which was before disabled?

Here is the code for both the approaches I tried:


  1. The button's
    enabled
    property is changed via
    FocusListener
    inside
    focusLost()
    method:



package verifiertest;

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Dimension;

import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import javax.swing.text.JTextComponent;
import javax.swing.UIManager;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.math.BigDecimal;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.SwingConstants;
import javax.swing.JTextField;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;

import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.ActionEvent;

public class TestVerifier implements FocusListener {

private JFrame frmInputverifierTest;
private JTextField tfFirstNum;
private JTextField tfSecondNum;
private JLabel lblStatus;
private JButton btnStart;
private String statusText = "Input the numbers and press the \"Start!\" button...";

public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
TestVerifier window = new TestVerifier();
window.frmInputverifierTest.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}

public TestVerifier() {
initialize();
}

private void initialize() {
frmInputverifierTest = new JFrame();
frmInputverifierTest.setTitle("InputVerifier Test");
frmInputverifierTest.setBounds(100, 100, 500, 450);
frmInputverifierTest.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// center the window
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
frmInputverifierTest.setLocation(dim.width/2 - frmInputverifierTest.getWidth()/2, dim.height/2 - frmInputverifierTest.getHeight()/2);

JPanel panelContainer = new JPanel();
panelContainer.setBorder(new EmptyBorder(5, 5, 5, 5));
frmInputverifierTest.getContentPane().add(panelContainer, BorderLayout.CENTER);
panelContainer.setLayout(new BorderLayout(0, 0));

JPanel panelInput = new JPanel();
panelInput.setBorder(new TitledBorder(null, "Input", TitledBorder.LEADING, TitledBorder.TOP, null, null));
panelContainer.add(panelInput, BorderLayout.NORTH);
panelInput.setLayout(new GridLayout(2, 2, 10, 4));

JLabel lblFirstNum = new JLabel("Number #1:");
lblFirstNum.setHorizontalAlignment(SwingConstants.TRAILING);
panelInput.add(lblFirstNum);

tfFirstNum = new JTextField();
panelInput.add(tfFirstNum);
tfFirstNum.setColumns(10);
// setup the verifier
MyTxtVerifier txtVerifier = new MyTxtVerifier();
tfFirstNum.setInputVerifier(txtVerifier);
// add focus listener
tfFirstNum.addFocusListener(this);

JLabel lblSecondNum = new JLabel("Number #2:");
lblSecondNum.setHorizontalAlignment(SwingConstants.TRAILING);
panelInput.add(lblSecondNum);

tfSecondNum = new JTextField();
panelInput.add(tfSecondNum);
tfSecondNum.setColumns(10);
// setup the verifier
tfSecondNum.setInputVerifier(txtVerifier);
// add focus listener
tfSecondNum.addFocusListener(this);

JPanel panelOutput = new JPanel();
panelOutput.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Output (not used at the moment)", TitledBorder.LEADING, TitledBorder.TOP, null, null));
panelContainer.add(panelOutput, BorderLayout.CENTER);

JPanel panelSouth = new JPanel();
panelSouth.setBorder(null);
panelContainer.add(panelSouth, BorderLayout.SOUTH);
panelSouth.setLayout(new GridLayout(0, 1, 0, 0));

JPanel panelStatus = new JPanel();
FlowLayout flowLayout_1 = (FlowLayout) panelStatus.getLayout();
flowLayout_1.setAlignment(FlowLayout.LEFT);
panelStatus.setBorder(new TitledBorder(null, "Status", TitledBorder.LEADING, TitledBorder.TOP, null, null));
panelSouth.add(panelStatus);

lblStatus = new JLabel(statusText);
panelStatus.add(lblStatus);

JPanel panelActions = new JPanel();
panelActions.setBorder(new TitledBorder(null, "Actions", TitledBorder.LEADING, TitledBorder.TOP, null, null));
FlowLayout flowLayout = (FlowLayout) panelActions.getLayout();
flowLayout.setAlignment(FlowLayout.RIGHT);
panelSouth.add(panelActions);

btnStart = new JButton("Start!");
btnStart.setEnabled(false);
btnStart.setVerifyInputWhenFocusTarget(true);
btnStart.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(frmInputverifierTest, "Start button pressed...", "Start", JOptionPane.PLAIN_MESSAGE);
}
});
panelActions.add(btnStart);
}

// an inner class so it can access parent fields
public class MyTxtVerifier extends InputVerifier {
// This method should have no side effects
@Override
public boolean verify(JComponent input) {
String text = ((JTextField)input).getText();
// to allow changing focus when nothing is entered
if(text.isEmpty())
return true;
try {
BigDecimal value = new BigDecimal(text);
if(value.floatValue() <= 0.0)
return false;
return (value.scale() <= 4);
} catch (Exception e) {
return false;
}
}

// This method can have side effects
@Override
public boolean shouldYieldFocus(JComponent input) {
String statusOld, status;

statusOld = statusText; // remember the original text
boolean isOK = verify(input); // call overridden method
if(isOK)
status = statusOld;
else {
btnStart.setEnabled(false);
status = "Error: The parameter should be a positive number up to 4 decimal places";
}
lblStatus.setText(status);
// return super.shouldYieldFocus(input);
return isOK;
}
}

@Override
public void focusGained(FocusEvent e) {
// nothing to do on focus gained
}

@Override
public void focusLost(FocusEvent e) {
// in case we want to show a message box inside focusLost() - not to be fired twice
if(e.isTemporary())
return;
final JTextComponent c = (JTextComponent)e.getSource();
// in case there are more text fields but
// we are validating only some of them
if(c.equals(tfFirstNum) || c.equals(tfSecondNum)) {
// are all text fields valid?
if(c.getInputVerifier().verify(tfFirstNum) && c.getInputVerifier().verify(tfSecondNum) &&
!tfFirstNum.getText().isEmpty() && !tfSecondNum.getText().isEmpty())
btnStart.setEnabled(true);
else
btnStart.setEnabled(false);
}
}
}



  1. The button's
    enabled
    property is changed inside overriden
    shouldYieldFocus()
    method:



package verifiertest;

import java.awt.EventQueue;

import javax.swing.JFrame;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.Dimension;

import javax.swing.border.EmptyBorder;
import javax.swing.border.TitledBorder;
import javax.swing.UIManager;
import java.awt.GridLayout;
import java.awt.Toolkit;
import java.math.BigDecimal;

import javax.swing.JLabel;
import javax.swing.JOptionPane;
import javax.swing.SwingConstants;
import javax.swing.JTextField;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;

import java.awt.FlowLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;

public class TestVerifier {

private JFrame frmInputverifierTest;
private JTextField tfFirstNum;
private JTextField tfSecondNum;
private JLabel lblStatus;
private JButton btnStart;
private String statusText = "Input the numbers and press the \"Start!\" button...";

public static void main(String[] args) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
try {
TestVerifier window = new TestVerifier();
window.frmInputverifierTest.setVisible(true);
} catch (Exception e) {
e.printStackTrace();
}
}
});
}

public TestVerifier() {
initialize();
}

private void initialize() {
frmInputverifierTest = new JFrame();
frmInputverifierTest.setTitle("InputVerifier Test");
frmInputverifierTest.setBounds(100, 100, 500, 450);
frmInputverifierTest.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
// center the window
Dimension dim = Toolkit.getDefaultToolkit().getScreenSize();
frmInputverifierTest.setLocation(dim.width/2 - frmInputverifierTest.getWidth()/2, dim.height/2 - frmInputverifierTest.getHeight()/2);

JPanel panelContainer = new JPanel();
panelContainer.setBorder(new EmptyBorder(5, 5, 5, 5));
frmInputverifierTest.getContentPane().add(panelContainer, BorderLayout.CENTER);
panelContainer.setLayout(new BorderLayout(0, 0));

JPanel panelInput = new JPanel();
panelInput.setBorder(new TitledBorder(null, "Input", TitledBorder.LEADING, TitledBorder.TOP, null, null));
panelContainer.add(panelInput, BorderLayout.NORTH);
panelInput.setLayout(new GridLayout(2, 2, 10, 4));

JLabel lblFirstNum = new JLabel("Number #1:");
lblFirstNum.setHorizontalAlignment(SwingConstants.TRAILING);
panelInput.add(lblFirstNum);

tfFirstNum = new JTextField();
panelInput.add(tfFirstNum);
tfFirstNum.setColumns(10);
// setup the verifier
MyTxtVerifier txtVerifier = new MyTxtVerifier();
tfFirstNum.setInputVerifier(txtVerifier);

JLabel lblSecondNum = new JLabel("Number #2:");
lblSecondNum.setHorizontalAlignment(SwingConstants.TRAILING);
panelInput.add(lblSecondNum);

tfSecondNum = new JTextField();
panelInput.add(tfSecondNum);
tfSecondNum.setColumns(10);
// setup the verifier
tfSecondNum.setInputVerifier(txtVerifier);

JPanel panelOutput = new JPanel();
panelOutput.setBorder(new TitledBorder(UIManager.getBorder("TitledBorder.border"), "Output (not used at the moment)", TitledBorder.LEADING, TitledBorder.TOP, null, null));
panelContainer.add(panelOutput, BorderLayout.CENTER);

JPanel panelSouth = new JPanel();
panelSouth.setBorder(null);
panelContainer.add(panelSouth, BorderLayout.SOUTH);
panelSouth.setLayout(new GridLayout(0, 1, 0, 0));

JPanel panelStatus = new JPanel();
FlowLayout flowLayout_1 = (FlowLayout) panelStatus.getLayout();
flowLayout_1.setAlignment(FlowLayout.LEFT);
panelStatus.setBorder(new TitledBorder(null, "Status", TitledBorder.LEADING, TitledBorder.TOP, null, null));
panelSouth.add(panelStatus);

lblStatus = new JLabel(statusText);
panelStatus.add(lblStatus);

JPanel panelActions = new JPanel();
panelActions.setBorder(new TitledBorder(null, "Actions", TitledBorder.LEADING, TitledBorder.TOP, null, null));
FlowLayout flowLayout = (FlowLayout) panelActions.getLayout();
flowLayout.setAlignment(FlowLayout.RIGHT);
panelSouth.add(panelActions);

btnStart = new JButton("Start!");
btnStart.setEnabled(false);
btnStart.setVerifyInputWhenFocusTarget(true);
btnStart.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(frmInputverifierTest, "Start button pressed...", "Start", JOptionPane.PLAIN_MESSAGE);
}
});
panelActions.add(btnStart);
}

// an inner class so it can access parent fields
public class MyTxtVerifier extends InputVerifier {
// This method should have no side effects
@Override
public boolean verify(JComponent input) {
String text = ((JTextField)input).getText();
// to allow changing focus when nothing is entered
if(text.isEmpty())
return true;
try {
BigDecimal value = new BigDecimal(text);
if(value.floatValue() <= 0.0)
return false;
return (value.scale() <= 4);
} catch (Exception e) {
return false;
}
}

// This method can have side effects
@Override
public boolean shouldYieldFocus(JComponent input) {
String statusOld, status;

statusOld = statusText; // remember the original text
boolean isOK = verify(input); // call overridden method
if(isOK)
status = statusOld;
else {
status = "Error: The parameter should be a positive number up to 4 decimal places";
}
lblStatus.setText(status);
setBtnState(input); // enable or disable the button
//btnStart.requestFocusInWindow(); // <-- does not help
// return super.shouldYieldFocus(input);
return isOK;
}
}

private void setBtnState(JComponent input) {
if (input.equals(tfFirstNum) || input.equals(tfSecondNum)) {
// are all text fields valid?
if (input.getInputVerifier().verify(tfFirstNum) && input.getInputVerifier().verify(tfSecondNum)
&& !tfFirstNum.getText().isEmpty() && !tfSecondNum.getText().isEmpty())
btnStart.setEnabled(true);
else
btnStart.setEnabled(false);
}
}
}


Here is the screenshot of the test application:

The test application screenshot

Note:

The code is related to the code contained in the question I asked before, which was another topic.

EDIT:

Upon trying out the suggestion (using
invokeLater()
to run the
requestFocusInWindow()
) proposed by the author of the accepted answer, here is the code that can serve as a proof of concept:

@Override
public void focusLost(FocusEvent e) {
// in case we want to show a message box inside focusLost() - not to be fired twice
if(e.isTemporary())
return;
final JTextComponent c = (JTextComponent)e.getSource();
// in case there are more text fields but
// we are validating only some of them
if(c.equals(tfFirstNum) || c.equals(tfSecondNum)) {
// are all text fields valid?
if(c.getInputVerifier().verify(tfFirstNum) && c.getInputVerifier().verify(tfSecondNum) &&
!tfFirstNum.getText().isEmpty() && !tfSecondNum.getText().isEmpty())
btnStart.setEnabled(true);
else
btnStart.setEnabled(false);
}
if (btnStart.isEnabled() && e.getOppositeComponent()==tfFirstNum) {
EventQueue.invokeLater(new Runnable() {
@Override
public void run() {
btnStart.requestFocusInWindow();
}
});
}
}


This is just the changed
focusLost()
method pertaining to the
approach #01
. I am not aware if there is similar solution to be used with the
approach #02
- since I don't know if it is possible to reference the
opposite
from inside the
shouldYieldFocus()
when there isn't a
FocusListener
.

Note:

When using this solution it can be clearly observed that after entering the 2nd number and pressing the TAB button, focus first (for the moment of time) jumps to the first text field and only then moves to the button.

Answer

I would suggest you don't use an InputVerifier, but instead use a DocumentListener.

The benefit of using the DocumentListener is that the text field can be edited as each character is entered, so the user has immediate feedback. Then as soon as you enter the first digit the button can be enabled (if it passes your editing criteria).

Since the button will now be enabled before the user attempts to enter the Tab key you will not have any focus issues.

Here is a basic example to get your started:

import java.awt.*;
import java.awt.event.*;
import java.util.List;
import java.util.ArrayList;
import javax.swing.*;
import javax.swing.event.*;

public class DataEntered implements DocumentListener
{
    private JButton button;
    private List<JTextField> textFields = new ArrayList<JTextField>();

    public DataEntered(JButton button)
    {
        this.button = button;
    }

    public void addTextField(JTextField textField)
    {
        textFields.add( textField );
        textField.getDocument().addDocumentListener( this );
    }

    public boolean isDataEntered()
    {
        for (JTextField textField : textFields)
        {
            if (textField.getText().trim().length() == 0)
                return false;
        }

        return true;
    }

    @Override
    public void insertUpdate(DocumentEvent e)
    {
        checkData();
    }

    @Override
    public void removeUpdate(DocumentEvent e)
    {
        checkData();
    }

    @Override
    public void changedUpdate(DocumentEvent e) {}

    private void checkData()
    {
        button.setEnabled( isDataEntered() );
    }

    private static void createAndShowUI()
    {
        JButton submit = new JButton( "Submit" );
        submit.setEnabled( false );

        JTextField textField1 = new JTextField(10);
        JTextField textField2 = new JTextField(10);

        DataEntered de = new DataEntered( submit );
        de.addTextField( textField1 );
        de.addTextField( textField2 );

        JFrame frame = new JFrame("SSCCE");
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.add(textField1, BorderLayout.WEST);
        frame.add(textField2, BorderLayout.EAST);
        frame.add(submit, BorderLayout.SOUTH);
        frame.pack();
        frame.setLocationByPlatform( true );
        frame.setVisible( true );
    }

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

The basic code enables the button whenver any text is entered. You would need to modify the dataEntered() method to apply your editing criteria.

Edit:

I don't know any way using the API to do what you want. Following is a possible hack.

As I understand it you will have the problem is two situations:

  1. When focus is on the last field of the form and you use Tab
  2. When focus is on the first field of the form and you use Shift-Tab

So maybe what you can do is create you InputVerifier with two parameter, the first and last components. Then when you use the FocusListener and

  1. current focus is on the first component and the opposite component is the last
  2. current focus is on the last component and the opposite component is on the first

You know you are wrapping around the form. In these two situations you want focus to be placed on the "Save" button so you need to manually request focus on the Save button. So you could do this by just using:

saveButton.requestFocusInWindow();

Note focus would still go the to opposite component first and then to the button. You might also need to wrap that code in a SwingUtilities.invokeLater().

Comments