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
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
modelKeyListener
doesn't catch the use-case when the user pastes text into the fieldKeyListener
doesn't catch the use-case when setText
is calledKeyListener
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);
}
}
}
}