1

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.

Community
  • 1
  • 1
Chupo_cro
  • 698
  • 1
  • 7
  • 21

1 Answers1

3

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().

camickr
  • 321,443
  • 19
  • 166
  • 288
  • Thank you very much for the code! Are you suggesting it is **not** possible to implement the desired behavior using either of my approaches? The code I posted was only to demonstrate the issue, the real application is going to contain many input fields with different and more complicated validating algorithms and I have a feeling it would be a large overhead to re-evaluate everything after each entered character. I wrote the question out of curiosity too, not only to find a workaround. There might be something that could be done - maybe changing the `opposite` on-the-fly somehow or similar? – Chupo_cro Aug 05 '16 at 01:18
  • @Chupo_cro, maybe my hack will give you some ideas? See edit. – camickr Aug 05 '16 at 01:35
  • I have already tried to force the focus target with `requestFocusInWindow()`, you can see the commented line `//btnStart.requestFocusInWindow(); // <-- does not help` in my code, I also mentioned it in the question and said it did not work either. I'll try with `invokeLater()` to see if that helps. I am surprised such a basic behavior is so difficult to achieve. I'd expect the API was designed with having 'allow creating the UI in an effectve and straightforward way' on developer's mind. – Chupo_cro Aug 05 '16 at 01:52
  • 2
    @Chupo_cro, `I am surprised such a basic behavior is so difficult to achieve.` - its not. The invokeLater works fine for me. I didn't notice your code because you should be setting focus when you enable the button, not when you return from the method. That is you don't want to try to request focus if the button is still disabled. – camickr Aug 05 '16 at 02:38
  • Yes, the `invokeLater()` indeed did the trick. I have just now edited the question and added the code upon your suggestion as proof of concept. I did request the focus **after** enabling the button, please see the line `setBtnState(input); // enable or disable the button` just one line above the `//btnStart.requestFocusInWindow(); // <-- does not help`. The first line enables the button but it didn't work without `invokeLater()`. Now the only 'mistery' remains if there is a way to achieve the same using my `approach 02` (see my edit). – Chupo_cro Aug 05 '16 at 02:55
  • @Chupo_cro `I did request the focus after enabling the button` - I said I didn't notice the code the first time because the code is in the wrong place. You should be requesting focus at the same time you enable the button. You should NOT be requesting focus if the button is still disabled, which your commented out code does. – camickr Aug 05 '16 at 03:10