Saturday, June 7, 2008

Lightweight Text Input in LWUIT (TextField) & AutoComplete

TextField has landed! LWUIT now features a real lightweight text field component that can accept arbitrary input and can be configured for additional languages/features.
This is the first version so bugs are to be expected but the basics seem to be working and the ability to "jump" into the native text box allow us to support everything including T9.

What is the use case for TextField and why should you care?

First: for username/password sort of input fields this is far more intuitive than the alternative of jumping to the native text box...

Second: You can listen to change events and do "cool stuff"!

What sort of stuff?

Hows about an address book where typing in the name of the recipient narrows down the list as you type?

As you can see in the pictures bellow typing c followed by k (its really typing 2 three times and 5 twice which caused the list to "jump" a bit) narrowed down the list to the only two names with "ck" in their names (case insensitive). All of that while the fish keeps swimming ;-)


This functionality requires a few very cool LWUIT tricks including both text field manipulation and a proxy filtering model. To narrow down the list as seen in the pictures we install a special list model that can reduce the element count on the fly, this is one way of pulling this trick off without too much effort!

This is the code for the text field and as you can see its trivial:
Form test = new Form("Address Book");
test.setLayout(new BorderLayout());
final TextField field = new TextField();
final ListModel underlyingModel = new DefaultListModel(new String[] {"Jack", "Kate", "Sawyer",
"Sayid", "Hurley", "Jin", "Sun", "Charlie", "Claire", "Aaron",
"Michael", "Walt", "Boone", "Shannon", "Locke", "Mr. Eko",
"Ana-Lucia", "Libby", "Desmond", "Benjamin Linus", "Juliet Burke" });

// this is a list model that can narrow down the underlying model
final FilterProxyListModel proxyModel = new FilterProxyListModel(underlyingModel);

List people = new List(proxyModel);
test.addComponent(BorderLayout.NORTH, field);
test.addComponent(BorderLayout.CENTER, people);
field.addDataChangeListener(new DataChangedListener() {
public void dataChanged(int type, int index) {
proxyModel.filter(field.getText());
}
});

test.show();
The filter proxy essentially implements the proxy pattern (or decorator if you are the type whose finicky about patterns) and exposes the list items that fall into the defined constraint, notice that it delegates all the heavy lifting to the underlying model.
This code really highlights the importance and power of MVC in the long run... For a small application MVC might cost in size, but once we start building large applications the ability to reuse programming concepts actually reduces code size and development time making MVC really appropriate for embedded devices!
class FilterProxyListModel implements ListModel, DataChangedListener {
private ListModel underlying;
private Vector filter;
private Vector listeners = new Vector();
public FilterProxyListModel(ListModel underlying) {
this.underlying = underlying;
underlying.addDataChangedListener(this);
}

private int getFilterOffset(int index) {
if(filter == null) {
return index;
}
if(filter.size() > index) {
return ((Integer)filter.elementAt(index)).intValue();
}
return -1;
}

private int getUnderlyingOffset(int index) {
if(filter == null) {
return index;
}
return filter.indexOf(new Integer(index));
}

public void filter(String str) {
filter = new Vector();
str = str.toUpperCase();
for(int iter = 0 ; iter < underlying.getSize() ; iter++) {
String element = (String)underlying.getItemAt(iter);
if(element.toUpperCase().indexOf(str) > -1) {
filter.addElement(new Integer(iter));
}
}
dataChanged(DataChangedListener.CHANGED, -1);
}

public Object getItemAt(int index) {
return underlying.getItemAt(getFilterOffset(index));
}

public int getSize() {
if(filter == null) {
return underlying.getSize();
}
return filter.size();
}

public int getSelectedIndex() {
return Math.max(0, getUnderlyingOffset(underlying.getSelectedIndex()));
}

public void setSelectedIndex(int index) {
underlying.setSelectedIndex(getFilterOffset(index));
}

public void addDataChangedListener(DataChangedListener l) {
listeners.addElement(l);
}

public void removeDataChangedListener(DataChangedListener l) {
listeners.removeElement(l);
}

public void addSelectionListener(SelectionListener l) {
underlying.addSelectionListener(l);
}

public void removeSelectionListener(SelectionListener l) {
underlying.removeSelectionListener(l);
}

public void addItem(Object item) {
underlying.addItem(item);
}

public void removeItem(int index) {
underlying.removeItem(index);
}

public void dataChanged(int type, int index) {
if(index > -1) {
index = getUnderlyingOffset(index);
if(index < 0) {
return;
}
}
for(int iter = 0 ; iter < listeners.size() ; iter++) {

((DataChangedListener)listeners.elementAt(iter)).dataChanged(type, index);
}
}
}

43 comments:

  1. Hi,Shai:
    Could you tell me how to realize handwriting input? Can LWUIT do it?

    ReplyDelete
  2. Hi Shai, I think there is an error in your code in the line:

    List people = new List(proxyModel);

    It says that it can't find the constructor.

    ReplyDelete
  3. @Eric, handwriting is recognized by the native text box component.

    @linuca, there is no error. Your filter needs to implement the list model interface as is shown in the code bellow.

    ReplyDelete
  4. Sorry Shai, I just realized that I was using javax.microedition.lcdui.List instead of com.sun.lwuit.List. Perhaps you should add the imports for newbies like me :P.

    ReplyDelete
  5. This was discussed in the forum. The static variables stand for the defaults only you can replace anything using the setters for input mode and their order. This should all be specified in the JavaDoc.

    ReplyDelete
  6. Hi shai

    how are u?
    this s working fine with single text field search.
    but i faced one issue.
    that issue is focus is not working properly when im using two text field with this searching technique in a single form. list showing properly. but focus is not focused on that text fields.
    im using setFocus(true) method.

    ReplyDelete
  7. @Germán I don't understand the question.

    @bharath The code isn't designed for two lists, I can't quite visualize what you are trying to do.

    ReplyDelete
  8. Hi..thxs but the problem has been dissaper...One year is enough to resolve it lol

    But thanks again :)

    ReplyDelete
  9. Hi, Shai.
    Thank you for the nice sample. The only thing i would like to add is a timer inside dataChanged().
    When you work with large data arrays or in case of more sophisticated processing it can be usefull.

    So, something like this:
    public void dataChanged(int type, int index) {
    if(null == inputTimer)
    inputTimer = new Timer();
    else
    inputTimer.cancel();
    inputTimer = new Timer();
    inputTimer.schedule(new TimerTask()
    {
    public void run() {
    proxyModel.filter (field.getText());
    }
    }, field.getCommitTimeout());}

    ReplyDelete
  10. Hi Shai,

    Thats a great example which you have provided the "FilterProxyListModel" class is awesome , thank you .

    Lakshmikanth Reddy.

    ReplyDelete
  11. Great stuff Shai. But how to get rid of the list numbering?

    ReplyDelete
  12. hi, shai
    i have use case where in text field i need to take only character, thanks to ur reply in java.net forums for my question..
    currently i found some thing like this.
    TextField tf = new TextField(){
    protected void insertChars(java.lang.String c){
    if(((int)c.getCharAt(0) >= 65 && (int)c.getCharAt(0) <= 91 )) || (((int)c.getCharAt(0) >= 97 && (int)c.getCharAt(0) <= 123 )) ){
    super.insertChars(c);
    return;
    }
    return;
    }
    };

    but this is not working for i am able see that text field is also taking numbers but u said that to use addDataChangeListener or addActionListener can use please send me a code snippet how to do this with above API's.. please help..

    ReplyDelete
  13. Numbering can be removed by using a different renderer.
    The final error message you would get when naming a local variable and using an inner class incorrectly.
    Multi-line text field isn't a simple task... We have it in our "todo".
    There are several samples for using DataChangeListener to filter a text field in the mailing list just add it and if the data is invalid call setText() with the last string of valid data.

    ReplyDelete
  14. Hi Shai, great article.
    Let us know if you have something new with the Multiple line text field .
    It is very common component in may applications. Any ideas how should be implemented ?

    ReplyDelete
  15. Hi Shai

    Great blog first off, its come in very useful for my implementation. One question I did have though, are we able to launch the native text entry screen without having a textfeild in the form? That is, can I use a button to launch an anonymous native text box, returning the entered text after when the user has 'Ok' soft key?

    ReplyDelete
  16. Hey shai the code works great... but what if i want to show only the list of item mathching with textfield and not the whole list covering the complete screen.. also how can we use scrollbar to populate the list.. i want to display the selected item from text int a list of checkbox with item status as selected exactly below the popup list.. so how can i do that.. pleas help me out asap....

    ReplyDelete
  17. in reply to Mark Sievers' post above.

    Figured out how to do this.

    Extend TextArea (you will need to put it in a package called com.sun.lwuit in order to override package private methods).

    public class NativeTextArea extends TextArea
    {
    /**
    * @override
    *
    * Prevents super class from re-validating parent component
    * (which will be null since we are headless)
    * */
    void onEditComplete(String pText)
    {
    super.setText(pText);
    }

    public void fireClicked()
    {
    //clear any previous text
    super.setText("");
    //invoke native screen
    super.fireClicked();
    }

    ReplyDelete
  18. @Alvaro: Multiline text field is targeted for 1.4 if we can get around all the issues it poses.

    @Mark Sievers: Display.editString might work. Also you can make a text area "look like" a button by delegating its paint logic to a button ;-)

    @ATIF: Create your own list model with the filtering logic you need.

    ReplyDelete
  19. Thanks for the source, it's really great.

    I want to display data from our database where the IDs are like 4,5,21 etc..but List index is going 0,1,2...which is the best solution to display the items and later easily get their original ID (the one used in the database)?

    Is there a way to use the database IDs in the list, or should I get original ID's after the user selected an item?

    Thanks for suggestions.

    ReplyDelete
  20. @Psysoul: use a renderer and keep business logic separate from view

    ReplyDelete
  21. Thanks for the answer. Can you write a little more info on this?

    I don't really know how to create a good architecture yet. Maybe I should use something like MVC but I want to keep things as simple as possible.

    ReplyDelete
  22. Hi shai


    Can u help me in posting a calculator program as am new to lwuit my friend told me if we understand that program when can easily work in lwuit as many concepts are covered in a single program so please can u help me in same......................

    ReplyDelete
  23. Hi Shai once again,
    I've solved the problem.
    The key is to use an EventDispatcher in the methods that update de Model just like in the setValueAt(...) method in DefaultTableModel.

    ReplyDelete
  24. Hi Shai, I am using lwuit TextField.When the focus is on the textfield and user is entering input, the soft buttons which appear are "clear" and "t9".When I use setReplaceMenu(false) the softbuttons are replaced by "back" and "submit" which are created by me but "clear" softbutton disappears.I want to add all the three functionalities of "clear", "back" and "submit" in the softbutton menu.How can I do that?

    ReplyDelete
  25. Override TextField.installCommands

    ReplyDelete
  26. Hi In list based on selected index its need to display can pls tell me..
    for(int i=0;i<JsonParser.Toptracklist.size();i++) {
    l.addItem(((TrackModel)JsonParser.Toptracklist.elementAt(i)).getTrackName().toString());
    }
    i need to get trackduration in my paga end of the track buts its coming down but last i requierd how to do can you pls tell me

    ReplyDelete
  27. I have no idea what you are asking?

    ReplyDelete
  28. Interesting, i am trying to implement reverse,

    when app starts it only shows textfied and empty list, as i start it should show filtering (as my vector may have 100 element) and i dont want to populate it :)

    Raxit

    ReplyDelete
  29. That should be pretty easy, in the filter method just return an empty vector for an empty string as a special case.

    ReplyDelete
  30. thanks Shai

    In my form i need to have two autocomplete textfield and now problems starting :)






    problem is there is tooo much space when List1 is going empty :) hope you got it.

    ReplyDelete
  31. Even i am asking question, let me be very honest, lwuit is going over my head :) i tried to go thru the doc, but nothing is precise for simple, dumb guy.

    PS : i am not coming from swing/awt background.

    ReplyDelete
  32. Set the form to grid layout that has 2 rows and one column.
    Place The first list and text field into a container and add it to the form (ideally use border layout, north for the field and center for the list). Do the same for the second list and text field.
    They will always fit in the screen.

    ReplyDelete
  33. Just invoke the same filter method from the command's action performed instead of the current listener.

    ReplyDelete
  34. call proxyModel.filter() in your commands action performed. You will need to rearrange your code a bit so the command creation happens bellow the proxyModel creation.

    ReplyDelete
  35. thanks shai's for your help, now my program can run well...in other case i want to ask you about datepicker in lwuit...does in lwuit can make datepicker or calendar???

    ReplyDelete
  36. LWUIT has a Calendar component although its very different from the MIDP date/time component.

    ReplyDelete
  37. Thank for nice example of autocomplte with list. I am interested to develop multipleautocomplete in lwuit (eg: To:Component in gmail and hotmail).
    Could you please help me with some simple example

    ReplyDelete
  38. You can just popup a dialog with the list in it. You should probably do the following though:
    1. Set the tint color of the form to 0.
    2. Override the dialogs keyPressed/Released methods to send their input to the text field component unless it is an up/down arrow key.
    3. Define the dialog to dispose on pointer press outside of dialog bounds.

    ReplyDelete
  39. Hi Shai!
    love the lwuit editor.
    I know this might be a silly question but i have been sitting for hours and am still stuck. ok so here's my question:
    Lets say you created a textfield where a user enters something, lets call it text1, this is created on a form in the resource editor, how do u access the text in netbeans. and also what is the code to display another form, example to create a login form and if the details are correct then go to formMain or formError

    ReplyDelete
  40. See some of my videos showing the binding of the UI to code. Generally once you generate the netbeans code binding becomes automatic and you should be able to invoke findMyComponentName(rootContainer) to find any component within the rootContainer hierarchy. E.g. I have a button that delivered an event and I want to find a text field named "search" in the GUI builder:
    TextField t = findSearch(myButton.getParent);

    ReplyDelete
  41. Vincente I suggest you use ActionListener rather than selection listener. Selection listener is designed for quick refresh of the data based on selection not user interaction.

    ReplyDelete
  42. Hi Shai and David
    will please let me know how to get rid of index numbers? I just need to show values.

    ReplyDelete