How to intercept input to JTextArea, JTextField, JTextPane, ...
Dear all -
How can one intercept the input coming from a keyboard before it is sent and displayed on some JTextComponent object (e.g., JTextPane)? For example, if you run the program below as is (built on Netbeans 5.5), anything you type will show on the JTextPane as expected; press the keys 'h', 'e', 'l', 'l', 'o', and you get "hello" on the JTextPane object. However, I need to intercept such typical behavior so that each key typed on the keyboard is mapped to whatever the application maps it to. For example, typing 'h', 'e', 'l', 'l', 'o' could result in the string "ifmmp", which is non other than mapping each character to the character next to it (Caesar cipher), or, typing 'h', 'e', 'l', 'l', 'o' may result in the string "HELLO" being displayed instead, where the mapping is simply to replace each character by it's uppercase equivalent.
I tried the obvious thing that may come to one's mind: add a key-press event handler to transform the keys I type before they are displayed on the jTextPanel, but by the time I get into that event handler, it's too late as the character typed are already shown therein. I could of course manipulate the data shown by executing methods such as getText(), replaceText(), setText() ..., but these are not elegant solutions as you may agree.
This is possible. Take a look at http://imtranslator.net/keyboard.asp for example. Change the pull-down menu value of the rendered Virtual Keyboard from English to even Arabic or Hebrew for example and see the immediate result! That's interception, right? How can I achieve a similar (and much simpler) functionality on JTextComponents? Pleeeeeeeeeeeease?
Thank for your time very much,
Mohsen
/*
* TextInterceptor.java
*
* Created on January 31, 2007, 10:22 PM
*/
publicclass TextInterceptorextends javax.swing.JFrame{
/** Creates new form TextInterceptor */
public TextInterceptor(){
initComponents();
}
privatevoid initComponents(){
jScrollPane1 =new javax.swing.JScrollPane();
jTextPane1 =new javax.swing.JTextPane();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
jTextPane1.addKeyListener(new java.awt.event.KeyAdapter(){
publicvoid keyPressed(java.awt.event.KeyEvent evt){
jTextPane1KeyPressed(evt);
}
});
jScrollPane1.setViewportView(jTextPane1);
javax.swing.GroupLayout layout =new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 216, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jScrollPane1, javax.swing.GroupLayout.PREFERRED_SIZE, 66, javax.swing.GroupLayout.PREFERRED_SIZE)
.addContainerGap(javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE))
);
pack();
}
privatevoid jTextPane1KeyPressed(java.awt.event.KeyEvent evt){
// Apparently, by the time I get here, it is too late to do
// any changes to the key I typed, as it is already shown on
// the jTextPanel.
}
/**
* @param args the command line arguments
*/
publicstaticvoid main(String args[]){
java.awt.EventQueue.invokeLater(new Runnable(){
publicvoid run(){
new TextInterceptor().setVisible(true);
}
});
}
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JTextPane jTextPane1;
}
# 1
> I tried the obvious thing that may come to one's mind:
Have you read the API? The API for the text components contains a link to the Swing tutorial on "Text Component Features" which has a section on implementing a DocumentFilter.
> That's interception, right? How can I achieve a similar (and much simpler) functionality on JTextComponents?
Don't really know what interception is. Using a DocumentFilter allows you to control the text that gets inserted into the Document. So for example its easy to convert all text from lower case to upper case as its typed.
I"m not sure what converting from Western to Arabic would entail.
# 2
> but by the time I get into that event handler, it's too late as the character typed
> are already shown therein.
not really.
for a (very) simple app, something like this might be all you need.
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
class Testing
{
JTextArea ta = new JTextArea(10,20);
int key;
public void buildGUI()
{
JFrame f = new JFrame();
f.getContentPane().add(new JScrollPane(ta));
f.pack();
f.setLocationRelativeTo(null);
f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
f.setVisible(true);
ta.addKeyListener(new KeyAdapter(){
public void keyPressed(KeyEvent ke){
key = ke.getKeyCode();
}
public void keyTyped(KeyEvent ke){
ke.consume();
ta.append(String.valueOf((char)(key+1)));//needs error handling for +1
//also needs conversion upper-->lower/whatever
}
});
}
public static void main(String[] args)
{
SwingUtilities.invokeLater(new Runnable(){
public void run(){
new Testing().buildGUI();
}
});
}
}
# 3
> for a (very) simple app, something like this might be all you need.But it won't support "pasted" text (if that is a requirement).
# 4
> > but by the time I get into that event handler, it's
> too late as the character typed
> > are already shown therein.
>
> not really.
>
> for a (very) simple app, something like this might be
> all you need.
>
public void keyPressed(KeyEvent ke){
key = ke.getKeyCode();
}
public void keyTyped(KeyEvent ke){
ke.consume();
ta.append(String.valueOf((char)(key+1)));
}
Now, that's elegant! Thanks very much Michael. Even more, consider this:
private void jTextArea1KeyTyped(java.awt.event.KeyEvent evt) {
evt.setKeyChar(keyTyped.charAt(0));
}
private void jTextArea1KeyPressed(java.awt.event.KeyEvent evt) {
keyTyped = "" + evt.getKeyChar();
keyTyped = keyTyped.toUpperCase();
}
Here, the evt input parameter is fooled into thinking the key is something different. My shortcoming was thinking that I could do all this in the keyPress event handler where in fact it is the keyType event handler that intercepts the process.
As per the comment that this approach won't allow "pasting", I think the same approach can take place using mouse event handlers, where the pasting is intercepted and manipulated before it is displayed.
# 5
> As per the comment that this approach won't allow
> "pasting", I think the same approach can take place
> using mouse event handlers, where the pasting is
> intercepted and manipulated before it is displayed.
I think you are wrong. Feel free to prove me wrong, though.
Consider the scenario where the key event is Ctrl-V, which under windows
is paste. And let's say the clipboard has 'this is some text' in it. What will
happen with your event handler?
As camickr rightly pointed out, the one place you are guaranteed to catch
all possibilities is DocumentFilter.
# 6
I wouldn't really describe the code as elegant :-)
it's purpose was mainly to show how to consume the typed key.
if considering a similar approach for pasting you would have to override paste()
JTextArea ta = new JTextArea(10,20){
public void paste(){
//modify pasted text here
}
};
but all you would end up doing is duplicating the code of a DocumentFilter
# 7
> > As per the comment that this approach won't allow
> > "pasting", I think the same approach can take
> place
> > using mouse event handlers, where the pasting is
> > intercepted and manipulated before it is
> displayed.
>
> I think you are wrong. Feel free to prove me
> wrong, though.
>
> Consider the scenario where the key event is Ctrl-V,
> which under windows
> is paste. And let's say the clipboard has 'this is
> some text' in it. What will
> happen with your event handler?
>
> As camickr rightly pointed out, the one place you are
> guaranteed to catch
> all possibilities is DocumentFilter.
Done Jay! And thanks for the motivation ("... prove me wrong ..."); I was heading in the direction of implementing that feature any way. The answer is retrieved right from your response: clipboard! The class Clipboard provides a handle on the system clipboard and Java 1.5+ made things even tastier for getting all sorts of flavors (one of which is your good old string flavor ... yummy). So my approach is to go on uppercasing (as a sample, simple application) any key that gets typed. Once the ctrl-v key combination is encountered, I retrieve the system clipboard contents and uppercase them too before appending them to the jTextArea object. Simply focus on the three methods jTextArea1KeyTyped(), jTextArea1KeyPressed(), and paste() in the code included below.
Elegant or not? Well, one can never be too "simple" if the required result is achieved. True, it is a very simple solution, but since I am not particularly interested in any transformed character once transformed, then what's better than doing it right as the key is introduced to the application? For my application, you put me right on the right track.
DocumentFilter or not? I haven't familiarized myself with it yet, but I am thinking it may not help me so much. In the application I am building, the keys that are mapped to are not only a function of the keys pressed, but of the time interval elapsed between two key presses as well. So, while pressing 'a' may result in one mapping, pressing 'a''a' within 300 ms could result in a different mapping, and pressing 'a''a''a' may lead to yet a different one! Now, that's too much for the DocumentFilter class to handle, isn't it?
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.UnsupportedFlavorException;
import java.awt.event.KeyEvent;
import java.io.IOException;
public class TextInterceptor extends javax.swing.JFrame {
/** Creates new form TextInterceptor */
public TextInterceptor() {
initComponents();
}
private void initComponents() {
jScrollPane1 = new javax.swing.JScrollPane();
jTextArea1 = new javax.swing.JTextArea();
setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
jTextArea1.setColumns(20);
jTextArea1.setRows(5);
jTextArea1.addKeyListener(new java.awt.event.KeyAdapter() {
public void keyPressed(java.awt.event.KeyEvent evt) {
jTextArea1KeyPressed(evt);
}
public void keyTyped(java.awt.event.KeyEvent evt) {
jTextArea1KeyTyped(evt);
}
});
jScrollPane1.setViewportView(jTextArea1);
javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
getContentPane().setLayout(layout);
layout.setHorizontalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 288, Short.MAX_VALUE)
.addContainerGap())
);
layout.setVerticalGroup(
layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
.addGroup(layout.createSequentialGroup()
.addContainerGap()
.addComponent(jScrollPane1, javax.swing.GroupLayout.DEFAULT_SIZE, 206, Short.MAX_VALUE)
.addContainerGap())
);
pack();
}
private void jTextArea1KeyTyped(java.awt.event.KeyEvent evt) {
evt.setKeyChar(keyTyped.charAt(0));
}
private void jTextArea1KeyPressed(java.awt.event.KeyEvent evt) {
int keyCode = evt.getKeyCode();
if (keyCode == KeyEvent.VK_V && evt.isControlDown()) {
evt.consume();
paste();
} else {
keyTyped = "" + evt.getKeyChar();
keyTyped = keyTyped.toUpperCase();
}
}
private void paste() {
Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
DataFlavor flavor = DataFlavor.stringFlavor;
if (clipboard.isDataFlavorAvailable(flavor)) {
try {
String clippedText = (String) clipboard.getData(flavor);
clippedText = clippedText.toUpperCase();
jTextArea1.append(clippedText);
} catch (UnsupportedFlavorException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
}
}
}
/**
* @param args the command line arguments
*/
public static void main(String args[]) {
java.awt.EventQueue.invokeLater(new Runnable() {
public void run() {
new TextInterceptor().setVisible(true);
}
});
}
// Variables declaration - do not modify
private javax.swing.JScrollPane jScrollPane1;
private javax.swing.JTextArea jTextArea1;
// End of variables declaration
String keyTyped;
}
# 8
> DocumentFilter or not? I haven't familiarized myself with it yet,
Then how can you make a valid design decision?
Its a couple of lines of code to handle "typed" or "pasted" text and the code is in one place. How much simpler can it be?
What if you decide to add cut, copy, paste menu items or toolbar buttons for additional functionality? Your approach won't work.
# 9
> > DocumentFilter or not? I haven't familiarized
> myself with it yet,
>
> Then how can you make a valid design decision?
Thanks. I have taken a deeper look now.
>
> Its a couple of lines of code to handle "typed" or
> "pasted" text and the code is in one place. How much
> simpler can it be?
It is very simple. Here is page showing how uppercasing any input is done: http://www.java2s.com/Code/Java/Swing-JFC/DocumentFilterthatmapslowercaseletterstouppercase.htm. But how about the scenario problem I provided in the previous reply where the mapping is not one-to-one? DocumentFilter is not a silver-bullet.
>
> What if you decide to add cut, copy, paste menu items
> or toolbar buttons for additional functionality? Your
> approach won't work.
The approach worked if you haven't already noticed! As far as the destination text-area is concerned, it doesn't matter whether the text brought in through the clipboard is "cut" or "copied", the same "paste" method is used for both. Actually, accessing the clipboard allows you to go beyond fetching just text.
As for providing such operations through a menubar menu items, well ... what's the magic? It doesn't matter whether the action is generated through a ctrl-v, "paste" push-button or some mouse button now is it?
# 10
> But how about the scenario problem I provided in the previous reply where the mapping is not one-to-one?
Just as easily handled in the Document filter as it would be handled in your other code. The code is all still in one place.
> It doesn't matter whether the action is generated through a ctrl-v, "paste" push-button or some mouse button now is it?
Of course it does matter when you use your approach. As I stated your current code can't handle it. I'm not saying you can't change your code to handle it. However it will be extra code written to specifically handle the situation.
Using a DocumentFilter it will be handled automatically without writing any extra code.
> Actually, accessing the clipboard allows you to go beyond fetching just text.
If you change the requirement then you change the solution. The original requirement was about intercepting text as it is entered. I showed you the most applicable solution.
Now you will probably say that using menus and toolbar buttons was not part of the origianl requirement. Agreed, but most editors have this built in so I was just trying to anticipate further problems you would have.
Since you apparently don't, or aren't willing, to understand the concept, I'll just let you play around on your own to find the problems.
# 11
So, while pressing 'a' may result in one mapping, pressing 'a''a' within 300 ms could result in a different mapping, and pressing 'a''a''a' may lead to yet a different one! Now, that's too much for the DocumentFilter class to handle, isn't it?
All you have to do is extend DocumentFilter and maintain a record of the last time a change was made, surely?
# 12
> ..., I'll just let you play around
> on your own to find the problems.
Oh no, please don't let me play all by myself now! I was just getting started!
I chased you a little on the forum to see what you are made of! In the beginning, I didn't know why I had the number 6 and you had the number 20,649 before your name. Realizing what those numbers stood for, I now feel scared, shivering, threatened and just anticipating a sense of "calm before the storm" if I carry on with my approach and not obediently listen to you :-)
I will definitely learn all I can about DocumentFilter, especially that all of Michael_Dunn, JayDS, itchyscratchy ... seem to agree that DocumentFilter may be extended to handle my problem. Hey, hey hey hey, are you guys all working in the same floor and decided that any thing I do will not be accepted until I succumb?
Thank you all.
# 13
> All you have to do is extend DocumentFilter and
> maintain a record of the last time a change was made,
> surely?
Yes sir! Works like magic. And yes, just a couple of lines of code from the outside and all editing operations work as expected. What was I thinking huh? Thanks again.