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;
}
}
}

Auto Select Support

I made auto select support for swing components. At this time, only
JTextField auto select works correctly. The spinner one does not behave
as expected. Because the code is small, I pasted it below.

package com.katalisindonesia.swingpack.support;

import javax.swing.JTextField;
import javax.swing.JSpinner;
import javax.swing.text.JTextComponent;
import java.awt.EventQueue;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;

/*
* User: thomas
* Date: Jul 1, 2005
* Time: 2:07:30 PM
*/

/**
* Add auto select feature in component when focused.
*
* @author thomas
*/
public class AutoSelect {
private boolean enabled = true;

public void install(final JTextField textField) {
textField.addFocusListener(new TextComponentFocusListener(textField));
}

public void install (final JSpinner spinner) {
if (spinner.getEditor() instanceof JSpinner.DefaultEditor) {
spinner.addFocusListener(new TextComponentFocusListener(((JSpinner.DefaultEditor)spinner.getEditor()).getTextField()));
}
}

public boolean isEnabled() {
return enabled;
}

public void setEnabled(boolean enabled) {
this.enabled = enabled;
}

class TextComponentFocusListener
implements FocusListener {
private JTextComponent textComponent;

TextComponentFocusListener(JTextComponent textComponent) {
this.textComponent = textComponent;
}

public void focusGained(FocusEvent e) {

if (enabled) {
EventQueue.invokeLater(
new Runnable() {
public void run() {
textComponent.selectAll();
}
});
}
}

public void focusLost(FocusEvent e) {
if (enabled) {
EventQueue.invokeLater(
new Runnable() {
public void run() {
textComponent.select(0, 0);
}
});
}
}
}
}


Use is simple. First, instantiate the auto select.

AutoSelect autoSelect = new AutoSelect();

Then, install to any component you want.

autoSelect.install(textField0);
autoSelect.install(textField1);
...

You may enable or disable it by using setEnabled().
I do this instead of subclassing the JTextField because I think that it
would scale better than the latter.
Please comment on it. Thanks!

Incomplete

My profile is incomplete. At least, just for a while ... I promise ;)

Email Client Preferred

At last, I prefer email client. I personally choose Evolution. Why I do not use Drivel or BloGTK? Both of them cannot support title correctly. Also, there is no blog draft queue. Evolution permits me queueing my blogs.

Tuesday, July 5, 2005

Email Client

I send this blog using email client.

I'm currently evaluating several blog clients. Those are BloGTK and drivel.
Which one do you use most?
I finally figured out how to do that. Note that these steps bellow might not be accurate as I recall them out of my mind.

  1. Make sure you have attached both your network cards. :)

  2. Create network connections using Configure Your Computeer.
    Add both network cards. Just make sure they use different subnets.

  3. Edit /etc/ifplugd/ifplugd.conf.
    Change the line

    INTERFACES="eth0"

    to

    INTERFACES="auto"



The last item is the most frustating thing because without it the client, especially if you use crossed UTP, will sometime behave strangely.
Enough for now. Next time perhaps I will show my dhcpd.conf.
Hello, world :)
My blog is alive!