Thursday, October 21, 2010

The UI Builder Class: How to actually use the LWUIT GUI builder

In my previous GUI Builder post I was a bit vague mostly because I was doing it in a rush in a day of some serious major commits to LWUIT's SVN, I didn't have that much time to post and when I do get a chance to write something there is so much new stuff I'm just overwhelmed with what to write about... (FYI allot more is coming really soon!).

Please notice that when you use the LWUIT GUI builder and its related API's that its alpha level software likely to break and fail in interesting ways. I did my best to prevent a resource file corruption but I strongly suggest to constantly backup the resource file while working (and please file issues if you run into them!).
The UIBuilder API is also in alpha form and might change significantly although I tend to believe we have the right direction there...

The premise is this: the designer creates a UI version and names GUI components, he can create commands as well including navigation commands, exit, minimize. He can also define a long running command which will by default trigger a thread to execute...

All UI elements can be loaded using the UIBuilder class. Why not just use the Resources API?
Since Resources is essential for using LWUIT, adding the UIBuilder as an import would cause any application (even those that don't use the UIBuilder) to increase in size! We don't want people who aren't using the feature to pay the penalty for its existence!

The UI Builder is designed for use as a state machine carrying out the current state of the application so when any event occurs a subclass of the UIBuilder can just process it. The simplest way and most robust way for changes is to use the resource editor to generate some code for you (yes I know its not a code generation tool but there is a hack...). The code you need to write to process the UI includes some boilerplate code which is pretty mundane for processing commands based on id's or processing an event from a component, to simplify this process I added to the resource editor two menu items, the firs is simpler: "Help->Show Source For Using". The second is far more elaborate and I will try to go over it and its output bellow: "MIDlet->Generate UI State Machine".

When running the Generate UI state machine menn option in a project with UI builder content it prompts you for a directory/file name for the output. Just give it a new file name in your project directory and it will create a UIBuilder subclass containing most of what you need...
The trick is not to touch that code! DO NOT CHANGE THAT CODE!

Sure you can change it and everything will be just fine, however it you will make changes to the GUI regenerating that file will obviously loose all those changes which is not something you want!
To solve it you need to subclass the generated class rather than subclass UIBuilder and just override the appropriate methods (some might even be abstract for your convenience), then if a UI changes you can safely overwrite the base class since you didn't change anything there...

Just so we are clear on the subject, after generating the code I can run the code by just using this and nothing else:
import javax.microedition.midlet.*;
import com.sun.lwuit.*;

public class RatioMIDlet extends MIDlet {
public void startApp() {
Display.init(this);
new StateMachine("/ratios.res");
}

public void pauseApp() {
}

public void destroyApp(boolean unconditional) {
}
}


Obviously for a real world example that actually does something I would want to subclass and override methods in StateMachine.java. Lets look at the generated code which will help us understand both how to use the UIBuilder ourselves and how to subclass the code:


import com.sun.lwuit.*;
import com.sun.lwuit.util.*;
import com.sun.lwuit.events.*;
import com.sun.lwuit.plaf.*;

public class StateMachine extends UIBuilder {
public StateMachine(Resources res, String resPath, boolean loadTheme) {
UIBuilder.registerCustomComponent("HTMLComponent", com.sun.lwuit.html.HTMLComponent.class);
if(res != null) {
setResourceFilePath(resPath);
Form f = (Form)createContainer(res, "Splash");
f.show();
} else {
Form f = (Form)createContainer(resPath, "Splash");
f.show();
}
if(loadTheme) {
if(res == null) {
try {
res = Resources.open(resPath);
} catch(java.io.IOException err) { err.printStackTrace(); }
}
UIManager.getInstance().setThemeProps(res.getTheme(res.getThemeResourceNames()[0]));
}
}

public StateMachine(String resPath) {
this(null, resPath, true);
}

public StateMachine(Resources res) {
this(res, null, true);
}

public StateMachine(String resPath, boolean loadTheme) {
this(null, resPath, loadTheme);
}

public StateMachine(Resources res, boolean loadTheme) {
this(res, null, loadTheme);
}

public static final int COMMAND_BREAD_BACK = 5;
public static final int COMMAND_DOUGHS_BREAD = 4;
public static final int COMMAND_DOUGHS_BACK = 3;
public static final int COMMAND_MAINSCREEN_DOUGHS = 2;

protected boolean onBreadBack() {
return false;
}

protected boolean onDoughsBread() {
return false;
}

protected boolean onDoughsBack() {
return false;
}

protected boolean onMainscreenDoughs() {
return false;
}

protected void processCommand(ActionEvent ev, Command cmd) {
switch(cmd.getId()) {
case COMMAND_BREAD_BACK:
if(onBreadBack()) {
ev.consume();
}
return;

case COMMAND_DOUGHS_BREAD:
if(onDoughsBread()) {
ev.consume();
}
return;

case COMMAND_DOUGHS_BACK:
if(onDoughsBack()) {
ev.consume();
}
return;

case COMMAND_MAINSCREEN_DOUGHS:
if(onMainscreenDoughs()) {
ev.consume();
}
return;

}
}

protected void handleComponentAction(Component c, ActionEvent event) {
if(c.getComponentForm().getName().equals("Bread")) {
if("TextField1".equals(c.getName())) {
onTextfield1Action(c, event);
return;
}
if("ComboBox1".equals(c.getName())) {
onCombobox1Action(c, event);
return;
}
if("TextField11".equals(c.getName())) {
onTextfield11Action(c, event);
return;
}
if("ComboBox11".equals(c.getName())) {
onCombobox11Action(c, event);
return;
}
if("TextField111".equals(c.getName())) {
onTextfield111Action(c, event);
return;
}
if("ComboBox111".equals(c.getName())) {
onCombobox111Action(c, event);
return;
}
if("TextField12".equals(c.getName())) {
onTextfield12Action(c, event);
return;
}
if("ComboBox12".equals(c.getName())) {
onCombobox12Action(c, event);
return;
}
}
if(c.getComponentForm().getName().equals("MainScreen")) {
if("Doughs".equals(c.getName())) {
onDoughsAction(c, event);
return;
}
if("Batters".equals(c.getName())) {
onBattersAction(c, event);
return;
}
if("Custards".equals(c.getName())) {
onCustardsAction(c, event);
return;
}
if("Fat-Based Sauces".equals(c.getName())) {
onFatBasedSaucesAction(c, event);
return;
}
if("Stocks & Thickeners".equals(c.getName())) {
onStocksThickenersAction(c, event);
return;
}
if("Meat Related Ratios".equals(c.getName())) {
onMeatRelatedRatiosAction(c, event);
return;
}
if("Desert Sauces".equals(c.getName())) {
onDesertSaucesAction(c, event);
return;
}
}
if(c.getComponentForm().getName().equals("Doughs")) {
if("Bread".equals(c.getName())) {
onBreadAction(c, event);
return;
}
if("Pasta Dough".equals(c.getName())) {
onPastaDoughAction(c, event);
return;
}
if("Pie Dough".equals(c.getName())) {
onPieDoughAction(c, event);
return;
}
if("Biscuit".equals(c.getName())) {
onBiscuitAction(c, event);
return;
}
if("Cookie Dough".equals(c.getName())) {
onCookieDoughAction(c, event);
return;
}
if("Pâte à Choux".equals(c.getName())) {
onPTeChouxAction(c, event);
return;
}
}
}

protected void onTextfield1Action(Component c, ActionEvent event) {
}

protected void onCombobox1Action(Component c, ActionEvent event) {
}

protected void onTextfield11Action(Component c, ActionEvent event) {
}

protected void onCombobox11Action(Component c, ActionEvent event) {
}

protected void onTextfield111Action(Component c, ActionEvent event) {
}

protected void onCombobox111Action(Component c, ActionEvent event) {
}

protected void onTextfield12Action(Component c, ActionEvent event) {
}

protected void onCombobox12Action(Component c, ActionEvent event) {
}

protected void onDoughsAction(Component c, ActionEvent event) {
}

protected void onBattersAction(Component c, ActionEvent event) {
}

protected void onCustardsAction(Component c, ActionEvent event) {
}

protected void onFatBasedSaucesAction(Component c, ActionEvent event) {
}

protected void onStocksThickenersAction(Component c, ActionEvent event) {
}

protected void onMeatRelatedRatiosAction(Component c, ActionEvent event) {
}

protected void onDesertSaucesAction(Component c, ActionEvent event) {
}

protected void onBreadAction(Component c, ActionEvent event) {
}

protected void onPastaDoughAction(Component c, ActionEvent event) {
}

protected void onPieDoughAction(Component c, ActionEvent event) {
}

protected void onBiscuitAction(Component c, ActionEvent event) {
}

protected void onCookieDoughAction(Component c, ActionEvent event) {
}

protected void onPTeChouxAction(Component c, ActionEvent event) {
}

} 
 
 
Like most generated code it is rather on the verbose side and methods/variables don't have the best naming logic, this is to be expected since the tool generates methods whether needed or not and provides names based on the names given by the designer (who in this case often chose not to change the default names given by the tool...).
The code starts with several constructors to create the builder they all lead to one central constructor which I would like to focus on:

public StateMachine(Resources res, String resPath, boolean loadTheme) {
UIBuilder.registerCustomComponent("HTMLComponent", com.sun.lwuit.html.HTMLComponent.class);
if(res != null) {
setResourceFilePath(resPath);
Form f = (Form)createContainer(res, "Splash");
f.show();
} else {
Form f = (Form)createContainer(resPath, "Splash");
f.show();
}
if(loadTheme) {
if(res == null) {
try {
res = Resources.open(resPath);
} catch(java.io.IOException err) { err.printStackTrace(); }
}
UIManager.getInstance().setThemeProps(res.getTheme(res.getThemeResourceNames()[0]));
}
}


Notice the first line: registerCustomComponent(). Its crucial.
We need this line to let the GUI builder "know" how to locate a component by mapping the UIID to the component class. A Java SE developer might think "why not use Class.forName()". The problem is that Class.forName() will not work with obfuscated code, so we have to explicitly declare which classes we are using beyond the "core" LWUIT classes (even if they are within the LWUIT sub packages!).
This is a good thing since it means that you won't be importing Table, Tree & HTMLComponent unless you explicitly want them in your code. (BTW You will get an exception from the UIBuilder if you miss such an register call).
Notice that this also allows you to import your own arbitrary components and even use them in the resource editor's GUI builder (just by using pickMIDlet, but more on that at a later date).

To create the first form (which I picked from a combo box in the UI builder when generating the code) the UI builder needs the resource file and just invokes createContainer (which in this case returns a Form). Notice the call to setResourceFilePath, its important.
Once a form was created the UI builder tries to discard the resource file from RAM, however the resulting UI might include a Command for navigation to another form (declared within the resource file). How would the resource file be opened?
The solution is quite simple, if we have the name of the resource file we just open it (so creating the UI using a path name from the JAR is the simplest way possible to work with the UI builder). However, for more elaborate use cases such as storing downloaded resources in the RMS etc. one can just override the fetchResources() method and do pretty much anything to load a resources file on demand.


Next in the class we have constants for the command id's declared in the GUI builder, the GUI builder tries to push for them to be unique but doesn't force it (since you might want two commands with the same id). The generated code automatically adds a switch case and methods to process the commands which return true if the triggering event should be consumed.

And finally we have the exact same thing for every component that triggers an actionEvent. You can override handleComponentAction directly or let it do its magic and override your component name method in the code.

There are quite allot of additional methods in UIBuilder designed to enable you to use it in many varied ways (e.g. without subclassing and by binding listeners), I would like to mention a few methods of interest:
protected void processBackground(Form f)

This method is designed for the Splash Screen/wait dialog use case and is invoked on a separate thread to allow a long running task to take place. The default implementation is a sleep of 3000 ms allowing for easy debugging when running an application (e.g. I used this for the splash screen of the Ratio demo and it waited slightly even though the application doesn't load anything). To enable this in the GUI define the next form property which will trigger this method upon showing the form and seamlessly move to the next form on completion.

Similarly we have asyncCommandProcess(Command cmd, ActionEvent sourceEvent) and postAsyncCommand(Command cmd, ActionEvent sourceEvent).
A navigation command in  the builder can be defined as asynchronous which will trigger these methods allowing the user to write background logic in the asyncCommandProcess() method and update the UI with the changes in the postAsyncCommand() method. Currently the UI builder doesn't generate code for async commands using these methods but it probably should do that for the next iteration.

I have allot more to write about and this was supposed to be a really short post so I'll try to keep some energy for these posts.

5 comments:

  1. Wow, beautiful work!

    Question: will this work yet currently with the Blackberry port? I'm assuming otherwise.

    (I ask because I generated a StateMachine demonstration project like you show here, but ran into "verification" errors when I tried using the PreviewMidlet.jar generated by the resource editor.)

    ReplyDelete
  2. Yes generated code and the GUI builder will work on the blackberry with the SVN trunk. The preview MIDlet won't work on the blackberry since it uses the LWUIT MIDP implementation and not the blackberry implementation.

    ReplyDelete
  3. Question regarding UIBuilder.addComponentListener:

    I want to add a FocusListener to a component. But I note in the javadoc that it says "Notice that this method is only effective before the form was created and would do nothing for an existing form"

    So does that mean that if I call this method in the constructor for MyStateMachine, where MyStateMachine extends the autogenerated StateMachine, that I'm already too late? But if so, then how would I go about it, without editing the autogenerated StateMachine??

    ReplyDelete
  4. Oh, maybe I didn't read the javadoc for UIbuilder closely enough before asking my last question: I take it I should be invoking bindComponentToListener() inside of postCreateComponent() ?

    ReplyDelete
  5. The UIBuilder has already been quite useful to me in my little project. I think I might be ready to try adding user components now. I've done the "registerCustomComponent()' in my StateMachine and setUIID() in my custom component. Is there anything else I need to do to make a midlet jar suitable for "Pick midlet" in the ResourceEditor? (Does the jar have to have all the LWUIT classes in it, by the way?)

    ReplyDelete