Core text2gui

Now that we understand the basic concepts of what we are trying to do, we can now describe the actual API provided by the text2gui library.

Interpolating Converters, Revisited

We know from the basics that we need interpolating converters from strings and resource bundle keys to a variety of objects. So that all converters appear uniform, they all implement the interface com.taco.text.IInterpolatingConverter which has the following two methods:

/** Convert the resource bundle key BASEKEY to an object. */
Object toObject(ResourceBundle bundle, String baseKey,
INoReturnMap argMap, KeyLookupRecord context);

/** Convert the string S to an object. */
Object toObject(String s, ResourceBundle bundle,
INoReturnMap argMap, KeyLookupRecord context);
These methods return Object because it is the most general type available; implementations of IInterpolatingConverter can return objects of any type. The Object return type has the following consequences:
Regarding the arguments to both of the toObject() methods, bundle is a resource bundle that contains the configuration data needed, if necessary, for conversion. In the case of the string to object conversion, if no data will be taken from a resource bundle, the bundle argument may be null.

argMap is an argument map as described in Argument Maps. Its type is com.taco.data.INoReturnMap, which is simply a map whose values can be put without retrieving the previous value, using putNoReturn(). Normally, argument maps contain values that depend on dynamic data, such as the name of a song that is being played. Using interpolation, converters can retrieve these values and use them to configure their returned objects. If no values need to be passed to the converter,  argMap may be null.

For all but the most advanced users of text2gui, the context argument should be set to null.

For the resource bundle key to object conversion method, the baseKey argument refers to the name of the resource bundle key to be converted. It is named baseKey because subkeys like panel.font are read given a base key panel.

For the string to object conversion method, s is simply the string to convert.

First, let's look at an example of string to object conversion:
INoReturnMap argMap = new NoReturnMapAdapter(4);
argMap.putNoReturn("w", new Integer(50));
argMap.putNoReturn("h", new Integer(25));
String s = "{width=$w, height=$h}";
Dimension dim = (Dimension) DimensionConverter.instance.toObject(
s, null, argMap, null);
sets the dim to a Dimension of width 50 and height 25. The resource bundle parameter is null because we don't need any data from a resource bundle in this example. Even though DimensionConverter only creates instances of one class, java.awt.Dimension, we still need to cast the result of toObject() into Dimension.

Here's an example of resource bundle key to object conversion. First we need a resource bundle, "FooResourceBundle.properties":
# Entry point: "panel"
panel=jpanel layout={box axis=x} contents=[
  {jlabel text="Age\:" hAlign=left},
  {strut length=15},
  {jtextfield text=$initialText editable=false
disabledTextColor=gray}
]
Now in our Java code, to create the panel we would write:
ResourceBundle bundle = 
ChainedResourceBundleFactory.DEFAULT_INSTANCE.getBundle(
"FooResourceBundle");
INoReturnMap argMap = SwingInvokeProxyFactory.makeSwingInvokeProxyMap();
argMap.putNoReturn("initialText", "None of your business!");
JPanel panel = (JPanel)
DispatchingComponentConverter.DEFAULT_INSTANCE.toObject(bundle, "panel",
argMap, null);
which would create a panel with a label and a uneditable text field which says "None of your business!". Because the argument map is a Swing invoke proxy map, which is observable, the text can be updated later through the argument map.

Interpolation Types

Now might be a good time to actually read through some of the details on the two types of interpolation:
  1. Argument map interpolation
  2. Global variable interpolation
Atomic and Composite Converters

In the previous example we created a panel, which has many properties like layout, contents, backgroundColor, etc. We call converters that create objects with many properties composite converters because they create objects composed of other objects. Most of the converters that have properties, including the converters for Swing components, are composite converters.

Another kind of converter is an atomic converter defined in com.taco.text.AtomConverter. Since atomic converters have no properties, when an atomic resource bundle key to object converter operates, it only needs to read the value mapped to the original bundle key. From that single value (usually a string) it is possible to create the entire object (assuming the string isn't a reference to another key like %otherKey). The following is the conversion process for an atomic converter, given a resource bundle key baseKey and a resource bundle bundle:
  1. If baseKey is assigned to a value in bundle, set val to that value.
Now in retrospect, it is clear why string to object conversion should be implemented in the same converter object that performs resource bundle key to object conversion.

In contrast, a composite resource bundle key to object converter reads many different keys of the resource bundle. Given a resource bundle key baseKey and a resource bundle bundle, a composite converter performs the following steps:
  1. If baseKey is assigned to a value in bundle, set val to that value.
  2. Otherwise, construct an instance of the composite object. (This may need to read subkeys as in step 3; the properties corresponding to these subkeys are called creation properties.)
  3. For each ordinary property with name prop:
    1. Determine the converter associated with prop. For example, the converter associated with the foreground property of a Swing component is the color converter.
    2. Using the associated converter, convert the subkey baseKey.prop to an object. If an error occurs, ignore it -- this allows the user to omit property assignments.
    3. Set the property of the composite object with the value determined in step ii.
  4. If no properties were set in step 3), throw a MissingResourceException. This implies at least one property of a composite must be set, or a conversion will fail.
The important point is that if a value is directly mapped to a resource bundle key, the value is used for conversion,  and the subkeys are ignored. For example, if we had the properties file:
okButton=jbutton text="All righty then" \
prefSize={width=100, height=50}
okButton.text=Explicatives that will never see the light of day!
then creating the OK button will result in a button with text "All righty then", not "Explicatives that will never see the light of day!". Furthermore, any other subkeys of okButton will be ignored.

Here's an example that creates the same panel as above, but with strings broken up into subkeys:

# Entry point: "panel"
panel.dispatchType=jpanel
panel.layout=box axis=x
panel.contents=[%label, {strut length=15}, %textField]

label.text=Age\:
label.hAlign=left

textField.dispatchType=jtextfield
# Reference the "initialText" key in the argument map
textField.text=$initialText
textField.editable=false
textField.disabledTextColor=GRAY

The panel would be created with the same Java code as in the previous example.

Creation Properties of Composites

In step 2 of the above process, we mentioned that some properties may be read before a composite object is constructed. These properties, called creation properties, are needed to pass to the constructor of the composite; they cannot be set after creation. This means the property values are created before the actual composite exists. (If the composite is assigned to a global variable, the global variable won't be available until after all creation properties are converted). Also, the creation properties of a composite cannot be updated through an argument map, nor will map consistency on creation properties be available.

As an example, consider the borders in javax.swing.border. Once they are constructed, they are immutable. Thus all properties of borders are creation properties.

Asides: More Details on Composite Creation

The following asides provide more details on the creation process of composite objects.
Configuration of Composite Objects

Not only can composite converters create composite objects, but they can also configure the properties of existing ones using a resource bundle key. The class com.taco.text.CompositeConverter contains the following method:

void configureComposite(Object composite, String baseKey, ResourceBundle bundle,
  INoReturnMap argMap);


However, it is unlikely you will call this method directly, for reasons we will see shortly.

Braced Property String Syntax for Composites

So far we have discussed how resource bundle keys are converted to composite objects, but not how strings are converted. Recall that step 1) of the creation process was the following:
  1. If baseKey is assigned to a value in bundle, set val to that value.
Now we consider the case where baseKey is assigned to a string in the resource bundle. Actually, the string syntax for composites can vary, but the most common syntax is the braced property syntax. Here is the general form:

classID prop1=value1 prop2=value2 ...

where classID is an identifier for the type of composite object, prop1 and prop2 are property names of the composite, and value1 and value2 are strings that specify the value for prop1 and prop2, respectively. Of course, there can be no property assignments at all or as many property assignments as needed. Any value string that is not a reference, unbraced (with any Java brace character), and conforms to a syntax that might contain spaces must be enclosed in curly braces ('{' and '}').

For example, consider a resource bundle backed by the following properties file:
cutMenuItem=jmenuitem text="Cut" mnemonic=c \
icon={icon location="toolbarButtonGraphics/general/Cut16.gif"} \
actionListeners=%cutActionListeners

# A single listener implemented with a BeanShell script -- see below
cutActionListeners=[{ new ActionListener() { public void actionPerformed(ActionEvent event) { JOptionPane.showMessageDialog(event.getSource(), "Ouch don't cut me!"); } } }]
By converting the resource bundle key cutMenuItem, we get a menu item with its text, mnemonic, icon, and action listeners set. Now let's pick apart the string. jmenuitem is the class ID for a menu item. Since "Cut" is already enclosed in Java braces (in this case double quotes), it does not need to be enclosed in curly braces. The mnemonic property is converted by the character converter, whose string syntax does not allow for spaces, so c does not need to be enclosed in curly braces either. But because the string syntax for an icon is itself a braced property syntax, which can contain spaces, the value for the icon property does need to be enclosed in curly braces. Finally, the actionListeners value is set to a reference to a resource bundle key. Pure references never need to be enclosed in curly braces, so %cutActionListeners is not braced.

Dispatching Converters

Sometimes it is desirable to have a converter that can create many subclasses of a parent class. For example, the contents of a container can be any component type. For each of the contents, a generic component converter is needed -- one that is able to create any kind of component -- buttons, labels, frames, etc. But how would such a converter decide what is actually being created?

If a string is being converted, and the string conforms to the braced property syntax, the class ID determines which specific subclass is to be created. In the example above, the class ID is jmenuitem, so the generic component converter knows to create a menu item.

If a resource bundle key is being converted, the dispatchType subkey is examined. The value of the dispatchType subkey is exactly the same as the class ID that would have been used if a string were converted instead of a bundle key. For example, the above menu item could also be coded as:

cutMenuItem.dispatchType=jmenuitem
cutMenuItem.text=Cut
cutMenuItem.mnemonic=c
cutMenuItem.icon.location=toolbarButtonGraphics/general/Cut16.gif
cutMenuItem.actionListeners=%cutActionListeners

Now it's time to discuss the implementation of these generic converters. We call these converters dispatching converters because based on the class ID or dispatch type, they dispatch the conversion task to converters specialized for the appropriate type. In the example above, the class DispatchingComponentConverter can be used to convert the cutMenuItem resource bundle key. When it finds that dispatchType is jmenuitem, it dispatches an instance of JMenuItemConverter to perform the conversion.

There are three dispatching converters in the text2gui library:
  1. The component converter (com.taco.swinger.text2gui.DispatchingComponentConverter)
  2. The layout converter (com.taco.swinger.text2gui.DispatchingLayoutConverter)
  3. The border converter (com.taco.swinger.text2gui.border.DispatchingBorderConverter)
Most likely, you'll only need to use the dispatching component converter in your Java code. But whenever a component is created, the dispatching layout converter and dispatching border converter are also used for layout and border properties of the component. Here's an example properties file that demonstrates dispatching of border and layout converters:

#Entry point: panel
panel.border.dispatchType=titled
panel.border.title=Lifestyle Choices
panel.layout=grid cols=2 hgap=7
panel.contents=[
  %smokeLabel, %smokeCheckBox,
  %exerciseLabel, %exerciseField
]

smokeLabel.dispatchType=jlabel
smokeLabel.text=Do you smoke?

smokeCheckBox.dispatchType=jcheckbox
smokeCheckBox.hAlign=center
smokeCheckBox.selected=$doSmoke:rw

exerciseLabel.dispatchType=jlabel
exerciseLabel.text=How many times per week do you exercise?

exerciseField.dispatchType=jformattedtextfield
exerciseField.value=$exerciseFreq:rw
exerciseField.hAlign=right

In the above example, the dispatchType subkey for the panel's border is set to titled, so the dispatching border converter knows to use TitledBorderConverter, which knows how to interepret the title subkey. The layout is given by a string conforming to the braced property syntax, so the dispatching layout converter knows to use the first word, grid, as the class ID, and GridLayoutConverter is used to convert the layout string. Each of the four components inside the panel is given by a reference to a resource bundle key. Each of the bundle keys has its dispatchType subkey set so that the dispatching component converter knows what kind of converter to use.

Installing Types in a Dispatching Converter

A nice feature of dispatching converters is that additional types can be added without any code changes. com.taco.text.DispatchingConverter, the common parent class for all dispatching converters, contains the following method:
/** Use CONVERTER as the converter for strings or resource bundle keys with 
* class
ID CLASSID.
 */

void installType(String classID, IInterpolatingConverter converter);

The usage of this method is best illustrated with an example. If we were to create a subclass of JTable called SuperTable, we could install an interpolating converter to instances of SuperTable by calling

DispatchingComponentConverter.DEFAULT_INSTANCE.installType("supertable",
  new SuperTableConverter());


assuming that we want the class ID to be supertable. Then we could use the component converter to create component hierarchies containing instances of SuperTable, so long as we specify the class ID supertable for every instance of SuperTable in the hierarchy.

The converter used for a given class ID can also be replaced using the same method. Perhaps we want all tables created to be instances of SuperTable instead of JTable. Then we would call

DispatchingComponentConverter.DEFAULT_INSTANCE.installType("jtable",
  new SuperTableConverter());


Class ID Guessing in DispatchingComponentConveter

The dispatching component converter also has a feature which makes code development much faster: class ID guessing. This feature allows you to omit the dispatchType subkey when converting a resource bundle key to a component. If the dispatchType subkey is not explicitly set, the component converter can make an educated guess as to what kind of component you are trying to create, based on the name of the resource bundle key. To see how this works, suppose we try to convert the resource bundle key okButton22. If the dispatchType subkey is not set, the string okButton22 is examined. The ending digits are ignored, leaving okButton. Then, for each component type jxxx, the dispatching component converter sees if okButton ends with xxx, ignoring case. It turns out that jbutton satisifies this condition, so okButton22 is converted with the JButton converter.

In the case of component types that have common suffixes, the longest one takes precedence. For example, the resource bundle key wordWrapCheckBoxMenuItem is converted with the JCheckBoxMenuItem converter, not the JMenuItem converter. For efficiency reasons, class ID guessing won't occur if the base key contains a dot ('.').

In the previous example in which a Lifestyle Choices panel was created, the following lines could have been omitted, due to class ID guessing:

smokeLabel.dispatchType=jlabel
smokeCheckBox.dispatchType=jcheckbox
exerciseLabel.dispatchType=jlabel

However, the line

exerciseField.dispatchType=jformattedtextfield

is still required, since exerciseField doesn't end with formattedtextfield.

As convenient as this feature is, it is not recommended for production use. It's best to fill in the dispatchType subkeys once you have finished your GUI design, because of the runtime cost of class ID guessing.

Composite Configuration with a Dispatching Converter

We mentioned that existing composite objects can also be configured with the corresponding composite converter, but that the user is unlikely to do so. It is more convenient for the user to perform the configuration through the dispatching converter that contains the converter for the special type of composite.

This way the user doesn't need to know about JPanelConverter, JTableConverter, etc. -- the user only needs to know about DispatchingComponentConverter. DispatchingConverter, the superclass of  DispatchingComponentConverter, contains the following method that configures a composite object:
/** Configure the composite object COMPOSITE with the converter for CLASSID, 
* using BASEKEY as the resource bundle key.
 */
void configureComposite(String classID, Object composite, String baseKey,
  ResourceBundle bundle, INoReturnMap argMap);
Let's say we have an existing split pane, splitPane, and we want to configure its properties. Then we would call
DispatchingComponentConverter.DEFAULT_INSTANCE.configureComposite(
"jsplitpane", splitPane, "theSplitPane", bundle, argMap);
The resource bundle backing bundle might look something like this:

theSplitPane.orientation=v
theSplitPane.left=%topPanel
theSplitPane.right=%bottomPanel
theSplitPane.border=%border

#topPanel, bottomPanel, border defined below
...

Composite configuration is most useful when you already have an instance of a custom subclass of a component. For example, the class com.taco.swinger.FontChooser, which comes with the developer's kit, extends JPanel, so that it can be put into any component hierarchy. But how does FontChooser determine what what its contents are? By composite configuration on the this pointer, of course! The configuration process does everything except create the panel, including setting the contents of the panel. Thus FontChooser does not need to create any GUI components manually, and the code in FontChooser mainly just keeps track of what fonts and colors are selected. A true MVC implementation!

Also, configuration of non-standard components can be performed. Suppose we have a custom subclass of JComponent, called JPalette. To configure the properties of an instance of JPalette, palette, we would write the following:
DispatchingComponentConverter.DEFAULT_INSTANCE.configureComposite(
"jcomponent", palette, "thePalette", bundle, argMap);
Of course, only the properties of JComponent will be set. It's up to the user to set the subclass-specific properties. Alternatively, the user can install a JPalette converter into DispatchingComponentConverter.

Integration with BeanShell

Though interpolation provides a way of references values from outside the current context, it is still not powerful enough for our needs. We don't have a converter for every single type of object, nor can we expect the user to pass in all non-supported objects. In addition, the behavior of an object is not always determined solely by its properties. Finally, we would like the ability to set properties based on conditions. To solve these problems, we need a way to specify, through strings, any programming task. In other words, we need scripting.

Our solution is to allow all converters to execute Java-like code using the BeanShell interpreter. The BeanShell language is nearly identical to Java, except it is much easier to use and has many additional features. There are four features of primary interest to users of text2gui:
  1. BeanShell can create Java objects and call methods of Java classes using the same syntax.
  2. Variables do not need to be declared.
  3. Types are automatically converted as necessary, eliminating the need for casting. Parameter and return types are optional.
  4. Methods are invoked reflectively, so you don't need to know the interface of an object before invoking one of its methods.
These four features make writing BeanShell scripts a snap for Java programmers. As of BeanShell 2.0, BeanShell claims to be completely compatible with the Java language, so there is no need for a Java programmer to learn anything in order to write BeanShell scripts. BeanShell is small, has been around for many years, and best of all, it's free. For those of you with a sense of nobility, it might be worth mentioning that a portion of the sales of the text2gui library will be donated to the BeanShell project. See the Free Software Donation Program for details.

Embedded BeanShell Syntax and Environment

Now that we have finished our sales pitch, it's back to describing how scripts can be embedded and executed by converters. For most converters, the following string syntax is required:

<{ ScriptContents }>

When a converter detects this syntax, it creates a new BeanShell interpreter, so that the environment of each script is independent. In the environment of the script,
most of the normally used classes in the java packages are imported automatically by BeanShell. See the BeanShell manual for a complete list. On top of these imports, converters also import into this environment the following classes:
Finally, the members of com.taco.text.GlobalUtilities and com.taco.swinger.SwingInvokeProxyFactory are statically imported (this is a Java 1.5 feature, but you don't need Java 1.5 since BeanShell does the importing, not Java).

The script environment is also set with two variables: bundle and argMap. They are the resource bundle and argument map passed into the uppermost call to either toObject() method of a converter.

For example, here is a text field that that contains the capitalized version of a name, passed in through the argument map:

textfield.text=<{ argMap.get("name").toUpperCase() }>

The above line may seem a bit mysterious, but it's really quite simple. The name key of the argument map is accessed, then its uppercased version is returned. Since BeanShell invokes methods reflectively, we don't need to cast the result of get() to String before calling toUpperCase(). Also, since the expression is the last one in the script, the return keyword and the final semicolon (';') are optional.

Instances of com.taco.text.AtomConverter (including instance converters) make the angle brackets ('<' and '>') that surround a script optional. As an example, whenever a component's listener list is specified, the instance converter is used to create each listener. Thus we can write:
closeButton.actionListeners.0={
new ActionListener() {
public void actionPerformed(ActionEvent event) {
  // Assume the dialog that this button belongs to
      // has global name "dialog".
  getGlobal("dialog", argMap).dispose();
    }  
  }
}
Creating listeners is probably the most common usage of BeanShell, but there are many more. AppBaseResourceBundle.properties, which comes with the text2gui developer kit, contains a script to create a menu that allows the user to set the look and feel. Because the available look and feels depend on the computer on which an application executes, the look and feel menu needs to be generated dynamically. But we don't want to put the burden on the non-GUI side of the application to pass the available look and feels to the GUI, so we use BeanShell on the GUI side to generate the look and feel menu.

Also, BeanShell is useful for the logic that maintains the state of the GUI. Global variables provide the storage of the state, but by themselves they do nothing. BeanShell allows methods to be defined that manipulate the state.

For example, TextEditorResourceBundle.properties, which also comes with the developer kit, uses a scripted class to maintain state, named State. An instance of State is put into the global namespace so that it is accessible. The subclass of State that the text editor uses contains an instance of javax.swing.undo.UndoManager. Whenever a change occurs, listeners notify the instance of State, which in turn updates the UndoManager. Based on the status of the UndoManager, the State then updates the enabled property of the Undo and Redo menu items. So the listeners of changes don't need to know anything about undoing changes -- they only need to call the setModified() method of State.

BeanShell Caveats

With BeanShell it is possible to perform almost every task that could have been done in Java, without having the burden of code compilation. Therefore, it's easy to get carried away with BeanShell, and not fully utilize the text2gui library's capabilities. Moreover, it's tempting to use BeanShell to define classes instead of using precompiled Java classes. In fact, if your really wanted to, you could define an entire component hierarchy using only BeanShell. But there are several caveats to keep in mind when using BeanShell:
BeanShell Usage Guidelines

Because of the caveats described above, here are some suggested guidelines for using BeanShell with text2gui:
Summary

This document supplied the real details that were missing from the Basics. By referring to this document, you can figure out how to create intricate component hierarchies with very little code. This is because the text2gui library tries to make GUI construction easy and intuitive, without hiding the power of Swing. Learning to use text2gui is not a substitute for learning Swing, but it can actually make Swing easier to learn, because intricate GUI constructions can be expressed quickly, and thus tested quickly.

Although it's easy to write text2gui code, it takes skill to write good text2gui code. Like any language, a certain amount of practice and experimentation is required before becoming proficient. But we're confident the time you spend learning text2gui will be well worth it -- after all the the amount of code you need to write is substantially reduced, and there is no need to compile text2gui code before you try it out. Also, writing text2gui code is much more fun than the drudgery of obeying the strict Java syntax.

From here, there are three possible directions to go towards: