Wednesday, July 18, 2007

About SwingBuilder

Disclamer: I will use the term "closure" quite often here and experts will say they are not closures. I still call them closure in the sense, that they are instances of groovy.lang.Closure. So If I say closure I don't mean that functional thing ;)

This time I thought I should write some things about Groovy SwingBuilder and assumptions people seem to make about it.

groovy.util.BuilderSupport

First thing you need to know is that SwingBuilder is a builder... that might be obvious, but it implies, that if I do a method call in the builder structure, then the builder will handle that call and map the method names to certain actions.

Now in Groovy we have this class BuilderSupport, that you can use to map structures in a builder. Personally I don't like that class much, because the logic looks more complicated than needed, but it fits very general cases. Anyway, the class tries to map method calls in the builder structure to calls of createNode in the builder class. There are several of them, each responsible for a certain case controlled by your method call. The most important fact here is that if your last argument is a closure, then this closure will not be part of the creatNode call, instead the closure will be used by the builder directly. I guess it is best to show examples:

def builder = new MyBuilder()
builder.start {
methodWithClosure {
methodWithMap(foo:"bar")
}
methodWithNormalArgument("I am a argument I guess")
}
methodWithClosure a normal method call with one argument, that is the closure containing the method call with methodWithMap. methodWithClosure is now mapped to createNode(Object), the object there is the method name "methodWithClosure" as String. methodWithMap is mapped to createNode(Object,Map), where the first is again the method name and the map is our [foo:"bar"]. If you combine one normal argument and a map entries, then you get createNode(Object,Map,Object), where the last one contains your normal argument. And if there is no map and no closure, just a normal parameter, like with methodWithNormalArgument, then createNode(Object,Object) will be called. This logic supports only 1(!) normal argument, but that is enough in general.

After the createNode call of your choice is made the return value of that will be hold, I will call this currentNode. To connect the currentNode and its parent, which is done by setParent(currentNode,parentNode). now what is parentNode? Remember? we still have a closure to call. When we do, then our currentNode becomes parentNode and the new currentNode will have a parent. So the first time setParent is called we are not in a closure that belongs to the builder, which means the parentNode is null. If you would build a tree using this logic, then you would build the tree starting with the root and then adding node by node in I think it is called preorder traversal.

Architecture of SwingBuilder

SwingBuilder is making use of these methods in BuilderSupport. For each method call SwingBuilder creates a new instance of a bean we specified with the method call. So
frame(title:"I am a JFrame")
will create a new JFrame instance and set the property title. And as we just learned
frame(title:"I am a JFrame"){
label(text:"I am a JLabel")
}
will also create the JFrame, the property title will be set again, then the closure will be executed causing the label method to create a JLabel and the text property on that label is set. After that setParent is called with the first parameter being the JLabel and the second parameter being the JFrame.

The logic we stored in setParent will connect our frame with the label by frame.getContentPane().add(label). SwingBuilder#setParent knows several cases and handles adding a JMenuBar to a JFrame different from adding a JLable. This method and helper are around 100 lines, about 20% of SwingBuilder source.

So basically SwingBuilder is a builder that maps method calls to bean creation actions, using map arguments to init the beans and the closures to connect the created beans. It using a mapping method name -> bean class and contains itself nearly no methods you call when using SwingBuilder.

Names supported by SwingBuilder

If you are not sure if SwingBuilder supports a swing widget, just remove the J, keep the next letter in lower case and try it. For example JEditorPane becomes editorPane, JSplitPane becomes splitPane (both supported). But SwingBuilder does not only know widgets, it does also know layouts. there is usually no 'J', so just use the next letter in lower case, as in gridBagLayout, flowLayout or others. You can use the layout as normal method causing the layout property of the container to be set. I have often seen code like:
frame(layout:new FlowLayout()) {
label(text:"1")
label(text:"2")
}
but you can write that also as
frame() {
flowLayout()
label(text:"1")
label(text:"2")
}
I like this version much better, because you do not need to import FlowLayout and can give the layout some options while keeping the frame call simple. Groovy supports all the normal layouts, even Box layout. Another special thing is maybe the method gbc, which is the same as the method gridBagCosntraints, which maps to GridBagConstraints. Maybe I should also mention TableLayout, which tries to implement the layout you know from the table tag in html. It needs tr and td calls to place the componentes... really just like in html. Take a look at alpahbetic widget list to get an ideas what you can do.

Another important link is extending SwingBuilder. It does not mention the possibility of simply subclassing the class SwingBuilder, but that should be obvious and was done for example by SwingXBuilder.

All in all it is a bit difficult to provide a documentation for SwingBuilder, because you still need to learn swing, SwingBuilder doesn't help you with that. And then it is just connecting instances of classes... For example when people ask how to attach an action to a JButton and I tell them to assign a closurey to the actionPerformed property, then I am not talking about a special property, actionPerformed is defined by the bean specification and assigning a closure to that property is a normal thing in Groovy.

New things in 1.1-beta2

We got complains that if I do
def frame = swing.frame(...) {
...
}
frame.pack()
frame.visible = true
that the resulting gui will not be constructed in the EDT thread, but in the normal main thread. Now I am no Swing expert and I always assumed it makes no difference, but it seems that future changes in Java will need you to change in the EDT. And of course it is more clean that way too. So we eneded with adding two methods, the first is edt, which causes the attached closure to be executed while in EDT. the code looks then like
swing.edt {
def frame = frame(...) {
...
}
frame.pack()
frame.visible = true
}
unlike many other methods available in SwingBuilder the edt method is a real method and no registered widget. the other method is static and called build. It will automatically create a new SwingBuilder instance and call the attached closure with that instance as parameter
SwingBuilder.build {
def frame = frame(...) {
...
}
frame.pack()
frame.visible = true
}
build uses the edt method, so we build the GUI while in the EDT thread, just like before.

Future Plans

I think SwingBuilder is already a nice piece of work, but its evolution might not stop here. Currently I am thinking about integrating a Binding framework. That would some update logic to SwingBuilder, something you have to do all by yourself atm. For example imagine a label and a button and each time you press the button the label text should show a higher number. What do you do? You use a closure as actionPerformed for your button that increases a number and sets a new text for the label...
import groovy.swing.*
import groovy.swing.impl.*;
import javax.swing.border.EmptyBorder
import javax.swing.WindowConstants

def numClicks = 0
def state = {"Number of button clicks: $numClicks"}
def label
SwingBuilder.build {
def frame = frame (
title: "SwingBuilder Label Update",
defaultCloseOperation: WindowConstants.EXIT_ON_CLOSE
){
panel (border: new EmptyBorder(30, 30, 30, 30)) {
gridLayout(rows: 2, columns: 1, vgap: 10)
button (text: "I'm a button!",
mnemonic: "I",
actionPerformed: {numClicks ++; label.text = state()})
label = label (text: state())
}
}
frame.pack()
frame.visible = true
}
note the closure state, that is called at different places? that's quite ugly I think. With a binding framework the code might become
import groovy.swing.*
import groovy.swing.impl.*;
import javax.swing.border.EmptyBorder
import javax.swing.WindowConstants

def model = new BindModel(numClicks:0)

SwingBuilder.build {
def frame = frame (
title: "Binding and SwingBuilder Test",
defaultCloseOperation: WindowConstants.EXIT_ON_CLOSE
){
panel (border: new EmptyBorder(30, 30, 30, 30)) {
gridLayout(rows: 2, columns: 1, vgap: 10)
button (text: "I'm a button!",
mnemonic: "I",
actionPerformed: {model.numClicks ++})
label (text: bind("Number of button clicks: $model.numClicks"))
}
}
frame.pack()
frame.visible = true
}
as you see the need to keep an external closure vanished along with the need to keep a reference to the label. the way it will be used in the end is not yet sure, this is just a sketch based on a simple implementation I made.

While doing my "research" in this area I noticed that binding frameworks are not that well known. At last by the people I know. I myself didn't here about that before, but I am usually not doing much with swing. So I got a bit puzzled why people don't know these things if they can save so much code... but then I saw it. Some of these frameworks are producing rather cryptic code, that makes sense for the framework but is just plain hard to read. Your former models are now hidden in abstract constructs, but you still need to connect the things. So I guess SwingBuilder could find a more "natural" way by hiding all these things.

We will see.

5 comments:

akochnev said...

Jochen,
i'm just wondering, how do you make the fancy "code blocks" in your posting, i was never able to figure out how to do them on blogger.com..

Jochen "blackdrag" Theodorou said...

"fancy code blocks" eh? *g* I have a javascript added to my page that will rewrite all "pre" blocks with what you see. I used the script from http://codepress.org/ as a base, but I modified it very much to fit my needs.

fils said...

Can applications built with groovy and swing builder be jar'd distributed via web start (jnlp)?

Jochen "blackdrag" Theodorou said...

Hi fils,

yes, it is possible, but you need to allow reflection and the creation of classloaders. Besides that it is a normal java application... with some fancy stuff ;)

Martin said...

Hi Jochen,

finde gerade keine gültige Email-Adresse, deswegen über den blog - falls Du bis 96 am TG in GP warst, melde Dich bitte kurz per email bei mir - falls ich den falschen hab: sorry und bitte Kommentar einfach löschen ;)