Wednesday, July 6, 2005

Auto Complete Combo Box

I created an auto complete combo box. This combo box is based on
PComboBox from PSwing project (http://pswing.sourceforge.net/).
I resolved some threading issues and pop up problem. There is known
issue which sometime throws index out of bound exception.
If you modify this combo, please tell me, so I can post the updated
version.
Enjoy!

This is the code

package com.katalisindonesia.swingpack;


import javax.swing.ComboBoxModel;
import javax.swing.JComboBox;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.ListDataEvent;
import javax.swing.event.ListDataListener;
import java.awt.AWTEvent;
import java.awt.EventQueue;
import java.awt.event.KeyEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

/**
* Autocompleting JComboBox. Original code is from PComboBox class of
* PSwing project (
* http://pswing.sourceforge.net/
.
*
* @author Thomas Edwin Santosa
*/
public class JComboBoxExt
extends JComboBox {
// ------------------------------ FIELDS ------------------------------

private List itemIndex;
private Map listLocationIndex;
private final FullWordComboKeySelectionModel mgr = new FullWordComboKeySelectionModel();
private boolean allowNewEntries;
private BuildIndexListener listener = new BuildIndexListener();

// --------------------------- CONSTRUCTORS ---------------------------

/**
* Constructor for JComboBoxExt.
*/
public JComboBoxExt() {
buildIndex();
}

/**
* Must be called after all items have been added to combo box, or after a new item is added. This indexes all items
* String values for fast searching and matching.
* JComboBoxExt WILL NOT WORK IF YOU DO NOT CALL THIS METHOD.
*/
private void buildIndex() {
// simpan item yang lama jika ada
Object oldSelectedItem = getSelectedItem();

ComboBoxModel model = getModel();
itemIndex = Collections.synchronizedList(new ArrayList(model.getSize()));
listLocationIndex = Collections.synchronizedMap(new LinkedHashMap(model.getSize()));
for (int i = 0; i < model.getSize(); i++) {
Object o = model.getElementAt(i);
itemIndex.add(o);
listLocationIndex.put(o.toString().toUpperCase(), new Integer(i));
}
Collections.sort(itemIndex, new ToStringComparator());

// Kembalikan item yang lama
setSelectedItem(oldSelectedItem);
}

/**
* Constructor for JComboBoxExt.
*
* @param items
*/
public JComboBoxExt(Object[] items) {
super(items);
buildIndex();
}

/**
* Constructor for JComboBoxExt.
*
* @param aModel
*/
public JComboBoxExt(ComboBoxModel aModel) {
super(aModel);
aModel.addListDataListener(listener);
buildIndex();
}

// --------------------- GETTER / SETTER METHODS ---------------------

/**
* Returns the allowNewEntries.
*
* @return boolean
*/
public boolean isAllowNewEntries() {
return allowNewEntries;
}

/**
* Sets the allowNewEntries.
*
* @param allowNewEntries The allowNewEntries to set
*/
public void setAllowNewEntries(boolean allowNewEntries) {
this.allowNewEntries = allowNewEntries;
}

// -------------------------- OTHER METHODS --------------------------

public void addItem(Object anObject) {
super.addItem(anObject); //To change body of overridden methods use File | Settings | File Templates.
buildIndex();
}

public boolean containsDisplayString(String s) {
int i = search(s);
boolean contains = false;
if (i >= 0) {
contains = true;
}
return contains;
}

/**
* Returns the index of the first item in the list that matches the passed in string. A match is made if the first
* x letters of the item match the search string, where x is the length of the search
* string.
*/
public int search(String search) {
int listLocation = -1;

if (itemIndex.size() > 0) {
String s = search.toUpperCase();
//System.out.println("[C] search for: " + s);

int iBeginSearchRange = 0;
int iEndSearchRange = itemIndex.size() - 1;

//First, let's check index 0.
Object oZ = itemIndex.get(0);
int compZ = compareStrings(s, oZ.toString());
if (compZ == 0) {
//Index 0 matches.
listLocation = 0;
} else {
while (iEndSearchRange != iBeginSearchRange) {
int iRange = iEndSearchRange - iBeginSearchRange + 1;
int iCheckPoint = iBeginSearchRange + (iRange >> 1);
//System.out.println("[C] begin: " + iBeginSearchRange);
//System.out.println("[C] end: " + iEndSearchRange);
//System.out.println("[C] check: " + iCheckPoint);
Object o = itemIndex.get(iCheckPoint);
//System.out.println("[C] checking: " + o);
int comp = compareStrings(s, o.toString());
//System.out.println("[C] comp: " + comp);
if (comp == 0) {
//We've got a likely match.
listLocation = iCheckPoint;
//This will bump us out of the loop.
iEndSearchRange = iBeginSearchRange;
} else if (comp == 1) {
//search value must be after checkpoint.
iBeginSearchRange = iCheckPoint;
} else {
//search value is before checkpoint.
iEndSearchRange = iCheckPoint - 1;
}
}//end while
}//end else index 0 does not match.
if (listLocation != -1) {
listLocation = getBestMatchIndex(s, itemIndex.get(listLocation).toString(), listLocation);
}
}
return listLocation;
}

/**
* Returns -1, 0, or 1
*/
private static int compareStrings(String search, String item) {
//System.out.println("[C] item: " + item);
int comp;
String sBeginItem = item;
if (item.length() >= search.length()) {
sBeginItem = item.substring(0, search.length());
}
if (sBeginItem.equalsIgnoreCase(search)) {
comp = 0;
} else if (ToStringComparator.firstStringSortsBeforeSecond(search, sBeginItem)) {
comp = -1;
} else {
comp = 1;
}
return comp;
}

/**
* Double checks list for an exact match of search.
*/
private int getBestMatchIndex(String search, String item, int listLocation) {
int bestMatchIndex;
String searchItem = item.toUpperCase();

if (search.length() < searchItem.length() &&
listLocationIndex.containsKey(search)) {
//For now, just look for exact match of search.
Integer intLoc = (Integer) listLocationIndex.get(search);
bestMatchIndex = intLoc.intValue();
} else {
//listLocation is it.
Integer intLoc = (Integer) listLocationIndex.get(itemIndex.get(listLocation).toString().toUpperCase());
bestMatchIndex = intLoc.intValue();
}
return bestMatchIndex;
}

/**
* Returns the editor component of the JComboBoxExt.
*/
public JTextField getTextField() {
return (JTextField) getEditor().getEditorComponent();
}

protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
if (pressed && condition == WHEN_FOCUSED) {
requestFocusInWindow();
getTextField().setText(String.valueOf(e.getKeyChar()));
// System.out.println("mgr.caret="+mgr.getCaretPosition());
mgr.setCaretPosition(1);
// System.out.println("mgr.caret="+mgr.getCaretPosition());
// System.out.println("field.caret="+getTextField().getCaretPosition());
getTextField().setCaretPosition(1);
// System.out.println("field.caret="+getTextField().getCaretPosition());
}
return super.processKeyBinding(ks, e, condition, pressed);
}

/**
* This method does nothing. JComboBoxExt is always editable.
*/
public void setEditable(boolean editable) {
//Do nothing.
}

public void setModel(ComboBoxModel aModel) {
if (getModel() != null) {
getModel().removeListDataListener(listener);
}
super.setModel(aModel); //To change body of overridden methods use File | Settings | File Templates.
aModel.addListDataListener(listener);
buildIndex();
}

/**
* Selects the first item in the list where itemInList.equals(item). Returns the itemInList.
*/
public Object setSelectedItemByValue(Object item) {
//Brute force search for now.
Iterator itr = itemIndex.iterator();
Object foundItem = null;
while (itr.hasNext() && foundItem == null) {
Object o = itr.next();
if (o.equals(item)) {
foundItem = o;
}
}
getModel().setSelectedItem(foundItem);
return foundItem;
}

/**
* Selects the item with the specified display string. If the display string does not occur in the combo box, nothing
* happens.
*/
public void setSelectionByDisplay(String display) {
display = display.toUpperCase();
Integer index = (Integer) listLocationIndex.get(display);
if (index != null) {
int iIndex = index.intValue();
getModel().setSelectedItem(getModel().getElementAt(iIndex));
}
}

public void updateUI() {
super.updateUI();
//Update key selection.
initializeComponent();
}

/**
* Initializes the JComboBoxExt.
*/
protected void initializeComponent() {
enableEvents(AWTEvent.KEY_EVENT_MASK | AWTEvent.INPUT_METHOD_EVENT_MASK);
super.setEditable(true);
//getEditor();
setKeySelectionManager(mgr);
}

/**
* This method does nothing. JComboBoxExt always uses FullWordComboKeySelectionModel.
*/
public void setKeySelectionManager(JComboBox.KeySelectionManager manager) {
}

// -------------------------- INNER CLASSES --------------------------

private class BuildIndexListener
implements ListDataListener {
public void contentsChanged(ListDataEvent e) {
// buildIndex();
}

public void intervalAdded(ListDataEvent e) {
buildIndex();
}

public void intervalRemoved(ListDataEvent e) {
buildIndex();
}
}

public class FullWordComboKeySelectionModel
implements KeySelectionManager {
private JTextField field;
private volatile int caretPosition;


public FullWordComboKeySelectionModel() {
field = getTextField();
field.getDocument().addDocumentListener(new PComboDocumentListener());
}

/**
* Returns editor component of the JComboBoxExt.
*/
JTextField getField() {
return field;
}

/**
* Returns user's current caret position inside of editable component.
*/
int getCaretPosition() {
return caretPosition;
}

/**
* Can't get this to work consistently, so we are not using this method.
*/
public int selectionForKey(char key, ComboBoxModel model) {
return 0;
}

/**
* LIke selectionForKey, but we are calling this indirectly from PComboDocumentListener, passing in the caret
* position, and ignoring the key.
*/
public int selectionForKey(char key, ComboBoxModel model, int position) {
int selection;
String searchString = getCurrentSearchString(position);
//System.out.println("[FW] caret: " + caretPosition);
//System.out.println("[FW] searchString: " + searchString);
selection = search(searchString);
//System.out.println("[FW] selection is " + selection);
updateSearchStringField(getString(model, selection), model, searchString);
return selection;
}

public void updateSearchStringField(String s,
ComboBoxModel model,
String searchString) {
if (s != null) {
field.setText(s);
int iCaretPos = searchString.length();
field.setCaretPosition(iCaretPos);
setCaretPosition(iCaretPos);
} else if (!isAllowNewEntries()) {
int iCaretPos = Math.max(0, searchString.length() - 1);
field.setText(getSelectedString(model));
field.setCaretPosition(iCaretPos);
setCaretPosition(iCaretPos);
} else {
//New entries allowed, so let them keep typing.
int iCaretPos = searchString.length();
field.setCaretPosition(iCaretPos);
field.setText(searchString);
setCaretPosition(iCaretPos);
}
}

private String getCurrentSearchString(int position) {
String s = field.getText();
//System.out.println("[FW] field text: " + s);
//System.out.println("[FW] caret: " + field.getCaretPosition());
s = s.substring(0, position);
// s = s.substring(0, field.getCaretPosition());
//System.out.println("[FW] search string: " + s);
return s;
}

private String getString(ComboBoxModel model, int index) {
Object o = model.getElementAt(index);
String s = null;
if (o != null) {
s = o.toString();
}
return s;
}

private String getSelectedString(ComboBoxModel model) {
Object o = model.getSelectedItem();
return o != null ? o.toString() : "";
}

private void setCaretPosition(int caretPosition) {
this.caretPosition = caretPosition;
}
}

private class PComboDocumentListener
implements DocumentListener {
public void changedUpdate(DocumentEvent ev) {
int useOffset = ev.getOffset();
if (getTextField().getCaretPosition() > ev.getOffset())
useOffset = getTextField().getCaretPosition();
generalUpdate(ev, useOffset);
}

public void insertUpdate(DocumentEvent ev) {
int useOffset = ev.getOffset();
if (getTextField().getCaretPosition() > ev.getOffset())
useOffset = getTextField().getCaretPosition();
generalUpdate(ev, useOffset);
}

public void removeUpdate(DocumentEvent ev) {
int useOffset = ev.getOffset();
if (getTextField().getCaretPosition() < ev.getOffset())
useOffset = getTextField().getCaretPosition();
generalUpdate(ev, useOffset);
}

/**
* All event methods call this method.


*


* Fires asyncronous fireKeys if caret position in field is greater than 0, or if this is an insert and length of
* insert is 1 (this means the user has made a single keystroke).
*/
private void generalUpdate(DocumentEvent ev, int useOffset) {
int lengthOfChange = ev.getLength();
if (lengthOfChange == 1) {
if (useOffset > 0 ||
ev.getType() == DocumentEvent.EventType.INSERT && ev.getLength() == 1) {
int offset = ev.getOffset() + 1;
if (ev.getType() == DocumentEvent.EventType.REMOVE &&
ev.getOffset() > 0)
offset = ev.getOffset();

fireKeys(offset);
} else if (ev.getType() == DocumentEvent.EventType.REMOVE &&
ev.getDocument().getEndPosition().getOffset() > 1) {
fireClear();
}
}
}

private void fireClear() {
EventQueue.invokeLater(
new Runnable() {
public void run() {
getTextField().getDocument().removeDocumentListener(PComboDocumentListener.this);
try {
getTextField().setText("");
} finally {
getTextField().getDocument().addDocumentListener(PComboDocumentListener.this);
}
}
});
}

private void fireKeys(final int position) {
EventQueue.invokeLater(
new Runnable() {
public void run() {
getTextField().getDocument().removeDocumentListener(PComboDocumentListener.this);
try {
//System.out.println("[FK] firing selection for key: " + key);
int i;
i = mgr.selectionForKey(' ', getModel(), position);

//System.out.println("[DL] key is "+ i );
if (i >= 0) {
getModel().removeListDataListener(JComboBoxExt.this);

getModel().setSelectedItem(getModel().getElementAt(i));

getModel().addListDataListener(JComboBoxExt.this);

getTextField().setCaretPosition(mgr.getCaretPosition());
// System.out.println("caret thread="+mgr.getCaretPosition());

// Must be done after this event.
if (isShowing()) {
setPopupVisible(false);
setPopupVisible(true);
invalidate();
}
// System.out.println("hello");
} else {
//No match.
}
} finally {
getTextField().getDocument().addDocumentListener(PComboDocumentListener.this);
}
}
});
}
}

private static class ToStringComparator
implements Comparator {
/**
* Sorts in alphabetical order if this constructor is used.
*/
private ToStringComparator() {
}

/**
* @see java.util.Comparator#compare(Object, Object)
*/
public int compare(Object o1, Object o2) {
int sort;
String s1 = o1.toString();
String s2 = o2.toString();

if (firstStringSortsBeforeSecond(s1, s2)) {
sort = -1;
} else if (s1.equals(s2)) {
sort = 0;
} else {
sort = 1;
}
return sort;
}

public static boolean firstStringSortsBeforeSecond(String sOne, String sTwo) {
boolean bReturn;
try {
double dblOne = Double.parseDouble(sOne);
double dblTwo = Double.parseDouble(sTwo);
bReturn = dblOne <= dblTwo;
} catch (NumberFormatException ex) {
int iComp = sOne.compareToIgnoreCase(sTwo);
bReturn = iComp <= 0;
}

return bReturn;
}
}
}

No comments: