1
  • The code below shows just a textfield and a button.

  • The textfield has got an inputVerifier which doesn't accept an empty field.

  • As long as the verifier's "false" result is signalled by an optionPane, the button's background turns gray after closing the optionPane and turns "pressed" on mouseOver (and only if the button was clicked; not if the textfield was left with the tab key).

    Now remove the comment slashes for the button's MouseListener and run the program again. You'll see that the button returns to its regular background once the optionPane is closed.

    This solution works in many cases, but is not free from getting instable when it comes to real programs beyond the scope of an SSCCE. With instable I mean that sometimes everything works as expected and sometimes although the inputVerifier signals the error and returns "false", the focus is released from the textField and thus the missing input is accepted. I assume this is due to the invokeLater in the MouseListener.

    I could reduce my current actual code to the minimum to demonstrate the problem, but I'm afraid that I end up with several pages of code. So I'd first like to ask, whether someone has already dealt with the problem and can give a hint. Thanks.

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
 
public class ButtonBackground2 extends JFrame {

    public ButtonBackground2() {
        setSize(350, 200);
        setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);
        setLocationRelativeTo(null);

        JPanel p= new JPanel();
        JTextField tf = new JTextField();
        tf.setPreferredSize(new Dimension(100, 20));
        tf.setInputVerifier(new NonEmptyVerifier());
        p.add(tf);
        add(p, BorderLayout.CENTER);

        p= new JPanel();
        JButton btn = new JButton("Button");
        btn.setPreferredSize(new Dimension(80, 30));
//        btn.addMouseListener(new BtnBackgroundListener());
        p.add(btn);
        add(p, BorderLayout.SOUTH);
        setVisible(true);
    }
 
 
    public static void main(String arg[]) {
        EventQueue.invokeLater(ButtonBackground2::new);
    }
 
 
    class NonEmptyVerifier extends InputVerifier {
/*
        public boolean shouldYieldFocus(JComponent source, JComponent target) {
            return verify(source);
        }
*/
        public boolean verify(final JComponent input) {
            JTextField tf = (JTextField) input;
            if (tf.getText().trim().length()>0) {
                System.out.println("OK");
                return true;
            }
            JOptionPane.showMessageDialog(ButtonBackground2.this,
                        "Enter at least one character.",
                        "Missing input", JOptionPane.ERROR_MESSAGE);
            return false;
        }
    }


    class BtnBackgroundListener extends MouseAdapter {
        public void mousePressed(final MouseEvent e) {
            SwingUtilities.invokeLater(() -> {
                JButton btn= (JButton)e.getSource();
                if (!btn.hasFocus()) btn.getModel().setPressed(false);
            });
        }
    }
 
}



EDIT
Surprisingly I could reduce my actual code to a small portion to demonstrate the misbehaviour.

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

public class Y extends JFrame {
  public static final long serialVersionUID = 100L;

  public Y() {
    setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    setSize(300, 240);
    setLocationRelativeTo(null);

    add(createTextFieldPanel(), BorderLayout.CENTER);
    JButton bOK= new JButton("OK");
    bOK.addActionListener(e -> System.out.println("OK, input accepted."));
/*  Adding the following listener makes in case of erroneous input the focus
    locking of the textfield's InputVerifier shaky. The InputVerifier itself,
    however, works alright, as one sees from the unfailingly displayed error
    message.
*/
    bOK.addMouseListener(new BtnBackgroundListener());
    add(bOK, BorderLayout.SOUTH);
    setVisible(true);
  }


  static public void main(String args[]) {
    EventQueue.invokeLater(Y::new);
  }


  private JPanel createTextFieldPanel() {
    JPanel p= new JPanel(new FlowLayout(FlowLayout.LEFT));
    p.add(new JLabel("Input:"));
    MyTextField tf= new MyTextField(this);
    tf.setPreferredSize(new Dimension(95, 20));
    tf.setFont(new Font("Monospaced", Font.PLAIN, 13));
    p.add(tf);
    return p;
  }
}

-----------------------------------------------------------


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

public class MyTextField extends JTextField {
  public static final long serialVersionUID = 50161L;

  Component parent;

  public MyTextField(Component parent) {
    this.parent= parent;
    setInputVerifier(new InputVerifier() {
/*
      public boolean shouldYieldFocus(JComponent source, JComponent target) {
        return verify(source);
      }
*/
      public boolean verify(JComponent comp) {
        if (getText().equals("pass")) return true;
        JOptionPane.showMessageDialog(parent,
            "Input does not match the requested format.\n"+getText(),
            "Input error", JOptionPane.ERROR_MESSAGE);
        return false;
      }
    });
  }
}

So first we can say that Camickr was right in doubting that length/complexity of code was of any influence.
And second that in this demo, too, removing the MouseListener stops the focus from being released inappropriately.
So why is program ButtonBackground2 working and program Y only at times? Sometimes the incorrect input is accepted on the very first button click, sometimes one has to repeat the click several times.
By the way I'm running jdk 18, build 18+36-2087.

Jörg
  • 214
  • 1
  • 9
  • 1
    Maybe you can use the approach found here: https://stackoverflow.com/questions/43762086/verifying-jtextfields-that-may-not-get-focus/43763162#43763162. It uses a DocumentListener instead of an InputVerifier to control when a button should be enabled. – camickr May 29 '22 at 19:32
  • @camickr Thanks for your response. Your code is nice for enabling/disabling a Submit button, but a DocumentListener never tells me when input in a field is finished. So, if I see things right, in applying your code I could do field validation only when the Submit button is pressed. However what I like with InputVerifiers is their immediate response, so that when one leaves a field without an error message, it is really done. - My current not very elegant workaround is to set a doNotDisplayTheErrorMessage flag and run the verifier again after pressing Submit. (cont.) – Jörg May 29 '22 at 21:07
  • This copes a false focus release, which fortunately happens only when pressing the button and not when focus goes to the next textField. – Jörg May 29 '22 at 21:07
  • 1
    *I assume this is due to the invokeLater in the MouseListener.* - the code in the MouseListener has nothing to do with focus. So I fail to see why focus would be affected. *but is not free from getting instable when it comes to real programs* - I fail to see how real programs would be different. The code only deals with two components at a time, the text field and the button. So even if your real application has multiple components on the frame it should not work differently. You need to isolate why/when it stops working as you have more components. – camickr May 29 '22 at 22:53
  • 1
    Also, when creating a JTextField use something like: `new JTextField(10)` so the text field can determine its own preferred size. You should not be hardcoding a preferred size. – camickr May 29 '22 at 22:55
  • Thanks for your remarks. Me too, I don't see why tiny and large programs sometimes show different behaviour. It's just that this is not the first time I experience this kind of inputverifier problems. I will cut down my code as much as I can tomorrow and come back here. - As for the ```new JTextField(10)``` I have some apps where I set the textfield to monospaced font and set the size to the maximum allowed character count. With the mentioned constructor the input string was always shifted a little bit to the left when reaching the maximum. – Jörg May 30 '22 at 22:01
  • I edited my post and added a demo code. – Jörg May 31 '22 at 23:58
  • Code should be posted in the form of an [mre]. This means all the code should be in a single source file so we can copy/paste/compile and test easily. You included two source files and the copy/paste the BtnBackgroundListener from your original code. We don't have time to fix compile errors. In any case I did take the time to fix the code and yes I can duplicate the problem which if I understand correctly is the the ActionListener code is still being invoked even when invalid text is entered in the text field. I don't know of any way to prevent this. – camickr Jun 01 '22 at 01:02
  • @Camickr Thanky you for taking your time. I shall improve in handing in code. And yes, the problem is that the ActionListener is invoked even when the text is invalid. – Jörg Jun 01 '22 at 08:11

2 Answers2

3

I can reproduce what you are seeing in Java 8 too. The rest of this answer is going to work with Java 8.

The problem lies in the implementation of BtnBackgroundListener.

The created JButton in the Y class (ie reference bOK) uses a DefaultButtonModel which is a ButtonModel. But in general any AbstractButton uses a ButtonModel instance.

According to the documentation of the ButtonModel interface:

...pressing and releasing the mouse over a regular button triggers the button and causes and ActionEvent to be fired.

Consider the following code:

import javax.swing.JButton;
import javax.swing.SwingUtilities;

public class Test {
    
    private static void runExperiment() {
        final JButton button = new JButton("Test");
        button.addActionListener(e -> System.out.println("Action!"));
        button.getModel().setArmed(true);
        button.getModel().setPressed(true);
        System.out.println("Before release...");
        button.getModel().setPressed(false);
    }
    
    public static void main(final String[] args) {
        SwingUtilities.invokeLater(Test::runExperiment);
    }
}

If you run this, you will see the following output:

Before release...
Action!

and then the program will terminate. This demonstrates that when releasing the button model (after it is pressed+armed), then an ActionEvent is fired on the given ActionListener. In your code the setPressed(false) is invoked inside the implementation of BtnBackgroundListener (within invokeLater but I will note this a bit later).

So who is calling setArmed(true) and setPressed(true) beforehand (which are required for initiating the pressed state)? According to the source or a simple expreriment (eg System.out.println(BasicButtonUI.class.isInstance(button.getUI()));), one can see that the installed ButtonUI on the button is (a subclass) of type BasicButtonUI, which in turn installs the default MouseListener which does what you can imagine it does: it makes the button function properly by changing the model's state to armed and pressed when the user clicks the mouse inside the button bounds. It also enables rollover effects, releasing, and other stuff, but those are irrelevant for the sake of the problem.

BtnBackgroundListener is a MouseListener too, which is also installed on the button (along with the default one that the UI installed). So when you click on the button then both MouseListeners are invoked (note MouseListeners also work on components which do not currently have focus). So the code internally calls all the MouseListeners sequencially, but it doesn't really matter in what sequence, because by calling the setPressed(false) inside the SwingUtilities#invokeLater method you make sure that the release of the model will happen after all MouseListeners have been invoked. Thus the default MouseListener first sets the button to armed and pressed, and some time later you release the model, which in turn fires an ActionEvent on each ActionListener (including the one which accepts the input).

Calling your MouseListener always happens. Releasing the model though in your MouseListener doesn't always trigger an ActionEvent and I can't think of a possible explanation for this right now.

To prevent this...

Don't use a MouseListener for listening on action events on buttons. This whole logic is already implemented and you only have to register an ActionListener alone.

Since you want to use an InputVerifier then I would suggest to pass a flag on the button's action listener (from the InputVerifier) which will indicate the sanity of the input. For example:

import java.awt.GridLayout;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class Main {
    
    private static class MyInputVerifier extends InputVerifier {
        
        private boolean validInput = false;
        
        @Override
        public boolean verify(final JComponent input) {
            validInput = ((JTextField) input).getText().equals("pass");
            return validInput;
        }
    }
    
    private static void createAndShowGUI() {
        
        final JTextField field1 = new JTextField(12),
                         field2 = new JTextField("Anything");
        final JButton accept = new JButton("Submit");
        
        final MyInputVerifier miv = new MyInputVerifier();
        field1.setInputVerifier(miv);
        
        accept.addActionListener(e -> {
            if (miv.validInput)
                System.out.println("Accepted!");
            else
                JOptionPane.showMessageDialog(accept, "Invalid input!");
        });
        
        final JPanel form = new JPanel(new GridLayout(0, 1));
        form.add(field1);
        form.add(field2);
        form.add(accept);
        
        final JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(form);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
    
    public static void main(final String[] args) {
        SwingUtilities.invokeLater(Main::createAndShowGUI);
    }
}
gthanop
  • 3,035
  • 2
  • 10
  • 27
  • Thank you for your keen and enlightening answer. Still, in my view your current solution has the drawback that the user after an erroneous input will not be informed why the focus is tied to field1, when tabbing or clicking to the next field. He even might be convinced that his input is right. So I added one line to the verify method before the return statement: `if (!validInput) JOptionPane.showMessageDialog(input, "Verifier detected invalid input!");` but with this line the original situation that the button's background changes has returned. Any idea for that? – Jörg Jun 01 '22 at 09:10
  • (1+) setting a boolean flag to prevent the ActionListener from proceeding was my thought as well. I used this MRE and the OP's original concept to reset the button model in my suggeston. – camickr Jun 01 '22 at 14:22
2

This suggestions builds on gthanop's suggestion and incorporates the OP's approach to reset the button model state when validation fails:

import java.awt.GridLayout;
import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JPanel;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class Main5 {

    private static class MyInputVerifier extends InputVerifier {

        private boolean validInput = false;
        private JButton button;

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

        @Override
        public boolean verify(final JComponent input)
        {
            validInput = ((JTextField) input).getText().equals("pass");

            if (!validInput)
            {
                JOptionPane.showMessageDialog(input, "Verifier detected invalid input!");

                button.getModel().setPressed(false);
            }

            return validInput;
        }
    }

    private static void createAndShowGUI() {

        final JTextField field1 = new JTextField(12),
                         field2 = new JTextField("Anything");
        final JButton accept = new JButton("Submit");

        final MyInputVerifier miv = new MyInputVerifier(accept);
        field1.setInputVerifier(miv);

        accept.addActionListener(e -> {
            if (miv.validInput)
                System.out.println("Accepted!");
//            else
//                JOptionPane.showMessageDialog(accept, "Invalid input!");
        });

        final JPanel form = new JPanel(new GridLayout(0, 1));
        form.add(field1);
        form.add(field2);
        form.add(accept);
        
        final JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.getContentPane().add(form);
        frame.pack();
        frame.setLocationRelativeTo(null);
        frame.setVisible(true);
    }
    
    public static void main(final String[] args) {
        SwingUtilities.invokeLater(Main5::createAndShowGUI);
    }
}
camickr
  • 321,443
  • 19
  • 166
  • 288