1

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

2 Answers2

1

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);
            }
        }

    }

}
MadProgrammer
  • 343,457
  • 22
  • 230
  • 366
  • Thanks, this works just fine for the + and - keys. However, my bindings on the arrow keys do not work as intended. They move the cursor around in the text field when selected. However the action is not triggered, while I have the WHEN_IN_FOCUSED_WINDOW InputMap. Changing it to WHEN_FOCUSED does work and the action is executed. However, that is not what I need. The action is supposed to work as long as the window is focused. – Joris Kühl Dec 16 '18 at 10:58
  • Yeah, I suspected that in my edit to the question. Your answer solves my original problem, so I'll mark it as accepted. Still open to suggestions about the new problem though ^^ – Joris Kühl Dec 16 '18 at 19:04
  • [Replace the existing bindings](https://stackoverflow.com/questions/18652481/how-do-i-prevent-arrow-key-press-in-jtextfield-from-scrolling-jscrollpane/18652651#18652651); [Intercept/Inject into existing key binding](https://stackoverflow.com/questions/33071844/cant-figure-out-error-with-actionlistener/33091886#33091886) – MadProgrammer Dec 16 '18 at 19:14
-2

You should add two separate panels and add listener to the JPanel in which degreeField is added.

import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;

public class MyClass extends JFrame {

private JTextField degreeField = new JTextField("0");
private JTextField anotherField = new JTextField("0");
private JPanel panel1 = new JPanel();
private JPanel panel2 = new JPanel();

MyClass(){
    this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);

    panel1.add(degreeField);
    panel2.add(anotherField);
    this.getContentPane().add(panel1, BorderLayout.CENTER);
    this.getContentPane().add(panel2, BorderLayout.PAGE_END);

    panel1.addKeyListener(new KeyListener() {
        @Override
        public void keyTyped( KeyEvent e ) {
            int keyCode = e.getKeyCode();
            switch(keyCode){
                // other cases
                case KeyEvent.VK_PLUS:
                    degreeField.setText((Integer.parseInt(degreeField.getText()) + 1 )+ "");
                case KeyEvent.VK_MINUS:
                    degreeField.setText((Integer.parseInt(degreeField.getText()) - 1 ) + "");

                default:
                    //do nothing
            }
        }

        @Override
        public void keyPressed( KeyEvent e ) {
            //do nothing
        }

        @Override
        public void keyReleased( KeyEvent e ) {
            //do nothing
        }
    });
}

public static void main(String[] args){
    MyClass m = new MyClass();
    m.pack();
    m.setVisible(true);

}
}
Ashish G
  • 1
  • 2
  • 1
    The ease at which `KeyListener` based solutions are broken, make it widely inappropriate as a solution. Paste some text (including the +/- characters) into the text field? Call `setText` on the text field, including the +/- characters – MadProgrammer Dec 15 '18 at 21:17
  • 1
    Your example will also not work, because `panel1` is neither focusable, nor can it receive focus, so it will never trigger any key events, which is yet another limitation of `KeyListener` – MadProgrammer Dec 15 '18 at 21:21