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:
- Implementations of IInterpolatingConverter
that are intended to convert to primitive types wrap their return
values in Object types like Boolean, Integer, etc. Their results usually
need to be unwrapped by the caller.
- The result of conversion must usually be cast to the desired
type.
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:
- Argument map interpolation
- 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:
- If baseKey is
assigned to a value in bundle,
set val to that value.
- If val is a
string, perform string to object conversion to convert val, and return the result.
- Otherwise, return val
immediately.
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:
- If baseKey is
assigned to a value in bundle,
set val to that value.
- If val is a
string, perform string to object conversion to convert val, and return the result.
- Otherwise, return val
immediately.
- 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.)
- For each ordinary property with name prop:
- Determine the converter associated with prop. For example, the
converter associated with the foreground
property of a Swing component is the color converter.
- 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.
- Set the property of the composite object with the value
determined in step ii.
- 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:
- If baseKey is
assigned to a value in bundle,
set val to that value.
- If val is a
string, perform string to object conversion to convert val, and return the result.
- Otherwise, return val
immediately.
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:
- The component converter (com.taco.swinger.text2gui.DispatchingComponentConverter)
- The layout converter (com.taco.swinger.text2gui.DispatchingLayoutConverter)
- 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:
- BeanShell can create Java objects and call methods of Java
classes using the same syntax.
- Variables do not need to be declared.
- Types are automatically converted as necessary, eliminating the
need for casting. Parameter and return types
are optional.
- 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:
- In package com.taco.data
- NoReturnMapAdapter
- ObservableMap
- DelayedResultObservableMap
- In package com.taco.swinger.text2gui
- DispatchingComponentConverter
- In package com.taco.text
- In package java.beans
- PropertyChangeEvent
- PropertyChangeListener
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:
- Since BeanShell scripts are interpreted and every method is
reflectively
invoked, scripts execute much slower than ordinary Java. The
definition of
subclasses requires both interpretation and byte code generation, which
is even slower.
- BeanShell syntax errors don't occur until runtime, and at that
point the
errors BeanShell reports are not as helpful as compile-time errors or
runtime errors caused by ordinary Java code. The line numbers BeanShell
reports are relative to the start of each script, not the properties file in which the
script is defined. These factors make BeanShell scripts harder to
debug than ordinary Java code.
- BeanShell is sensitive to the current directory when importing
classes. It might not be able to import a class if you run the
application in a directory containing .class files with the same name.
- The definition of subclasses is new to BeanShell, so there are
still some bugs in the implementation. In particular, with a class
hierarchy of more than 2 classes, the super keyword doesn't work
properly. See the bug
report.
- BeanShell won't work in most applet environments, due to its
reliance on reflection which is a security risk.
- BeanShell scripts are capable of doing anything Java is,
including wiping out the entire file system.
BeanShell Usage Guidelines
Because of the caveats described above, here are some suggested
guidelines for using BeanShell with text2gui:
- Use BeanShell scripts only for simple, quick tasks and "glue"
logic. Complicated, computationally expensive tasks should be done with
pre-compiled Java code.
- Debug and run your application either from the root of the
classpath for your project, or in a directory that contains no .class
files in it or any of its subdirectories.
- Whenever possible, avoid extending classes with BeanShell.
Implement interfaces from scratch instead. For example, don't extend javax.swing.AbstractAction.
Implement
java.awt.event.ActionListener instead, and add the scripted
listener to the list of action listeners instead of setting the action
of a button.
- Avoid BeanShell altogether when creating applets unless you
require that the user accept the security risks (typically by asking
the user to grant all priviledges to a signed applet). The tiny jar
file bshfacade.jar, supplied in the developer's kit, will allow
text2gui to execute without the full BeanShell jar file.
- If third-party properties
files are supported (in order to let the user change the look of the
application), the user must be made aware of the security risks. Or, a
security manager can be installed to deny scripts access to critical
resources.
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: