Remapping the Keyboard at a low level - java

We are replacing a legacy C application originally written for MSDOS (yes, believe it or not!). This application uses a specially remapped keyboard which intercepts the DOS keyboard interrupt (remember that??!) to sometimes alter the scan codes of the keys pressed by the user so that different processing would occur. Special labels were then placed on the keys telling the users the "new" meaning of these keys.
The new Java version is required to preserve this keyboard layout which the targeted group of users is very familiar with.
An example of what we are trying to do is as follows:
You may never have thought about this, but the numeric keypad of a modern telephone is reversed from the numeric keypad of a computer keyboard. On the former 1-2-3 is on the top row and on the latter it is on the bottom row. We are required to make the keyboard's numeric keypad look like the telephone. Let's say, when the user types "7" on the numeric keypad, we want it look as though he typed a "1", when he types an "8", we want a "2", when he types a "3" we want a "9".
There is much more that we have to do to emulate the DOS application, but we can't even solve this simple case now. I have been all over Key Binding, KeyAdapters, KeyListeners, and even KeyEventDispatchers, and I cannot make this work. I am pretty sure we have to work on the lowest level we are allowed to work by Java to come as close as possible to what the legacy app does. And needless to say, we want the cleanest implementation possible, so that the application level code is not littered with inputMaps and actionMaps etc. As much as possible this needs to be handled globally. Can anyone help?

If I were doing this, I would write the Java App without worrying about the key bindings. Assume that when a component gets a keyevent for #7 its #7, don't worry about whether the 7 or 1 was really typed. The app shouldn't care about how keys are mapped on the keyboard. This should let you start developing the app immediately.
As far as overriding key bindings, this seems like where you want to look: http://download.oracle.com/javase/7/docs/api/java/awt/KeyEventDispatcher.html
It sounds like you can write your own KeyEventDispatcher to handle all the key mapping logic and it prevents mapping logic from messing up the rest of the logic in your application.

This is hacky, and I'll admit I haven't used it myself, but you could subclass KeyEvent and override the fields as needed. So yourSubclass.VK_NUMPAD1 is the integer value of KeyEvent.VK_NUMPAD7, yourSubclass.VK_NUMPAD2 is the integer value of KeyEvent.VK_NUMPAD8, etcetera. Then use your subclass everywhere KeyEvent is normally used.

I also agree that the KeyEventDispatcher should work. But if this a version/platform issue, then maybe you can use a custom Event Queue. See Global Event Dispatching

My stack overfloweth!
The answer was here:
http://download.oracle.com/javase/6/docs/api/java/awt/event/KeyEvent.html
which says:
For key pressed and key released
events, the getKeyCode method returns
the event's keyCode. For key typed
events, the getKeyCode method always
returns VK_UNDEFINED.
My original attempt thought it could get a keyCode on KEY_TYPED. It could not, and it was the KEY_TYPED event that was clobbering the mapping done in KEY_PRESSED.
Here is a working implementation:
import static java.awt.event.KeyEvent.*;
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Dimension;
import java.awt.KeyEventDispatcher;
import java.awt.KeyboardFocusManager;
import java.awt.event.KeyEvent;
import java.util.HashMap;
import java.util.Map;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingConstants;
public class KeyboardDispatcherDemo extends JFrame {
/**
* This class shows how to map numeric keypad keys.
* It performs two conversions:
* 1. lower-to-uppercase
* 2. if numeric keypad 7-9 is typed, 1-3 appears and vice versa.
*
* This is modified from the code at
* http://www.exampledepot.com/egs/java.awt/DispatchKey.html#comment-51807
* which demoes the lower-to-upper conversion.
*
* It doesn't yet handle modified numeric keypad keys.
*
*/
public KeyboardDispatcherDemo() {
KeyboardFocusManager.getCurrentKeyboardFocusManager().addKeyEventDispatcher(
new KeyEventDispatcher() {
private char lastMappedKey;
private final Map<Integer, Character> keyMap =
new HashMap<Integer, Character>() {
{
put(VK_NUMPAD1, '7');
put(VK_NUMPAD2, '8');
put(VK_NUMPAD3, '9');
put(VK_NUMPAD7, '1');
put(VK_NUMPAD8, '2');
put(VK_NUMPAD9, '3');
}};
public boolean dispatchKeyEvent(KeyEvent e) {
System.out.println(String.format("INPUT: %s", e.toString()));
boolean dispatch = false;
switch (e.getID()) {
case KeyEvent.KEY_PRESSED:
dispatch = dispatchKeyPressed(e);
break;
case KeyEvent.KEY_TYPED:
dispatch = dispatchKeyTyped(e);
break;
case KeyEvent.KEY_RELEASED:
dispatch = dispatchKeyReleased(e);
break;
default:
throw new IllegalArgumentException();
}
System.out.println(String.format("OUTPUT: %s", e.toString()));
System.out.println();
return dispatch;
}
private boolean dispatchKeyPressed(KeyEvent e) {
char k = e.getKeyChar();
if (k != CHAR_UNDEFINED) {
if (Character.isLetter(k)) {
e.setKeyChar(Character.toUpperCase(e.getKeyChar()));
} else if (e.getModifiers() == 0){
Character mapping = keyMap.get(e.getKeyCode());
if (mapping != null) {
e.setKeyChar(mapping);
}
}
// save the last mapping so that KEY_TYPED can use it.
// note we don't do this for modifier keys.
this.lastMappedKey = e.getKeyChar();
}
return false;
}
// KEY_TYPED events don't have keyCodes so we rely on the
// lastMappedKey that was saved on KeyPressed.
private boolean dispatchKeyTyped(KeyEvent e) {
char k = e.getKeyChar();
if (k != CHAR_UNDEFINED) {
e.setKeyChar(lastMappedKey);
}
return false;
}
private boolean dispatchKeyReleased(KeyEvent e) {
char k = e.getKeyChar();
if (k != CHAR_UNDEFINED) {
e.setKeyChar(lastMappedKey);
this.lastMappedKey=CHAR_UNDEFINED;
}
return false;
}
});
setTitle("KeyboardDispatcherDemo");
JPanel panel = new JPanel();
panel.setBackground(new Color(204, 153, 255));
panel.setLayout(new BorderLayout());
getContentPane().add(panel, BorderLayout.CENTER);
JTextArea staticText = new JTextArea();
staticText.setText("This demonstrates how to map numeric keypad keys. It uppercases all letters and converts Numeric Keypad 1-3 to 7-9 and vice versa. Try it.");
staticText.setLineWrap(true);
staticText.setWrapStyleWord(true);
panel.add(staticText, BorderLayout.NORTH);
staticText.setFocusable(false);
JTextField textField = new JTextField();
textField.setText("");
textField.setHorizontalAlignment(SwingConstants.LEFT);
panel.add(textField, BorderLayout.SOUTH);
textField.setColumns(10);
textField.setFocusable(true);
setSize(getPreferredSize());
setVisible(true);
}
/**
* #param args
*/
public static void main(String[] args) {
new KeyboardDispatcherDemo();
}
#Override
public Dimension getPreferredSize() {
// TODO Auto-generated method stub
return new Dimension(400,300);
}
}
Thanks to all who nudged me toward the answer.
which brings me to my next question ... stay tuned.

Related

Custom JTextField bug when trying to process minus key

I made a custom JTextField to input numeric values. The class looks as follows:
import javax.swing.*;
import java.awt.*;
import java.awt.event.KeyEvent;
public class JNumberField extends JTextField {
public JNumberField(int width, int height) {
this();
setPreferredSize(new Dimension(width, height));
}
public JNumberField() {
setFont(new Font("Monospace", Font.BOLD, 23));
setHorizontalAlignment(JTextField.HORIZONTAL);
}
#Override
public void processKeyEvent(KeyEvent ev) {
//Accepts digits, backspace, minus key and left/right arrow keys
if (Character.isDigit(ev.getKeyChar()) || ev.getKeyCode() == KeyEvent.VK_BACK_SPACE || ev.getKeyCode() == KeyEvent.VK_MINUS || ev.getKeyCode() == KeyEvent.VK_LEFT || ev.getKeyCode() == KeyEvent.VK_RIGHT) {
super.processKeyEvent(ev);
}
ev.consume();
}
public static void main(String[] args) {
JOptionPane.showInputDialog(new JNumberField(300,50));
}
}
All the keys are working except for the "-" key. I also printed the keycode that was pressed and it responded with 45 so the if statement is executed but it doesn't insert the - into the text field. Is this a bug or how can it be fixed? I also tried removing the if statement and just call the processKeyEvent method and then it works to insert the - sign...
First of all I totally agree with Andrew, there are better solutions to solving the problem. For example you can use:
a JSpinner
a JFormattedTextField
a DocumentFilter - see: How to automatically update small letter characters to capital letter in Java JEditorPane?
The reason the above solutions are better is because:
you don't need to extend a class
the solutions are more abstract
Your solution is a low level solution, which means you need to understands the events that are generated when you press a key and how those events are handled by Swing.
A more abstract solution is always better since you don't need to know the details. This is why the Swing API has evolved.
In your case you are not understanding the difference between a "displayable" key event and a "non displayable" event.
A text component is updated when the keyTyped event is generated which is only generated for "displayable" character like " numeric digits" and "-".
A "keyTyped" event is not generated for using the arrow keys or backspace keys.
So your logic needs to be changed to handle the "-" as a character (not a key code):
//if (Character.isDigit(ev.getKeyChar()) || ev.getKeyCode() == KeyEvent.VK_MINUS ...
if (Character.isDigit(ev.getKeyChar()) || ev.getKeyChar() == '-' ...

How to define the keyStroke for the numpad - enter key

I'm about to use keybinding in a swing application for the num pad enter key, but the key is difficult to catch.
All examples I have seen rely on something like
key == KeyEvent.VK_KP_LEFT
where VK_KP_LEFT is some predefined value. Other options are to define a keystoke like this:
KeyStroke.getKeyStroke("control A");
KeyStroke.getKeyStroke(KeyEvent.VK_A, InputEvent.CTRL_MASK);
but I have not found the "modifier" for numpad.
What is easily to obtain is the difference between the general and the numpad-enter:
All numpad-keys (indepently if switched in the numeric mode or not) get assigned the
getKeyLocation() == 4
(I spottetd this from key pressed / key released methods)
The question is:
How to properly prepare the keyStroke for numpad enter key to use it in the
inputMap.put(KeyStroke keyStroke, Object actionMapKey)
key binding method?
Thanks,
Tarik
If you're looking for binding Enter key you can use KeyEvent.VK_ENTER, ie:
getInputMap().put(KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "someAction");
getActionMap().put("someAction", someAction);
Here is a short example:
import java.awt.event.*;
import javax.swing.*;
public class Test {
public static void main(String[] args) throws Exception {
final JFrame frame = new JFrame();
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
JPanel panel = new JPanel();
panel.add(new JLabel("Hit Enter"));
Action someAction = new AbstractAction() {
public void actionPerformed(ActionEvent e) {
JOptionPane.showMessageDialog(frame, "Got it");
}
};
panel.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(
KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0), "someAction");
panel.getActionMap().put("someAction", someAction);
frame.add(panel);
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
}
}
EDIT: VK_ENTER on numeric pad vs main keyboard
After some testing, it looks like it may not be possible to bind these keys separately.
The same KeyStroke is generated for both keys. The implementation of JComponent.processKeyBinding does not examine the KeyEvent, all it cares is KeyStroke in order to find the desired action.
SwingUtilities.notifyAction that is responsible for dispatching the actual action does not delegate all the details of the KeyEvent (only key, modifiers and when). So inside action there is no way to distinguish either as there is no details in ActionEvent.
If it worth the trouble, you can override processKeyBinding and add some logic if needed. You can also use KeyboardFocusManageraddKeyEventDispatcher() for blocking one of the keys.
How about this?
if(keyEvent.getKeyLocation() == KeyEvent.KEY_LOCATION_NUMPAD
&& keyEvent.getKeyCode() == KeyEvent.VK_ENTER)

How to detect ctrl-f in my SWT application

I have written an SWT UI which has a primary function of displaying text in a StyledText control. I want to add a handler for Ctrl+F so that when that shortcut is pressed the focus is set to a search box. I have tried using the following code to detect the keypress.
sWindow = new Shell();
...
sWindow.getDisplay().addFilter(SWT.KeyDown, new Listener()
{
#Override
public void handleEvent(Event e)
{
System.out.println("Filter-ctrl: " + SWT.CTRL);
System.out.println("Filter-mask: " + e.stateMask);
System.out.println("Filter-char: " + e.character);
}
});
I was expecting that when I pressed Ctrl+f I would see the following output:
Filter-ctrl: 262144
Filter-mask: 262144
Filter-char: f
However, in practice I actually see the following.
Filter-ctrl: 262144
Filter-mask: 262144
Filter-char: <unprintable char - displayed as a box in eclipse console>
I have two questions:
Is Display.addFilter(...) the best way to add a global shortcut? I tried Display.addListener(...) but this didn't receive any events at all.
Why don't I get the pressed character when I'm holding down Ctrl? When I hold down alt or shift I get the expected mask and the pressed character.
Is Display.addFilter(...) the best way to add a glbal shortcut? I tried Display.addListener(...) but this didn't receive any events at all.
Yes, normally Display.addFilter(...) is the best way to add a glbal shortcut because they have higher preference over the event listeners. See the below comment from Display.addFilter(...) javadoc.
Because event filters run before other
listeners, event filters can both
block other listeners and set
arbitrary fields within an event. For
this reason, event filters are both
powerful and dangerous. They should
generally be avoided for performance,
debugging and code maintenance
reasons.
For your second question:
Why don't I get the pressed character when I'm holding down ctrl? When I hold down alt or shift I get the expected mask and the pressed character.
The problem is that you are looking at the wrong place. Instead of querying e.character you should be using e.keyCode. As per javadoc of e.character you won't get just character f:
Depending on the event, the character
represented by the key that was typed.
This is the final character that
results after all modifiers have been
applied. For example, when the user
types Ctrl+A, the character value is
0x01 (ASCII SOH).
So when you press CTRL+f then it converts in 0x06 (ASCII ACK). Which is not the case when you do ALT+f or SHIFT+f.
On the other hand the javadoc of e.keyCode says:
depending on the event, the key code
of the key that was typed, as defined
by the key code constants in class
SWT. When the character field of the
event is ambiguous, this field
contains the unaffected value of the
original character. For example,
typing Ctrl+M or Enter both result in
the character '\r' but the keyCode
field will also contain '\r' when
Enter was typed and 'm' when Ctrl+M
was typed.
Check the below code for more details. For demo I have tried to put listener on Display and Test.
import org.eclipse.swt.SWT;
import org.eclipse.swt.events.KeyEvent;
import org.eclipse.swt.events.KeyListener;
import org.eclipse.swt.graphics.Color;
import org.eclipse.swt.widgets.Display;
import org.eclipse.swt.widgets.Event;
import org.eclipse.swt.widgets.Listener;
import org.eclipse.swt.widgets.Shell;
public class ControlF
{
public static void main(String[] args)
{
Display display = new Display ();
final Shell shell = new Shell (display);
final Color green = display.getSystemColor (SWT.COLOR_GREEN);
final Color orig = shell.getBackground();
display.addFilter(SWT.KeyDown, new Listener() {
public void handleEvent(Event e) {
if(((e.stateMask & SWT.CTRL) == SWT.CTRL) && (e.keyCode == 'f'))
{
System.out.println("From Display I am the Key down !!" + e.keyCode);
}
}
});
shell.addKeyListener(new KeyListener() {
public void keyReleased(KeyEvent e) {
if(((e.stateMask & SWT.CTRL) == SWT.CTRL) && (e.keyCode == 'f'))
{
shell.setBackground(orig);
System.out.println("Key up !!");
}
}
public void keyPressed(KeyEvent e) {
if(((e.stateMask & SWT.CTRL) == SWT.CTRL) && (e.keyCode == 'f'))
{
shell.setBackground(green);
System.out.println("Key down !!");
}
}
});
shell.setSize (200, 200);
shell.open ();
while (!shell.isDisposed()) {
if (!display.readAndDispatch ()) display.sleep ();
}
display.dispose ();
}
}

Java Hangman Project: Action Listener

I am creating a hangman game. I made a button A - Z using the GUI Toolbars in Netbeans as follows:.
My problem is, how can I add an actionlistener to all of it. Is it possible to use a loop? If i click the button A, i will get the character 'a' and so on..
Yes it is possible to use a loop, but since your JButtons were created by using NetBeans code-generation, they won't be in an array or collection initially, and so this is something that you'll have to do: create an array of JButton and fill it with the buttons created by NetBeans. Then it's a trivial matter to create a for loop and in that loop add an ActionListener that uses the ActionEvent's actionCommand (as noted above) in its logic.
Having said this, I think that the better solution is to forgo use of the NetBean's GUI builder (Matisse) and instead to create your Swing code by hand. This will give you much greater control over your code and a much better understanding of it as well. For instance, if you do it this way, then in your for loop you can both create your buttons, add the listeners, and add the button to its container (JPanel).
e.g.,
import java.awt.*;
import java.awt.event.*;
import java.util.ArrayList;
import java.util.List;
import javax.swing.*;
public class Foo2 {
public static void main(String[] args) {
JPanel buttonContainer = new JPanel(new GridLayout(3, 9, 10, 10));
List<JButton> letterButtons = new ArrayList<JButton>(); // *** may not even be necessary
for (char buttonChar = 'A'; buttonChar <= 'Z'; buttonChar++) {
String buttonText = String.valueOf(buttonChar);
JButton letterButton = new JButton(buttonText);
letterButton.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent e) {
String actionCommand = e.getActionCommand();
System.out.println("actionCommand is: " + actionCommand);
// TODO fill in with your code
}
});
buttonContainer.add(letterButton);
letterButtons.add(letterButton);
}
JOptionPane.showMessageDialog(null, buttonContainer);
}
}
Well, with some pseudo code, wouldn't this make sence for you?
for(button in bord) {
button.addActionListener(my_actionlistener);
}
Then in your actionlistener you can see which button was pressed
public void actionPerformed(ActionEvent e) {
// button pressed
if ("string".equals(e.getActionCommand()) {
// do something
}
// and so forth
}
You'll need to add the buttons to a list of some kind so you can iterate through them, Netbeans doesn't do this for you when you generate the buttons.
After that, just run a for each loop on the list containing all the buttons. To get the values of the characters just cast the relevant ascii value, which starts at 97 for a lower case a or 65 for an upper case A:
int charNum = 97;
for(Button b : board) {
char charVal = (char)charNum;
charNum++;
//add the action listener
}

How to intercept keyboard strokes going to Java Swing JTextField?

The JTextField is a calculator display initialized to zero and it is bad form to display a decimal number with a leading 0 like 0123 or 00123. The numeric buttons (0..9) in a NetBeans Swing JFrame use append() [below] to drop the leading zeros, but the user may prefer the keyboard to a mouse, and non-numeric characters also need to be handled.
private void append(String s) {
if (newEntry) {
newEntry = false;
calcDisplay.setText(s);
} else if (0 != Float.parseFloat(calcDisplay.getText().toString())) {
calcDisplay.setText(calcDisplay.getText().toString() + s);
}
}
You could restrict the characters input to the JTextField by adding a custom KeyListener. Here is a quick example to demonstrate the idea:
myTextField.addKeyListener(new KeyAdapter() {
#Override
public void keyTyped(KeyEvent e) {
char c = e.getKeyChar();
if (!Character.isDigit(c)) {
e.consume(); // Stop the event from propagating.
}
}
});
Of course, you need to consider special keys like Delete and combinations like CTRL-C, so your KeyListener should be more sophisticated. There are probably even freely available utilities to do most of the grunt work for you.
You can do this with DocumentFilter.
(Edit: This answer originally included a ling to a simple complete example program on my now defunct weblog.)

Categories

Resources