Tuesday, April 14, 2009

LWUIT Model Tree

Since the forum is down (as usual) my responses to some of the comments in the forum just never made it past the mailing list, one of the questions in the forum that comes up every now and again is "how to create a tree in LWUIT?"

We get this question every now and again and generally our response is that we don't but would like to get such a contribution. However, looking at our samples it seems that most of our component building explanations revolve around "small" components rather than composites which would fit a tree much better.

A composite component is really a Container which we would assemble into a specialized component by tailoring together smaller components. Naturally we could also perform custom painting for such a composite but that often defeats the purpose...

These components aren't suitable for everything but for things such as a tree we gain several huge advantages by using Container nesting to build a tree:
  • We represent the nodes using custom components allowing subclasses to manipulate the look of the tree to a great degree.
  • We hardly need to write any code and styles, touch support, keyboard navigation etc. are all built in
  • We can leverage transitions and component nesting for really stunning effects
The tree uses a simple model component with some dummy data, mapping it to a filesystem or any other such element is trivial. All you need to do is implement TreeModel appropriately and it only contains 2 methods (I spent allot of time with the Swing tree model...). Its not as powerful as the Swing model since it doesn't support editing or mutation (from the model) but these are relatively easy to add. It took me roughly 2 hours to write everything including this post, it was so long because I discovered a minor LWUIT bug in scrolling/animation that is triggered when a tree starts off small and ends up being rather large.

Lets go strait to the code, first we need the model:
/**
* Arranges tree node objects, a node can essentially be anything
*
* @author Shai Almog
*/

interface TreeModel {
/**
* Returns the child objects representing the given parent, null should return
* the root objects
*/

public Vector getChildren(Object parent);

/**
* Is the node a leaf or a folder
*/

public boolean isLeaf(Object node);
}

I think thats pretty self explanitory...
Then we would like the implementation:
class Node {
Object[] children;
String value;

public Node(String value, Object[] children) {
this.children = children;
this.value = value;
}

public String toString() {
return value;
}
}

TreeModel model = new TreeModel() {
Node[] sillyTree = {
new Node("Root 1", new Node[] {
new Node("Child 1", new Node[] {
new Node("Gand Child 1", new Node[] {
}),
new Node("Gand Child 2", new Node[] {
}),
new Node("Gand Child 3", new Node[] {
}),
new Node("Gand Child 4", new Node[] {
}),
}),
new Node("Child 2", new Node[] {
new Node("Something Else", new Node[] {
}),
new Node("More of the same", new Node[] {
}),
}),
new Node("Child 3", new Node[] {
}),
new Node("Child 4", new Node[] {
}),
}),
new Node("Root 2", new Node[] {
new Node("Something Else", new Node[] {
}),
new Node("More of the same", new Node[] {
}),
}),
new Node("Root 3", new Node[] {
new Node("Something Else", new Node[] {
}),
new Node("More of the same", new Node[] {
}),
}),
new Node("Root 4", new Node[] {
}),
};
Thats a bit long and tedious but its mostly unrelated to LWUIT, the main code is the tree component code which is really rather simple:
/**
* LWUIT Tree component sample
*
* @author Shai Almog
*/

public class TreeComponent extends Container {
private static final String KEY_OBJECT = "TREE_OBJECT";
private static final String KEY_PARENT = "TREE_PARENT";
private static final String KEY_EXPANDED = "TREE_NODE_EXPANDED";
private static final String KEY_DEPTH = "TREE_DEPTH";

private ActionListener expansionListener = new ActionListener() {
public void actionPerformed(ActionEvent evt) {
Component c = (Component)evt.getSource();
Object e = c.getClientProperty(KEY_EXPANDED);
if(e != null && e.equals("true")) {
collapseNode(c);
} else {
expandNode(c);
}
}
};
private TreeModel model;
private Image folder;
private Image nodeImage;
private int depthIndent = 15;

public TreeComponent(TreeModel model) {
try {
this.model = model;
folder = Image.createImage("/folder.png");
nodeImage = Image.createImage("/page_white.png");
setLayout(new BoxLayout(BoxLayout.Y_AXIS));
buildBranch(null, 0, this);
setScrollableY(true);
} catch (IOException ex) {
ex.printStackTrace();
}
}

private void expandNode(Component c) {
c.putClientProperty(KEY_EXPANDED, "true");
int depth = ((Integer)c.getClientProperty(KEY_DEPTH)).intValue();
Container parent = c.getParent();
Object o = c.getClientProperty(KEY_OBJECT);
Container dest = new Container(new BoxLayout(BoxLayout.Y_AXIS));
Label dummy = new Label();
parent.addComponent(BorderLayout.CENTER, dummy);
buildBranch(o, depth, dest);
parent.replace(dummy, dest, CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, true, 300));
}

private void collapseNode(Component c) {
c.putClientProperty(KEY_EXPANDED, null);
Container p = c.getParent();
for(int iter = 0 ; iter < p.getComponentCount() ; iter++) {
if(p.getComponentAt(iter) != c) {
Label dummy = new Label();
p.replaceAndWait(p.getComponentAt(iter), dummy, CommonTransitions.createSlide(CommonTransitions.SLIDE_VERTICAL, false, 300));
p.removeComponent(dummy);
}
}
}

/**
* Adds the child components of a tree branch to the given container.nt
*/

protected void buildBranch(Object parent, int depth, Container destination) {
Vector children = model.getChildren(parent);
int size = children.size();
Integer depthVal = new Integer(depth + 1);
for(int iter = 0 ; iter < size ; iter++) {
Object current = children.elementAt(iter);
Button nodeComponent = createNodeComponent(current, depth);
if(model.isLeaf(current)) {
destination.addComponent(nodeComponent);
} else {
Container componentArea = new Container(new BorderLayout());
componentArea.addComponent(BorderLayout.NORTH, nodeComponent);
destination.addComponent(componentArea);
nodeComponent.addActionListener(expansionListener);
}
nodeComponent.putClientProperty(KEY_OBJECT, current);
nodeComponent.putClientProperty(KEY_PARENT, parent);
nodeComponent.putClientProperty(KEY_DEPTH, depthVal);
}
}

protected Button createNodeComponent(Object node, int depth) {
Button cmp = new Button(node.toString());
if(model.isLeaf(node)) {
cmp.setIcon(nodeImage);
} else {
cmp.setIcon(folder);
}
updateNodeComponentStyle(cmp.getSelectedStyle(), depth);
updateNodeComponentStyle(cmp.getUnselectedStyle(), depth);
return cmp;
}

protected void updateNodeComponentStyle(Style s, int depth) {
s.setBorder(null);
s.setPadding(LEFT, depth * depthIndent);
}
}


Then all we need is to use it in order to produce the video on the right:
Form treeForm = new Form("Tree Test");
treeForm.setLayout(new BorderLayout());
TreeComponent tree = new TreeComponent(model);
treeForm.addComponent(BorderLayout.CENTER, tree);
treeForm.setScrollable(false);
treeForm.show();

9 comments:

  1. This could also make for a super-cool way to implement submenus (although you've already expressed your view on those ;-).

    It would be neat to use FIRE_RIGHT to open a submenu 'inside' the normal menu, and be able to close it again using FIRE_LEFT. By having the submenu visually inside the menu, submenus might become much more intuitive...

    Not sure I have the expertise level to do it myself, but maybe you would give some pointers as to how it could be done?

    ReplyDelete
  2. Shai, could you please add zip file of all sources in this post? Somehow I can't get examples working. I added some missing bits, but I get blank screen when running it.

    ReplyDelete
  3. as you sad when the content of container exceeds the size of the phone screen scrolling crashes. Is there a way to solve that container scrolling bug easily

    ReplyDelete
  4. This doesn't crash with scrolling.
    Check out the JavaOne demo we had that features a tree for image selection as part of the demo.

    ReplyDelete
  5. Thanks Shai
    could you plz tell me how to get the source code? thanks again

    ReplyDelete
  6. This comment has been removed by the author.

    ReplyDelete
  7. Shai, Please provide the code for the TreeView as a .java or .zip file. Please.

    ReplyDelete
  8. The tree component is officially in the LWUIT SVN.

    ReplyDelete
  9. Why aren't you using the Tree component from LWUIT?
    Its better tested and there is a sample in the LWUIT demo.
    The code above is outdated by now.

    ReplyDelete