Global Variables
Introduction
To significantly expand the expressive power of text2gui, objects can
be put and retrieved from a global namespace shared by the
model (Java code) and the view/controller (resource bundle). This has
the following benefits:
- Provides "state" to the view/controller, so that actions and
listeners implemented as scripts can reference previously created
objects which are actively displaying things or gathering user input.
- Object allocation can be reduced by sharing instances. (Hopefully
these instances are never modified!)
- Allows the model to access objects created by the view. This
violates the MVC principle, but can be useful.
Putting Globals
Via Resource Bundle Key
To
put an object converted from a resource bundle key into the globals,
just set its
globalName subkey like
this:
textArea.globalName=ta
textArea.text=Mary had a little
indigestion.
which maps ta to the text
area in the global namespace. The created object doesn't have to be a
composite for this technique to work.
Via String
If you are describing an entire object with a single string, just
prefix the string with an ampersand ('&'), the name of the
global, followed by a colon (':'):
panel1.contents=[{&fchooser:jfilechooser
approveButtonText="Nuke" multiSelectionEnabled=true}]
maps fchooser to
the file chooser in the global namespace.
Via Script or Java Code
Inside a BeanShell script, a global can be put by calling putGlobal().
angleModePanel.preInit={
putGlobal("abg", new
ButtonGroup(),
argMap);
}
The static utility class com.taco.text.GlobalUtilities
is statically imported to any BeanShell script (assuming
you are using BeanShell 2.0 or above), so you
can refer to
getGlobal() and putGlobal() without any
qualifiers. Also recall that the argMap
variable is
automatically set to the argument map in any BeanShell script used by
the tex2gui library. It needs to be passed to putGlobal() since the globals
are stored in the argument map.
A model (written in ordinary Java) can put a global like
this:
import
com.taco.text.GlobalUtilities;
...
GlobalUtilities.putGlobal("model",
this, argMap);
puts this instance as the value of the global variable model. Although, strictly
speaking this violates the MVC principle, this mechanism can be useful
if you want to allow the view / controller to call methods of the model
directly. For example, a "Save" button might be defined as follows:
saveButton.text=Save
saveButton.actionListeners.0={
new ActionListener() {
public void actionPerformed(ActionEvent event) {
getGlobal("model",
argMap).save();
}
}
}
putGlobal() never throws
an exception. putGlobal()
returns true iff the global was put successfully.
Retrieving Globals
Global variable values are stored in the argument map, so they can be
retrieved by both the model and the view/controller. If a global has
not been defined, retrieving the global will result in getting null.
Via String
To reference a global, just precede the name of the global by '&':
angleRadioButton.buttonGroup=&abg
sets the button group of the angle radio button to the global named abg.
panel.contents=[%okButton, {strut
length=&len}, %cancelButton]
creates a panel with a horizontal strut between the OK and cancel
buttons. The strut has length given by the global len.
Via Script or Java code
The syntax may differ slightly since BeanShell is loosely typed, but
the basic mechanism for retrieving a global is the same: calling getGlobal() in the class com.taco.text.GlobalUtilities. In a
script inside a resource bundle describing the view, you would write:
clearButton.action={
new
AbstractAction() {
public
void actionPerformed(ActionEvent e) {
textArea = getGlobal("ta", argMap)
textArea.setText("");
}
}
}
because all methods of GlobalUtilities
are statically imported into the BeanShell environment.
A model (written in ordinary Java) can retrieve a global like
this:
import
com.taco.text.GlobalUtilities;
...
argMap.putNoReturn("clearAction",
new
AbstractAction() {
public void
actionPerformed(ActionEvent e) {
JTextArea
textArea = (JTextArea) GlobalUtilities.getGlobal("ta", argMap);
textArea.setText("");
}
});
which puts an action into the argument map, which the clear
button would use. Note that this violates the MVC principle since the
model now assumes implementation details of the view/controller.
Retrieving globals can be a useful technique when the text2gui library
is only used for the construction of GUI's, not for the control. See The Use of Globals for Non-MVC Implementations
below.
Execution Order of
Global Creation
Globals created via strings or resource bundle keys are created only
when then the string or resource bundle key is converted to an object.
This means you cannot assume that a global has been set unless you know
that the associated string or resource bundle key has already been
read. For example, you might be tempted to write something like this:
commonBorder.globalName=cb
commonBorder=etched type=raised
highlight=0xffff shadow=0x808080
panel1.border=&cb
panel1.contents=[%panel2, {jlabel
text="Age:"}, {strut length=20}, %spinner}]
spinner.value=65
panel2.border=&cb
panel2.contents=[{jtextfield
text="Yo"}]
then in your Java code, convert the resource bundle key panel1 to an instance of JPanel:
JPanel panel = (JPanel)
DispatchingComponentConverter.toComponent(bundle, "panel1",
argMap);
However, the border of the returned panel won't be set to an etched
border. It will be set to null
instead, because the global cb
was never explicitly set; the resource bundle key commonBorder was never read.
To fix the example, we need to change the border line for panel1:
panel1.border=%commonBorder
Then when panel1 is read,
the border will be created from the resource bundle key. At that point,
cb will be set to
reference to the border.
Recall the order of composite creation is preorder.
This means in the above
example, panel1
is created first. According to the Component
Converters table, the border of a JPanel
is created before its contents. So the border is created, then panel2, then the text field,
then the age label, then the strut, and finally the spinner. That means
the line
panel2.border=&cb
is safe because cb will
have already been set to the etched border by the time the creation
process for panel2
begins.
In practice, this problem is not as complicated as it seems. Just be
sure that a global is set in the container of a child that uses it.
Setting the global in the instance
or preInit subkeys ensures
that the global is available before the child is created (see Special Properties of Composites).
Global Composites
Recall the steps involved in constructing a composite object:
- 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.
- 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.
If a composite is assigned to a global variable, the global variable
referencing the composite will be set after step 2. This means that the
global variable will be available when the composite's ordinary
properties are being created.
Avoiding Confusion
An easy way to eliminate all confusion as to when globals are ready to
be used is to set all globals before the main component is created.
This can be done with the instance collection converter. Let's say you
want to share a font and a border throughout your GUI. You don't want
to worry about creation order, so you create a resource bundle as
follows:
globals.0=b:{
return new
LineBorder(Color.GREEN);
}
globals.1=f:{
return new
Font("Monospaced", Font.PLAIN, 15);
}
textArea.border=&b
textArea.font=&f
...
Because globals key will
be read with an instance collection converter, each element must
conform to the syntax for instances, not for borders or fonts. Now to
create the text area in the Java code for the model, you need two steps:
CollectionConverter.INSTANCE_COLLECTION_CONVERTER.toObject(bundle,
"globals", argMap, null);
JTextArea = (JTextArea)
DispatchingComponentConverter.toComponent(bundle, "textArea",
argMap);
The first line reads the globals
key and creates all the instances.
Since the instances both have their globalName property set, the
globals b and f will be available by the time
the first line returns. The return value is not needed. The second line
can then create a text area that has access to b and f, with no worries.
The Use of Globals
for Non-MVC Implementations
One
technique for constructing a GUI using text2gui is to put only
initial values in the argument map. All of the components you wish to
have control over in your application can be put into the global
namespace during conversion. Then your application can retrieve all
of these components and manipulate them as desired. Here is an example
resource bundle:
panel.layout=border
panel.contents=[%scrollPane,
{%buttonPanel, {south}}]
scrollPane.viewportView=%list
list.globalName=names
list.listData=["Jeff", "Jim",
"Lily", "Helen", "James"]
buttonPanel.layout=box.axis=x
buttonPanel.contents=[%okButton,
{strut}, %cancelButton]
okButton.globalName=ok
okButton.text=OK
cancelButton.globalName=cancel
cancelButton.text=Cancel
An application could create the panel, then retrieve the components it
needs from the global namespace with the following code:
// An argument map is required to store globals.
JPanel panel = (JPanel) DispatchingComponentConverter.toComponent(bundle,
"panel", argMap);
JList list = (JList) GlobalUtilities.getGlobal("names", argMap);
JButton okButton = (JButton) GlobalUtilities.getGlobal("ok", argMap);
JButton cancelButton = (JButton) GlobalUtilities.getGlobal("cancel", argMap);
list.setSelectedIndex(2);
cancelButton.setEnabled(false);
// other manipulation of the list and buttons here
// you may want to store them in class fields for use by other methods
...
This is frequently
the easiest way to convert existing GUI's to use the text2gui library.
However, this violates the MVC principle and doesn't make the code much
better -- this method only eliminates the GUI construction code and
makes the GUI ready for localization. See The MVC Architecture and Argument Maps
for information on how to do better.