Search code examples
javaswingkeyevent

How to prevent char from being entered into JTextField after dispatching event?


In my MainWindow class (a JFrame), I use the "+" and "-" keys as hotkeys to modify the value of a certain JTextField called degreeField up or down. I add a KeyEventDispatcher with the following dispatchKeyEventMethod to my KeyboardFocusManger:

        @Override
        public boolean dispatchKeyEvent(KeyEvent evt) {
            if(simPanel.main.isFocused()){
                int type = evt.getID();
                if(type == KeyEvent.KEY_PRESSED){
                    int keyCode = evt.getKeyCode();
                    switch(keyCode){
                        // other cases
                        case KeyEvent.VK_PLUS:
                            // increase degreeField by 1, if double
                            // otherwise set it to "270.0"
                            return true;
                        case KeyEvent.VK_MINUS:
                            // decrease degreeField by 1, if double
                            // otherwise set it to "270.0"
                            return true;
                        default:
                            return false;
                    }
                }
                else if(type == KeyEvent.KEY_RELEASED){
                    // irrelevant
                    return false;
                }
            }
            return false;
        }

The KeyEventDispatcher works and degreeField's text is modified as expected. However, when I have another JTextField focused, the "+" or "-" is also entered into that field.

Since I return true, I was under the impression that the event should no longer be dispatched by the JTextField I have focused. Using the NetBeans debugger, I put a break point into the relevant case and checked the text of the focused text field. At the time, there was no + appended. The + is therefore appended after I finish dispatching this event and return true.

Anyone got any ideas, how I can actually prevent the event from being passed down further?

I know I could put an extra listener on the text field to prevent "+" and "-" chars from being entered, but I would prefer a solution that works for non-char keys as well. (I have a similar problem with up and down arrow keys; it doesn't break anything, just annoyingly cycles through my text fields).


Thanks to MadProgrammer for this:

InputMap im = senderXField.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
ActionMap am = senderXField.getActionMap();

im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "Pressed.+");
im.put(KeyStroke.getKeyStroke(KeyEvent.VK_UP, 0), "Pressed.up");

am.put("Pressed.+", angleUpAction); // works fine

Sadly

am.put("Pressed.up", indexUpAction); // does not trigger

This applies to all arrow keys. They do their usual thing (move cursor or focus respectively) and don't trigger the actions. If I use the WHEN_FOCUSED InputMap, they work as intended, leading me to believe that their default behavior is implemented as a KeyBinding at the WHEN_FOCUSED level that can be overwritten.

While, technically, as a workaround, I could implement the arrow key commands for all text fields in my MainWindow, that would be ... weird. I hope there's a better solution that let's me keep the command for the entire window but also overwrite their default behaviour.


Solved


Solution

  • I add a KeyEventDispatcher with the following dispatchKeyEventMethod to my KeyboardFocusManger:

    No, no, no and no on so many levels

    The actual answer to your question is two fold.

    First, use a DocumentFilter to filter out undesirable input, see Implementing a DocumentFilter for more details.

    The reason for using this comes down to a number of issues...

    • KeyListener may be notified AFTER the content is committed to the underlying Document model
    • Key processing routines may also ignore the consumed state
    • KeyListener doesn't catch the use-case when the user pastes text into the field
    • KeyListener doesn't catch the use-case when setText is called
    • KeyListener is just a poor choice, all round, for trying to filter content.

    Second, you should be using the Key Bindings API instead of KeyEventDispatcher.

    KeyEventDispatcher is to low level for what you need; is difficult to maintain; doesn't take into consideration other actions which might need to be associated with the same key strokes and quickly becomes messy.

    Key bindings are also more easily re-usable. You can apply the Action used by the key bindings to buttons or even to a more global state. They can also be used to bind multiple keys to a single action, for example, the + key (on the numpad) and the Shift+= keys

    import java.awt.EventQueue;
    import java.awt.GridBagConstraints;
    import java.awt.GridBagLayout;
    import java.awt.event.ActionEvent;
    import java.awt.event.KeyEvent;
    import javax.swing.AbstractAction;
    import javax.swing.ActionMap;
    import javax.swing.InputMap;
    import javax.swing.JButton;
    import javax.swing.JFrame;
    import javax.swing.JPanel;
    import javax.swing.JTextField;
    import javax.swing.KeyStroke;
    import javax.swing.UIManager;
    import javax.swing.UnsupportedLookAndFeelException;
    import javax.swing.text.AbstractDocument;
    import javax.swing.text.AttributeSet;
    import javax.swing.text.BadLocationException;
    import javax.swing.text.Document;
    import javax.swing.text.DocumentFilter;
    
    public class Text {
    
        public static void main(String[] args) {
            new Text();
        }
    
        public Text() {
            EventQueue.invokeLater(new Runnable() {
                @Override
                public void run() {
                    try {
                        UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                    } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | UnsupportedLookAndFeelException ex) {
                        ex.printStackTrace();
                    }
    
                    JFrame frame = new JFrame("Testing");
                    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                    frame.add(new TestPane());
                    frame.pack();
                    frame.setLocationRelativeTo(null);
                    frame.setVisible(true);
                }
            });
        }
    
        public class TestPane extends JPanel {
    
            public TestPane() {
                setLayout(new GridBagLayout());
                JTextField field = new JTextField(10);
                ((AbstractDocument)field.getDocument()).setDocumentFilter(new IntegerDocumentFilter());
                
                GridBagConstraints gbc = new GridBagConstraints();
                gbc.gridwidth = GridBagConstraints.REMAINDER;
                add(field, gbc);
                
                InputMap im = field.getInputMap(WHEN_IN_FOCUSED_WINDOW);
                ActionMap am = field.getActionMap();
    
                im.put(KeyStroke.getKeyStroke(KeyEvent.VK_EQUALS, KeyEvent.SHIFT_DOWN_MASK), "Pressed.+");
                im.put(KeyStroke.getKeyStroke(KeyEvent.VK_PLUS, 0), "Pressed.+");
                im.put(KeyStroke.getKeyStroke(KeyEvent.VK_MINUS, 0), "Pressed.-");
    
                am.put("Pressed.+", new DeltaAction(field, 1));
                am.put("Pressed.-", new DeltaAction(field, -1));
                
                add(new JButton("Test"), gbc);
            }
    
            protected class DeltaAction extends AbstractAction {
    
                private JTextField field;
                private int delta;
    
                public DeltaAction(JTextField field, int delta) {
                    this.field = field;
                    this.delta = delta;
                }
    
                @Override
                public void actionPerformed(ActionEvent e) {
                    String text = field.getText();
                    if (text == null || text.isEmpty()) {
                        text = "0";
                    }
                    try {
                        int value = Integer.parseInt(text);
                        value += delta;
                        field.setText(Integer.toString(value));
                    } catch (NumberFormatException exp) {
                        System.err.println("Can not convert " + text + " to an int");
                    }
                }
            }
    
            public class IntegerDocumentFilter extends DocumentFilter {
    
                @Override
                public void insertString(DocumentFilter.FilterBypass fb, int offset, String text, AttributeSet attr) throws BadLocationException {
                    try {
                        StringBuilder sb = new StringBuilder();
                        Document document = fb.getDocument();
                        sb.append(document.getText(0, offset));
                        sb.append(text);
                        sb.append(document.getText(offset, document.getLength()));
                        
                        Integer.parseInt(sb.toString());
                        super.insertString(fb, offset, text, attr);
                    } catch (NumberFormatException exp) {
                        System.err.println("Can not insert " + text + " into document");
                    }
                }
    
                @Override
                public void replace(DocumentFilter.FilterBypass fb, int offset, int length, String string, AttributeSet attr) throws BadLocationException {
                    if (length > 0) {
                        fb.remove(offset, length);
                    }
                    insertString(fb, offset, string, attr);
                }
            }
    
        }
    
    }