Then, is it easy for Swing’s textbox to realize this? Examples are also available on the Net, but either too simple-functioned or too wordy-coded. In addition, some commercial Swing components are not free-charged. This article involves the application of the free ERP software development in 2BizBox and manages to achieve the effects with a very easy and effective way.
At first, let’s see it closely: it is a textbox, not a ComboBox, from appearance and nature. Therefore, we would not like to make it in to a ComboBox, that is, we don’t feel like inheriting from JComboBox. Next, ComboBox is dynamic and completely nonsynchronous with the input field because it is just a suggestion, unable to interfere with the texts input. Finally, the appearance and the act of the ComboBox is undoubtedly that of the ComboBox of JComboBox. So, “the JTextField which is able to complete automatically” should be a combination of the ComboBox of both JTextField and JComboBox.
After the analysis above, we can make sure that it is actually a JTextField, but also combined with a JComboBox’s ComboBox. Then in this case it on earth inherits from which one? From JTextField?
In fact, inheriting from others is not the best way. Why? It means that only by inheriting from your class can others use this function. If there are already more than 10 thousand interfaces written in your project and you would like to add the intelligent suggesting functions into some of the textboxes, will you change all of these codes in order to let them inherit your class? There is no doubt that that is an awful idea. For this reason, it’s not good for those people who just learned OO to inherit at every move. Is it not a better idea that we just offer a method Util to make a little change on the already existing JTextField instances, thus having the intelligent suggesting functions?
In order to combine the two components JTextField and JComboBox, here we use a very “special”method which is definitely out of your imagination, that is, put a JComboBox into JTextField and make that invisible. Let’s see the code:
JTextField txtInput = new JTextField();
JComboBox cbInput = new JComboBox();
txtInput.setLayout(new BorderLayout());
txtInput.add(cbInput, BorderLayout.SOUTH);
What? Set JTextField as a layout and add a JComboBox to put it in south? I believe that you have never heard of that. It is so peculiar. But it looks perfect once you set the height of JComboBox as 0.
JComboBox cbInput = new JComboBox(model) {
public Dimension getPreferredSize() {
return new Dimension(super.getPreferredSize().width, 0);
}
};
Although you can’t see Combo, it does exist in the textbox and is at the south of it. Our idea is: when texts are typed into the textbox, it will judge whether there are something conform to the requirements in the ComboBox. If there is, then the ComboBox will pop out immediately or it will disappear.
It is not difficult to keep an eye on the textbox: add listener to its document. Here we have used the rules as “case-insensitive” and “the items having the same opening as the string input”to filter. All the alternative strings are put in a single array, and those which conform to the conditions will be filtered out dynamically. Then they will be added into JComboBox and pop up the ComboBox.
public void insertUpdate(DocumentEvent e) {
updateList();
}
public void removeUpdate(DocumentEvent e) {
updateList();
}
public void changedUpdate(DocumentEvent e) {
updateList();
}
private void updateList() {
setAdjusting(cbInput, true);
model.removeAllElements();
String input = txtInput.getText();
if (!input.isEmpty()) {
for (String item : items) {
if (item.toLowerCase().startsWith(input.toLowerCase())) {
model.addElement(item);
}
}
}
cbInput.setPopupVisible(model.getSize() > 0);
setAdjusting(cbInput, false);
}
});
In addition, for much more convenience, we have added several shortcut keys: when ESC is input, the ComboBox will be closed automatically; when inputting a carriage return or a space, the first string which meets the requirement will be input in the textbox directly:
@Override
public void keyPressed(KeyEvent e) {
setAdjusting(cbInput, true);
if (e.getKeyCode() == KeyEvent.VK_SPACE) {
if (cbInput.isPopupVisible()) {
e.setKeyCode(KeyEvent.VK_ENTER);
}
}
if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
e.setSource(cbInput);
cbInput.dispatchEvent(e);
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
txtInput.setText(cbInput.getSelectedItem().toString());
cbInput.setPopupVisible(false);
}
}
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
cbInput.setPopupVisible(false);
}
setAdjusting(cbInput, false);
}
});
There is another important method needed to be explained: when popping up the popup list, we want that the arrow on keyboard can select the items in the list both upward and downward and that at the same time the current cursor and the focus still stay in the textbox. This seems to be very difficult. Please look at how we make it: when we keep an eye on the textbox and find out that the items in the list are to be input by the up and down arrows, we change source of the current keyboard events to JComboBox and then distribute it to JComboBox. That is to say, originally, the source of the event is the textbox, but now we have intercepted the postman, changed the receiver and then continued to give it back to him. In this way, we have grafted one twig on another.
if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
e.setSource(cbInput);
cbInput.dispatchEvent(e);
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
txtInput.setText(cbInput.getSelectedItem().toString());
cbInput.setPopupVisible(false);
}
}
At last, to see the effects, we need to put some data in the ComboBox. Then what to put? We choose “all countries” in Java to avoid troubleness.
Locale[] locales = Locale.getAvailableLocales();
for (int i = 0; i < locales.length; i++) {
String item = locales[i].getDisplayName();
items.add(item);
}
Now let’s see the effect. Good. It well lives up to our expectations.
The complete code is listed as follow:
import java.awt.*;
import java.awt.event.*;
import java.util.*;
import javax.swing.*;
import javax.swing.event.*;
import twaver.*;
public class Test {
public static void main(String[] args) throws Exception {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
JFrame frame = new JFrame();
frame.setTitle("Auto Completion Test");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setBounds(200, 200, 500, 400);
ArrayList<String> items = new ArrayList<String>();
Locale[] locales = Locale.getAvailableLocales();
for (int i = 0; i < locales.length; i++) {
String item = locales[i].getDisplayName();
items.add(item);
}
JTextField txtInput = new JTextField();
setupAutoComplete(txtInput, items);
txtInput.setColumns(30);
frame.getContentPane().setLayout(new FlowLayout());
frame.getContentPane().add(txtInput, BorderLayout.NORTH);
frame.setVisible(true);
}
private static boolean isAdjusting(JComboBox cbInput) {
if (cbInput.getClientProperty("is_adjusting") instanceof Boolean) {
return (Boolean) cbInput.getClientProperty("is_adjusting");
}
return false;
}
private static void setAdjusting(JComboBox cbInput, boolean adjusting) {
cbInput.putClientProperty("is_adjusting", adjusting);
}
public static void setupAutoComplete(final JTextField txtInput, final ArrayList<String> items) {
final DefaultComboBoxModel model = new DefaultComboBoxModel();
final JComboBox cbInput = new JComboBox(model) {
public Dimension getPreferredSize() {
return new Dimension(super.getPreferredSize().width, 0);
}
};
setAdjusting(cbInput, false);
for (String item : items) {
model.addElement(item);
}
cbInput.setSelectedItem(null);
cbInput.addActionListener(new ActionListener() {
@Override
public void actionPerformed(ActionEvent e) {
if (!isAdjusting(cbInput)) {
if (cbInput.getSelectedItem() != null) {
txtInput.setText(cbInput.getSelectedItem().toString());
}
}
}
});
txtInput.addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
setAdjusting(cbInput, true);
if (e.getKeyCode() == KeyEvent.VK_SPACE) {
if (cbInput.isPopupVisible()) {
e.setKeyCode(KeyEvent.VK_ENTER);
}
}
if (e.getKeyCode() == KeyEvent.VK_ENTER || e.getKeyCode() == KeyEvent.VK_UP || e.getKeyCode() == KeyEvent.VK_DOWN) {
e.setSource(cbInput);
cbInput.dispatchEvent(e);
if (e.getKeyCode() == KeyEvent.VK_ENTER) {
txtInput.setText(cbInput.getSelectedItem().toString());
cbInput.setPopupVisible(false);
}
}
if (e.getKeyCode() == KeyEvent.VK_ESCAPE) {
cbInput.setPopupVisible(false);
}
setAdjusting(cbInput, false);
}
});
txtInput.getDocument().addDocumentListener(new DocumentListener() {
public void insertUpdate(DocumentEvent e) {
updateList();
}
public void removeUpdate(DocumentEvent e) {
updateList();
}
public void changedUpdate(DocumentEvent e) {
updateList();
}
private void updateList() {
setAdjusting(cbInput, true);
model.removeAllElements();
String input = txtInput.getText();
if (!input.isEmpty()) {
for (String item : items) {
if (item.toLowerCase().startsWith(input.toLowerCase())) {
model.addElement(item);
}
}
}
cbInput.setPopupVisible(model.getSize() > 0);
setAdjusting(cbInput, false);
}
});
txtInput.setLayout(new BorderLayout());
txtInput.add(cbInput, BorderLayout.SOUTH);
}
}