JButton stays pressed when focus stolen by JOptionPane

夙愿已清 提交于 2019-11-30 21:17:46

The method verify is actually not a good place to open a JOptionPane.

There are several approaches you could consider to solve your problem:

  1. You want this JOptionPane to appear everytime the textfield looses the focus and the input is incorrect: use a FocusListener on the JTextField and act upon appropriate events
  2. You want this JOptionPane to appear everytime the buttons is pressed: use your ActionListener to do it if the input is incorrect.

Here is a small snippet of the latter option:

import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;

import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;

public class VerifierTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public VerifierTest() {
        final JTextField tf = new JTextField("TextField1");

        getContentPane().add(tf, BorderLayout.NORTH);
        tf.setInputVerifier(new PassVerifier());

        final JButton b = new JButton("Button");
        b.setVerifyInputWhenFocusTarget(true);
        getContentPane().add(b, BorderLayout.EAST);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
                if (!tf.getInputVerifier().verify(tf)) {
                    JOptionPane.showMessageDialog(tf.getParent(), "illegal value: " + tf.getText(), "Illegal Value",
                            JOptionPane.ERROR_MESSAGE);
                }
                if (b.hasFocus()) {
                    System.out.println("Button clicked");
                }
            }
        });
        setDefaultCloseOperation(EXIT_ON_CLOSE);
    }

    public static void main(String[] args) {
        Frame frame = new VerifierTest();
        frame.setSize(400, 200);
        frame.setVisible(true);
    }

    class PassVerifier extends InputVerifier {

        @Override
        public boolean verify(JComponent input) {
            final JTextField tf = (JTextField) input;
            String pass = tf.getText();
            return pass.equals("Manish");
        }
    }
}

Also consider setting the default close operation of the JFrame instead of adding a window listener (but it is a good approach to use a WindowListener if you want to pop up a dialog asking the user if he is sure he wants to exit your application).

I added a call to SwingUtilities to ensure that the GUI is on the event thread, and I removed your reference to Frame.

The GUI works for me on Windows XP.

import java.awt.BorderLayout;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;

public class VerifierTest implements Runnable {

    private static final long serialVersionUID = 1L;

    public VerifierTest() {

    }

    @Override
    public void run() {
        JFrame frame = new JFrame();
        frame.setSize(400, 200);

        JTextField tf;
        tf = new JTextField("TextField1");
        tf.setInputVerifier(new PassVerifier());
        frame.getContentPane().add(tf, BorderLayout.NORTH);

        final JButton b = new JButton("Button");
        b.setVerifyInputWhenFocusTarget(true);
        frame.getContentPane().add(b, BorderLayout.EAST);
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (b.hasFocus())
                    System.out.println("Button clicked");
            }
        });

        frame.addWindowListener(new MyWAdapter());
        frame.setVisible(true);
    }

    public static void main(String[] args) {
       SwingUtilities.invokeLater(new VerifierTest());
    }

    class MyWAdapter extends WindowAdapter {
        @Override
        public void windowClosing(WindowEvent event) {
            System.exit(0);
        }
    }

    class PassVerifier extends InputVerifier {
        @Override
        public boolean verify(JComponent input) {
            JTextField tf = (JTextField) input;
            String pass = tf.getText();
            if (pass.equals("Manish"))
                return true;
            else {
                String message = "illegal value: " + tf.getText();
                JOptionPane.showMessageDialog(tf.getParent(), message,
                        "Illegal Value", JOptionPane.ERROR_MESSAGE);

                return false;
            }
        }
    }
}

I have added a new mouse listener to the button as below and its seems to be working fine for me now, but I am not sure if it is a good way of rectifying the buttons selection state.

package test;

import java.awt.BorderLayout;
import java.awt.Frame;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseEvent;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;

import javax.swing.InputVerifier;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JOptionPane;
import javax.swing.JTextField;
import javax.swing.plaf.basic.BasicButtonListener;

public class VerifierTest extends JFrame {

    private static final long serialVersionUID = 1L;

    public VerifierTest() {
        JTextField tf;
        tf = new JTextField("TextField1");

        getContentPane().add(tf, BorderLayout.NORTH);
        tf.setInputVerifier(new PassVerifier());

        final JButton b = new JButton("Button");
        b.setVerifyInputWhenFocusTarget(true);
        getContentPane().add(b, BorderLayout.EAST);
        b.addActionListener(new ActionListener() {
            public void actionPerformed(ActionEvent e) {
                if (b.hasFocus())
                    System.out.println("Button clicked");
            }
        });

        b.addMouseListener(new BasicButtonListener(b) {
            @Override
            public void mouseExited(MouseEvent e) {
                ((JButton)e.getSource()).getModel().setArmed(false);
                ((JButton)e.getSource()).getModel().setPressed(false);
            }

        });

        addWindowListener(new MyWAdapter());
    }

    public static void main(String[] args) {
        Frame frame = new VerifierTest();
        frame.setSize(400, 200);
        frame.setVisible(true);
        // frame.pack();
    }

    class MyWAdapter extends WindowAdapter {

        public void windowClosing(WindowEvent event) {
            System.exit(0);
        }
    }

    class PassVerifier extends InputVerifier {

        public boolean verify(JComponent input) {
            JTextField tf = (JTextField) input;
            String pass = tf.getText();
            if (pass.equals("Manish"))
                return true;
            else {
                final String message = "illegal value: " + tf.getText();
                        JOptionPane.showMessageDialog(null, message,
                                "Illegal Value", JOptionPane.ERROR_MESSAGE);

                return false;
            }
        }
    }
}

First: all implementations of InputVerifier which open the dialog in verify() are invalid. They violated their contract, API doc:

This method should have no side effects.

with the "should" really meaning "must not". The correct place for side-effects is shouldYieldFocus.

Second: moving the side-effect (showing the message dialog) correctly into the shouldYieldFocus doesn't work as well ... due to a bug (THEY call it feature request ;-), that's older than a decade and in the top 10 RFEs

Being a hack-around a bug, @dareurdrem's mouseListener is as good as any workable hack can get :-)

Update

After playing a bit with different options to hack around the bug, here's another hack - it's as brittle as all hacks are (and doesn't survive a LAF toggle, has to be re-installed if dynamic toggling is required)

For hacking the mouse behaviour the basic approach is to hook into the listener installed by the ui:

  • find the original
  • implement a custom listener which delegates most events directly to the original
  • for pressed events request focus first: if yielded delegate to original, if not do nothing

The last bullet is slightly more involved because focus events can be asynchronous, so we have to invoke the check for being focused. Invoking, in turn, requires to send a release in case nobody objected.

Another quirk is the rootPane's pressed action (for its defaultButton): it's done without respecting any inputVerifiers by unconditionally calling doClick. That can be hacked by hooking into the action, following the same pattern as hooking into the mouseListener:

  • find the rootPane's pressed action
  • implement a custom action which checks for a potentially vetoing inputVerifier: delegate to the original if not, do nothing otherwise

The example modified along those lines:

public class VerifierTest implements Runnable {

    private static final long serialVersionUID = 1L;

    @Override
    public void run() {
        InteractiveTestCase.setLAF("Win");
        JFrame frame = new JFrame();
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        frame.setSize(400, 200);

        JTextField tf = new JTextField("TextField1");
        tf.setInputVerifier(new PassVerifier());
        frame.add(tf, BorderLayout.NORTH);

        final JButton b = new JButton("Button");
        frame.add(b);
        b.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent e) {
               System.out.println("Button clicked");
            }
        });
        // hook into the mouse listener
        replaceBasicButtonListener(b);
        frame.add(new JTextField("not validating, something else to focus"),
                BorderLayout.SOUTH);
        frame.getRootPane().setDefaultButton(b);
        // hook into the default button action
        Action pressDefault = frame.getRootPane().getActionMap().get("press");
        frame.getRootPane().getActionMap().put("press", new DefaultButtonAction(pressDefault));
        frame.setVisible(true);
    }

    protected void replaceBasicButtonListener(AbstractButton b) {
        final BasicButtonListener original = getButtonListener(b);
        if (original == null) return;
        Hacker l = new Hacker(original);
        b.removeMouseListener(original);
        b.addMouseListener(l);
    }

    public static class Hacker implements MouseListener {
        private BasicButtonListener original;

        /**
         * @param original the listener to delegate to.
         */
        public Hacker(BasicButtonListener original) {
            this.original = original;
        }

        /**
         * Hook into the mousePressed: first request focus and
         * check its success before handling it.
         */
        @Override
        public void mousePressed(final MouseEvent e) {
            if (SwingUtilities.isLeftMouseButton(e)) {
                if(e.getComponent().contains(e.getX(), e.getY())) {
                    // check if we can get the focus
                    e.getComponent().requestFocus();
                    invokeHandleEvent(e);
                    return;
                }
            }
            original.mousePressed(e);
        }

        /**
         * Handle the pressed only if we are focusOwner.
         */
        protected void handlePressed(final MouseEvent e) {
            if (!e.getComponent().hasFocus())  {
                // something vetoed the focus transfer
                // do nothing
                return;
            } else {
                original.mousePressed(e);
                // need a fake released now: the one from the
                // original cycle might never has reached us
                MouseEvent released = new MouseEvent(e.getComponent(), MouseEvent.MOUSE_RELEASED,
                        e.getWhen(), e.getModifiers(), 
                        e.getX(), e.getY(), e.getClickCount(), e.isPopupTrigger()
                        );
                original.mouseReleased(released);
            }
        }


        /**
         * focus requests might be handled
         * asynchronously. So wrap the check 
         * wrap the block into an invokeLater.
         */
        protected void invokeHandleEvent(final MouseEvent e) {
            SwingUtilities.invokeLater(new Runnable() {
                @Override
                public void run() {
                    handlePressed(e);
                }
            });
        }

        @Override
        public void mouseClicked(MouseEvent e) {
            original.mouseClicked(e);
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            original.mouseReleased(e);
        }

        @Override
        public void mouseEntered(MouseEvent e) {
            original.mouseEntered(e);
        }

        @Override
        public void mouseExited(MouseEvent e) {
            original.mouseExited(e);
        }
    }
    public static class DefaultButtonAction extends AbstractAction {

        private Action original;

        /**
         * @param original
         */
        public DefaultButtonAction(Action original) {
            this.original = original;
        }

        @Override
        public void actionPerformed(ActionEvent e) {
            JRootPane root = (JRootPane) e.getSource();
            JButton owner = root.getDefaultButton();
            if (owner != null && owner.getVerifyInputWhenFocusTarget()) {
                Component c = KeyboardFocusManager
                        .getCurrentKeyboardFocusManager()
                         .getFocusOwner();
                if (c instanceof JComponent && ((JComponent) c).getInputVerifier() != null) {
                    if (!((JComponent) c).getInputVerifier().shouldYieldFocus((JComponent) c)) return;
                }


            }
            original.actionPerformed(e);
        }

    }
    /**
     * Returns the ButtonListener for the passed in Button, or null if one
     * could not be found.
     */
    private BasicButtonListener getButtonListener(AbstractButton b) {
        MouseMotionListener[] listeners = b.getMouseMotionListeners();

        if (listeners != null) {
            for (MouseMotionListener listener : listeners) {
                if (listener instanceof BasicButtonListener) {
                    return (BasicButtonListener) listener;
                }
            }
        }
        return null;
    }

    public static void main(String[] args) {
       SwingUtilities.invokeLater(new VerifierTest());
    }


    public static class PassVerifier extends InputVerifier {
        /**
         * Decide whether or not the input is valid without
         * side-effects.
         */
        @Override
        public boolean verify(JComponent input) {
            final JTextField tf = (JTextField) input;
            String pass = tf.getText();
            if (pass.equals("Manish"))
                return true;
            return false;
        }

        /**
         * Implemented to ask the user what to do if the input isn't valid.
         * Note: not necessarily the best usability, it's mainly to
         * demonstrate the different effects on not/agreeing with
         * yielding focus transfer.
         */
        @Override
        public boolean shouldYieldFocus(final JComponent input) {
            boolean valid = super.shouldYieldFocus(input);
            if (!valid) {
                String message = "illegal value: " + ((JTextField) input).getText();
                int goAnyWay = JOptionPane.showConfirmDialog(input, "invalid value: " +
                        message + " - go ahead anyway?");
                valid = goAnyWay == JOptionPane.OK_OPTION;
            }
            return valid;
        }
    }
}

Actually the real problem is in how the focus system and awt listeners interact. There are a few bugs declared in Java that the developers are going back and forth on who is responsible. The mouse listener does : processMouseEvent and within that logic, the current FocusOwner is asked to yield Focus. it fails. But because half the event is processed already, the button becomes armed and the focus remains with the field.

I finally saw one developer comment: Don't let the listener proceed if the field is not allowed to lose focus.

For example: Define a JTextfield with edits to only allow values < 100. A message pops up when you lose focus. I overrode my base JButton classes' processMouseEvent(MouseEvent e) with code:

protected void processMouseEvent(MouseEvent e) {
    if ( e.getComponent() != null && e.getComponent().isEnabled() ) { //should not be processing mouse events if it's disabled.
            if (e.getID() == MouseEvent.MOUSE_RELEASED && e.getClickCount() == 1) {
                // The mouse button is being released as per normal, and it's the first click. Process it as per normal.
                super.processMouseEvent(e);

                // If the release occured within the bounds of this component, we want to simulate a click as well
                if (this.contains(e.getX(), e.getY())) {
                    super.processMouseEvent(new MouseEvent(e.getComponent(),
                                                            MouseEvent.MOUSE_CLICKED,
                                                            e.getWhen(),
                                                            e.getModifiers(),
                                                            e.getX(),
                                                            e.getY(),
                                                            e.getClickCount(),
                                                            e.isPopupTrigger(),
                                                            e.getButton()));
                }
            }
            else if (e.getID() == MouseEvent.MOUSE_CLICKED && e.getClickCount() == 1) {
                // Normal clicks are ignored to prevent duplicate events from normal, non-moved events
            }
            else if (e.getID() == MouseEvent.MOUSE_PRESSED && e.getComponent() != null && (e.getComponent().isFocusOwner() || e.getComponent().requestFocusInWindow())) {// if already focus owner process mouse event
                super.processMouseEvent(e); 
            }
            else {
                // Otherwise, just process as per normal.
                if (e.getID() != MouseEvent.MOUSE_PRESSED) {
                    super.processMouseEvent(e); 
                }
            }
        }
}

in the guts of this logic is the simple questions. Button: Are you already focus owner. if not: can you(Button) possibly GAIN focus ( remember - shouldYieldFocus() is called on the current focus holder inside the requestFocusInWindow() call and will return false ALWAYS if not valid )

This Also has the side affect of popping up your error dialog!

This logic Stops the Java libraries processMouseEvent logic from processing half an event while the Focus System stops it from completing.

Obviously you'll need this type of logic on all your different JComponents that perform an action on a click.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!