"JSON → SWT shells" GUI Transformer tool for Java developers
Wish to see just the basics?
You use Baobab to convert JSON to create SWT beans using:
and then to configure them you use property setters. As usual, make sure the property you are setting value to is exposed in the means of setter method or public field.
Baobab uses type information to find out which converter to use to map the JSON node value to the target object.
If you wish to add children widgets to the current widget, or to know how to name the widget, or to define types you need to know everything about the special tags.
If you wish to use resource bundle messages, images or beans injections into SWT (or your custom) beans you should definitely take a look at how to implement providers.
After you have finished working with the user interface, you need to be able to use all those widgets and to react on their events, right? Take a look how I implemented events and widgets annotation-driven embedding.
Finally, starting from version 0.3.0 Java controller part can be made simple even further if you choose to utilize the Observed model approach which eliminates need for embedded components in most cases (still, you can continue to use embedded components in parallel if you wish to).
When the moment comes to combine all elements and run the application you need how to execute call to transformer and wiring it all together.
To create beans using this approach, you should use the special tag _type to note exact class you wish to instantiate:
{ "_type" : "org.eclipse.swt.widgets.Label", ..... }
This approach is used to make definition writing faster since it allows you to use short type identifiers instead of full class names.
{ "_type" : "label", ..... }
If you wish to see entire list of supported shortcuts out-of-the-box in Baobab, take a look at the
current version's knownShortcuts
field in
config
file.
If you wish to extend the list of the items to use some additional SWT classes or if you wish to
use shortcuts for your own widgets, you can create properties file <root>application.conf
like I did it in MovieCatalogSystem.
If you use this approach and wish to use Baobab's interactive editor for your project's GUI files,
just include the compiled output to the editor's classpath.
For some complex cases it is too exhausting to set all properties manually, especially if the widget appears too often. Builder pattern should help you with this problem by lowering the amount of text you need to enter to get the same result.
Notation is as follows:
[builderName](param1,param2...)
The param count is generic. Implementation of net.milanaleksic.baobab.builders.Builder<T>
has only one method create
that gets List of all parameters.
You can use builder notation in 2 different ways:
First approach is trivial - it lets builder do entire task for creating the object:
"someProperty" : "[myBuilder](param1,param2)"
But, in case you need to further customize the object created by the builder, use second approach:
"someProperty" : { "_type": "[myBuilder](param1,param2)", "param3" : "valueForParam3" }
Builder notation is used in Baobab to replace creation of the org.eclipse.swt.layout.GridData
object which can be quite cumbersome if you base you layout on it a lot. Using GridDataBuilder
you can replace following block of code:
"_type" : "composite", "layoutData" : { "_type" : "gridData", "horizontalAlignment" : "{center}", // what are these {???} things? Wait for Integer Converter part "verticalAlignment" : "{beginning}", "grabExcessHorizontalSpace" : true, "grabExcessVerticalSpace" : false },
with following shorter variant:
"_type" : "composite", "layoutData" : "[gridData](center,begin,true,false)"
This builder has variants of 4 and 6 parameters, where the 6-param version also accepts
horizontalSpan and verticalSpan properties to be set. But what if you have to set some additional
field for GridData, like heightHint
? Easy, use the object + builder notation approach:
"layoutData" : { "_type" : "[gridData](fill,fill,true,true,1,3)", "heightHint" : 180 },
First, you need to make your own implementations of interface net.milanaleksic.baobab.builders.Builder<T>
.
You need to modify your Typesafe Config file /application.conf
to include following
form of config pairs inside package net.milanaleksic.baobab.builders
:
myBuilderName=com.foo.MyBuilder myBuilderName2=com.foo.MyBuilder2 ...
The previous mapping will allow you to register (e.g.) a builder with name myBuilderName
which
is defined in the class com.foo.MyBuilder
. This builder will receive all parameters sent to it in
the GUI file. The response must be instance of net.milanaleksic.baobab.builders.BuilderContext<T>
which describes the built SWT component and its name. If you choose to return null
, it will not
be placed in the map on named components.
As you can see, extensibility of the builder notation allows you to create you own tiny DSL inside the GUI files for easier creation of GUI interface files.
Converters are critical part of any Baobab-driven application. JSON only recognizes Strings, booleans and number
values.
How can you possibly map a TabItem
? The trick is in converters - they are specialized
for converting a JSON node (which can be text, number, boolean, array or object node) into a specific
target type.
To detect target type, Baobab uses type introspection at runtime. For example, when it encounters
a field of type int
, it will use IntegerConverter
to convert the value
on the right side of JSON property to appropriate type - int. IntegerConverter is, though, more
powerful because it has magic value notation, which you can read in details later.
Currently supported converters (most of them are in package
net.milanaleksic.baobab.converters.typed
)
are:
BooleanConverter
ColorConverter
IntegerConverter
IntegerArrayConverter
FontConverter
ImageConverter
PointConverter
StringConverter
net.milanaleksic.baobab.converters.ObjectConverter
You need to modify your Typesafe Config file /application.conf
to include following
form of config pairs inside package net.milanaleksic.baobab.converters
:
com.foo.TargetType=com.foo.MyConverter com.foo.TargetType2=com.foo.MyConverter2 ...
The previous mapping will allow conversion of a JsonNode (Jackson is used for JSON processing)
to the type (e.g.) com.foo.TargetType
using an implementation of interface
net.milanaleksic.baobab.converters.Converter
or maybe (easier/cleaner for simple type
mapping) net.milanaleksic.baobab.converters.typed.TypedConverter
defined with the right
side of the property - com.foo.MyConverter
and so on.
This converter is the most basic one - it just converts the JSON native boolean value to Java's. Nothing else to it.
This converter can be used in 2 different ways:
{ "_type" : "label", "foreground" : "#0000ff", // HTML/CSS color definition: r:0, g:0, b:255 "background" : "color_list_foreground" // system color org.eclipse.swt.SWT.COLOR_LIST_FOREGROUND }
Allows not only exact integer value to be placed as property value, but also to use magic number notation to reference the colors from the SWT class (but in lower case). This approach can be combined with the special tag _style used to set the widget's style when creating instance.
Since it is quite often needed to use bitwise OR when declaring values (styles before all else), IntegerConverter also accepts OR operator:
{ "_type" : "text", "_style" : "{multi}|{wrap}|{v_scroll}|{border}" // SWT.MULTI | SWT.WRAP | SWT.V_SCROLL | SWT.BORDER }
The reason why this converter was made was the SashForm's weight property which demands array of integers.
{ "__comment" : "to set sashForm's weights you need to wait until components have been added", "_type" : "sashForm", "_style" : "{smooth}", "_children" : [ { "type" : "composite" // ... }, { "type" : "composite" // ... } ], "weights" : [ 2, 1 ] }
Since it uses (under the covers) the IntegerConverter, you can send magic constants, not only explicit numerical values.
This converter expects an object notation on the right side of the property with one of following properties:
name
- font name face,height
- height of the fontstyle
- it can be set to text values bold or italicFor whatever is not set, FontConverter will use values from default system font.
{ "_type" : "label", "font" : { "height" : 12, "style" : "bold", "name" : "Courier New" } }
This converter just delegates the image name to image provider to do whatever it wants with it.
For example, in MCS I have image provider implementation that uses the value as a full path to the location in application resources, from where it fetches it for Baobab to embed.
{ "_type" : "toolItem", "image" : "/net/milanaleksic/mcs/application/res/media.png" }
Very basic converter that uses simple string pattern x,y
as the value definition.
{ "_type" : "shell", "size" : "412,326" }
What ever string you enter on the right side of the property, it will be set in the bean's property.
If you put part of the text in the square brackets ([]), messageProvider will be asked for the value behind it. That should be, most logically, a String from the resourceBundle for the current application locale, but doesn't have to be.
{ "_type" : "label", "text" : "[delete.doYouReallyWishToDeleteMovie]" }
Object converter is the fallback converter. This means that when Baobab finds a field
which type is not directly registered in the list of converters, it goes to ObjectConverter
and says "Help!".
First thing ObjectConverter tries to do is to check if the object is maybe in injected object notation, which means that instead of creating it it needs to provide it. The injected object notation is:
"(objectName)"
Providing the object is two-phase process: first the named objects in the current context are browsed. You can use the special tag _name to give name to any bean. This is extremely useful when working on containers like TabFolder, where you first need to create container contents widget (Composite for example) and then create TabItem with the previously created widget as the control:
{ "_type" : "composite", "_name" : "settingsTab" }, { "_type" : "tabItem", "text" : "[settings.basicSettings]", "control" : "(settingsTab)" }
If none named bean is found ObjectProvider
is asked for the object under the name used.
If you implemented Object Provider to fetch the object from DI using the name, you can easily inject
a bean in custom SWT widgets.
{ "_type" : "myCustomComponent", "bundle" : "(resourceBundle)" // resourceBundle can be Spring bean for example! }
If the injected object notation is not used, ObjectConverter tries to render the value of the
property setter as JSON object and finds in it the special tag _type
to see which type of
object it needs to instantiate before proceeding with property setting (unless short children notation is used).
If you jumped over the first topic of the documentation - creation notations, now is a good time to
read it.
Special tags are used for situations when JSON limitations become apparent. In practice, special tags are only JSON node names which are not based on the property name in the target widget / bean.
This tag is used in object nodes to say which class/builder/shortcut to use to create the object. More details in the creation notations part.
What is quite specific for SWT widgets, controls etc. is that no-arg constructor does not exist. If you wish to create a new widget, you have to explicitly say what is the parent and what is the style (or style mix) you wish to enforce.
So, this tag was introduced to allow creation of "children" components by injecting the parent component while constructing the child. For example, if you wish to make shell with two labels you do it like this:
{ "_type" : "shell", "size" : "150,150", "layout" : { "_type" : "org.eclipse.swt.layout.RowLayout" }, "_children" : [ { "_type" : "label", "text" : "first label" }, { "_type" : "label", "text" : "second label" } ] }
To ease the pain of too much text, there is a short children syntax
which replaces array with JSON object notation. This shorter notation improves readability and lowers the GUI
definition file size by 15-25% in number of lines. As a side effect definition file is by far more fluent to
"follow the GUI tree". In practice, short children syntax collects information from _style
,
_name
and _type
special tags and sets them all at once in the JSON key name.
Following two examples are identical in what they are producing:
{ { "_type" : "shell", "_type" : "shell", "layout" : { "layout" : { "_type" : "gridLayout", "_type" : "gridLayout", "numColumns" : 1 "numColumns" : 1 }, }, "_children" : [ "_children" : { { "text(textName, {border}|{v_scroll})" : { "_type" : "text", "layoutData" : { "_style" : "{border}|{v_scroll}", "_type" : "[gridData](fill,fill,true,true)" "_name" : "textName", } "layoutData" : { }, "_type" : "[gridData](fill,fill,true,true)" "org.eclipse.swt.widgets.Label(labelName)" : { } "text" : "hello world!" }, } { } "_type" : "org.eclipse.swt.widgets.Label", } "_name" : "labelName", "text" : "hello world!" } ] }
As you can see, the syntax is type_syntax(object_name[,style_syntax]), which means that style parameter is not mandatory but name is. At the same time, any (valid) type node value is allowed to be placed in the type_syntax part (explicit class mention, shortcut notation and builder notation).
One bad thing with this approach is that you have to always give names to the components, otherwise (because of the nature of JSON), it will overwrite all previous nodes with the last one in case there is more than one node with the identical json key value (same combination type_syntax+object_name+style syntax) and thus create only one item. The good news is that you can use _* syntax which tells the Baobab not to push the widget name to transformation context (and thus not pollute it). If you wish to use this approach you need to have unique names only in once children collection (they don't have to be unique throughout entire definition file).
From this point on examples will use short children syntax so you can get used to it (I believe it is more human-friendly than the standard _type syntax).
Since style is set during the creation of the SWT widgets, normal setter does not exist. That was the reason for inclusion of this tag. Since style is of integer type, you are using IntegerConverter when setting the value, which gives you the freedom of using SWT's magic constants.
Name tag has a single usage - putting the widget in the TransformationContext. This context object is the one you get after calling transformation on the GUI file from the Transformer (Baobab entry point).
All possible names are allowed, as long as they don't take trailing spaces and comma. But, if you decide to name your component with a leading "low line" symbol (e.g. _label1 or _comp etc.) the widget will not be put in the output transformation context. This is done on purpose to allow "short children syntax".
After you have named some widget, you will be able to reference that object inside:
@EmbeddedEventListener
/@EmbeddedEventListeners
annotations,
@EmbeddedComponent
,net.milanaleksic.baobab.TransformationContext.getMappedObject(String name)
.
Examples for the reference and event embedding are given in the embedding part.
This is trivial special tag to be used if you have need to comment something in the GUI code. Proper JSON should not have JavaScript-styled comments so the only way to have a proper JSON is to use a custom JSON field which will be omitted by content parsers. So, just put it in any object definition and put in the value field whatever you wish - it will not be taken into consideration by Baobab.
"label(myLabel)" : { "__comment" : "This is a comment in the GUI file!" }
If I believe there is an extension point that you will really really like to use, there is a provider interface for it. You use your dependency injection framework to say "here is the implementor for the provider", and like magic you'll see our application messages, images from your resources or external location or even other beans from your DI framework showing up as possible candidates for wiring into Baobab definition JSONs.
Use your DI container to override
net.milanaleksic.baobab.providers.ResourceBundleProvider
with your implementation or just route it to SimpleResourceBundleProvider
to always use
messages_en.properties (or default messages.properties if first does not exist) as the source for resource
naming pattern for string properties. For example:
"label(myLabel)" : { "text" : "[string.from.bundle]" }
When you use named object syntax to ask from the DI for a named object in JSON definition,
you basically ask it from implementation of
net.milanaleksic.baobab.providers.ObjectProvider
you registered in the DI container.
You can though use AlwaysReturnNullObjectProvider
to always embed null when named object
is requested, for example:
"net.milanaleksic.application.MyCustomSWTComponent(coolName)" : { "someReallyCoolCustomObjectFromSpring" : "(customSpringObject)" }
Use your DI container to override net.milanaleksic.baobab.providers.ImageProvider
with your implementation or just route it to AlwaysReturnEmptyImageProvider to always embed dummy image
with the value of the JSON value node for the image setter.
Embedding is the process of wiring back all the components you created in the interface GUI files to the Java code. To be able to reference widgets (or other beans) you created in the GUI file they have to be named using special tag _name.
Java files that have identical name and package position as the GUI file are called managed forms
since you can embed components and event listeners directly using the
net.milanaleksic.baobab.Transformer.fillManagedForm(Shell parent, Object managedFormObject)
.
When Transformer analyzes the managedFormObject it will find all the fields with embedding annotations
and either inject values into them or attach listeners, depending on the annotation. It is
important to remember that Baobab expects the GUI file to be named exactly like the Java managedFormObject
from the call, just having .gui instead of java/class.
If you give your widget a name then you can include a field of type org.eclipse.swt.widgets.Listener
and annotate it with net.milanaleksic.baobab.EmbeddedEventListener
.
This annotation specializes the binding to make sure proper named component and event is listened to via
this field listener. There is even a shorter way, called "method level listeners", where you put the annotation
not on the field of type Listener but on a method, which removes a lot of clutter.
This annotation can be used on fields (should be but don't have to be private) in the form file.
If you wish to reuse a event listener method or field for diferent events, you can use
net.milanaleksic.baobab.EmbeddedEventListeners
to group more than one annotation
on a single listener.
So, if you have a field named "myLabel" in the GUI file:
"label(myLabel)" : { }
in the managed form Java file you just mark the appropriate field with the annotation:
@EmbeddedComponent private Label myLabel;
After transformation, you can just dereference the value in it - it will be injected by Baobab before that time or it will fail early while trying to do so.
To wire event listeners you also need just to reference the component with the name and then ask for a specific event to attach the listener to. Listener implementation fields can be but don't have to be static.
@EmbeddedEventListener(component="myLabel", event= SWT.Selection) private final Listener myLabelSelectionListener = new Listener() { public void handleEvent(Event event) { // handle event } }
Latter annotation (EmbeddedEventListeners)
is just a helper annotation in cases
where you want to use same listener implementation for multiple event sources for one or more
event sources:
@EmbeddedEventListeners({ @EmbeddedEventListener(component = "comboMediumType", event = SWT.Selection), @EmbeddedEventListener(component = "comboMediumType", event = SWT.Modify), @EmbeddedEventListener(component = "comboGenre", event = SWT.Selection) }) private final Listener complexListener = new Listener() { public void handleEvent(Event event) { // handle event } }
In case you think the creation of event listener objects is cumbersome - you are right. That is why starting from version 0.1.7 method-level listeners are allowed.
Candidates for method level listeners must fulfill following requirements:
@EmbeddedEventListener
or @EmbeddedEventListeners
,void
return type,org.eclipse.swt.widgets.Event
, or none
(the latter when you just don't need the event details).
Here is the method-level implementation of previous two examples:
@EmbeddedEventListener(component="myLabel", event= SWT.Selection) private void myLabelSelectionListener(Event event) { // handle event } @EmbeddedEventListeners({ @EmbeddedEventListener(component = "comboMediumType", event = SWT.Selection), @EmbeddedEventListener(component = "comboMediumType", event = SWT.Modify), @EmbeddedEventListener(component = "comboGenre", event = SWT.Selection) }) private void complexListener(Event event) { // handle event }
If a runtime exception is thrown in code executed by your listeners (SWTException is one of them),
your main SWT dispatch loop will either be prepared for it or face the consequences - immediate
program shutdown. How to deal with this issue and still avoid using try/catch in all
of your listener methods? By registering
net.milanaleksic.baobab.converters.MethodEventListenerExceptionHandler
in the Transformer
configuration.
If you do so, the exception will be caught in Transformer and delegated to this exception handler so your app does not face the problem in runtime. You are free to use appropriate mechanisms to handle the exception situation (message box / log / send email...). The fact to remember is: if you do not register a handler for exceptions Transformer will rethrow the exception and probably cause undesirable effects for your application UX.
Transformer should already have all of its dependencies wired before calling it using your dependency injection. Guice and Spring are the frameworks I used:
The code for wiring the form and GUI file uses the entry point to Baobab - Transformer
.
Calling the transformer is quite straight-forward:
@Inject private Transformer transformer; public showForm() throws TransformerException { net.milanaleksic.baobab.TransformationContext context = transformer.fillManagedForm(this); org.eclipse.swt.widgets.Shell shell = context.getRoot(); shell.open(); }
Now, what the call to will do is the following:
.gui
;
@EmbeddedComponents
and @TransformerModel
fields
(more in observed model part for @TransformerModel
) ;
@EmbeddedEventListener
annotations;TransformationContext
object which wraps the transformed shell and
all of the mapped objects and model binding metadata which is critical if you wish afterwards to
manually update SWT form based on current model state using call
transformer.updateFormFromModel(form, context)
.
You can access all the components in the UI either
by asking for them using their name (something similar to the Servlet getAttribute/getParameter approach)
using net.milanaleksic.baobab.TransformationContext.<*widgetClass*>getMappedObject(*widgetName*)
or using annotation net.milanaleksic.baobab.EmbeddedComponent
in your form class
to have the component injected for you (approach based on dependency injection pattern)
In my opinion, first approach is useful for the cases when you wish to execute a simple and small action, like setting a dynamic text in some component (e.g. web service version fetched from far away). Second approach is useful when you wish to maintain reference to the widget throughout the life of the form (e.g. you need to keep reference to username/password text widgets until the user clicks on OK).
Starting from Baobab v0.3.0 and with help of Cglib magic it is possible to simplify further the usage of Baobab using embedded model which wraps the binding between a standard Java object expressing the current state and the view presented to the user in the shape of SWT form. I believe the best way to show how to use it is via simple example.
Let us imagine a situation where a form called MovieDetailsForm uses Baobab for mapping the SWT form to a simple model object. This MovieDetailsForm can look something like this:
public class MovieDetailsForm { @EmbeddedComponent private Shell shell; @TransformerModel(observe = true) private MovieDetailsModel model; public void showDataForMovie(Shell parent, Movie movie) { model.fillFromMovie(movie); shell.setVisible(true); } }
There should not be any surprises in the form controller object described above besides the
@TransformerModel
. Let's see how this model looks like.
First of all, every model is a POJO which uses only annotations for customizations (if needed at all) and no inheritance or interface implementation. Here is a little more elaborate model to show different aspects:
package net.milanaleksic.mcs.application.gui.model; import com.google.common.collect.Sets; import net.milanaleksic.baobab.model.*; import net.milanaleksic.mcs.domain.model.*; import java.util.Set; public class MovieDetailsModel { private String comment; @TransformerProperty("selectedItems") private Iterable<String> tags; @TransformerProperty(component = "movieName", value = "text") private String name; public String getName() { return name; } public void setName(String name) { this.name = name; } public Iterable<String> getTags() { return tags; } public void setTags(Iterable<String> tags) { this.tags = tags; } public String getComment() { return comment; } public void setComment(String comment) { this.comment = comment; } @TransformerFireUpdate public void fillFromMovie(Movie theMovie) { final Set<String> tagNames = Sets.newLinkedHashSet(); for (Tag tag : theMovie.getTags()) { tagNames.add(tag.toString()); } tags = tagNames; comment = theMovie.getComment(); name = theMovie.getTitle(); } }
So, what model wrapping does is following: it goes through all of the fields which don't have the
annotation @net.milanaleksic.baobab.model.TransformerIgnoredProperty
. It finds the
named components for all of these fields and maps the text
property of them
to the value of the property in the model. This means that modifying the value in the model and then asking the
Baobab toolkit to update form based on model will force the value of the SWT component to be updated to this value.
At the same time Baobab will update the model field when SWT component's text value gets modified using dynamically
created event listener for SWT.Modify event.
Both default choices SWT.Modify
trigger and the bound property text
can be modified
using customization annotation @TransformerProperty
.
The hardest part with working with object is to understand that there are different ways to update form when after updating the model:
@TransformerModel
's attribute
observe
to true, Baobab will create Cglib-driven listeners for all setters of property-bound fields.
Property-bound fields are all the fields in model that don't have @TransformerIgnoredProperty
.
This listener will execute immediately after the setter finishes with execution;
@TransformerModel
's attribute
observe
to true, Baobab allows you to update more than one field in a single method to maintain
UX consistency using @net.milanaleksic.baobab.model.TransformerFireUpdate
.
If Baobab finds a method in the model object annotated with this annotation it will create additional Cglib-driven
listener which will execute the form update after this method has finished executing;
observe
attribute
set to true - you can execute it whenever from the form controller. It is based on the Transformer's
call transformer.updateFormFromModel(form, context)
. Please note that to be able to use this call
you need to maintain the model binding metadata by yourself (you receive it as part the context object which
is result of the Baobab transformation call).