Once again I had to write one of these stupid and boring GUIs. You know these things asking you stupid questions and showing irrelevant Data. Of course I do all in Groovy, no need for a IDE provided gui generation tool. So I used the SwingBuilder to make the GUI.
Really, I understand why people are asking for documentation. The 2 examples here are giving good examples, but basically you have to understand that components are registered and each property can be used using the map syntax. Embedding one component in another works using Closures. but there are things the Swing builder currently can't do. For example registering a MouseListener at a component using Closures as the functions executed by the MouseListener. Or a WindowsAdapter with Closures for certain action, like closing the window. So you still have to create a big bunch of useless classes. That's not nice. Ok, they are not that useless, because you can reuse them later.
We (the Groovy Team) had the idea to convert a closure to a class implementing a certain interface with just one method by the MetaClass or using the as-syntax. Just only that this won't help here, a listener normally has more than one method. So for now, I decided to make the helper classes, but maybe we should simply add an method to the Swingbuilder allowing to do such things. Would be possible and not that difficult I guess.
What puzzled me a bit was, that I have to do things like:
scrollpane {
viewport {
list()
}
}
Now I know a viewport is inside the ScrollPane, but that I have to do that explicitly isn't very nice. In fact I would like to have a default property and then be abel to do that:
list (scrollpane:true)
or at last
scrollpane {
list()
}
Anyway, it doesn't bother me too much.
So I wrote some glue classes like this:
class ClosureWindowClosingAdapter extends WindowAdapter {
def closure
void windowClosing(WindowEvent e) {
closure(e)
}
}
not much logic there, but it allows me to do things like that:
gui.addWindowListener (
new ClosureWindowClosingAdapter(closure:{System.exit(0)})
)
After I have done all these little things I got a nice small gui application. Just a few lines long itself, some minor helper classes and yet no data.
Yes, that's the point where
db4o comes to play with us. I had many lists basically showing all available instances of one type. And I never understood why I should make a custom model here to keep multiple lists of the same objects, maybe concat these lists so they can notify each other about changes and such.
I decided I want to use a central data holder this time, the objects are extracted from there using query-by-example and, no, the data holder is not a relational database with hibernate in the foreground. I wanted to have something easy, that allows me to change the class structure and doesn't requires as much overhead as hibernate. So I used one of my helper classes and wired some db4o parts in:
class ClosureQueryModel extends AbstractListModel implements ComboBoxModel{
def cl
def getElementAt(int i){cl()[i]}
int getSize(){cl(query).size()}
def selectedItem
def addElement(foo){/* ignore */}
def remove(foo){/* ignore */}
}
combo.setModel(new ClosureQueryModel(cl:closure))
nice isn't it? Again a predefined class would make life more easy here, but well, it isn't that this helper is a big one ;) and yes, I am ignoring removal and additon of elements, since I wanted to make that completely through the database. cl(query)[i] means I need a list or array or something like that as result. Now in db40 4.3 this would have been a problem, since a query on the database does return an instance of ObjectSet, which is no such list or array. But they changed that, and in 5.2, which means I can directly use the result of the query.
For all people here not knowing how such an query looks like:
def objectSet = db.get(Foo)
yes, that's all. Foo is the class of the objects I want to have and the above query allows me to get all instances from the database. So I would create a ClosureQueryModel isntance like that:
new ClosureQueryModel(cl:{db.get(Foo)})
So now my items would be displayed... but... there is no initial data. What is missing is a functionality to add remove and update items. Well, maybe I got here a bit over the edge, because I got this crazy idea of using a mapping to show the editable fields... but on the other hand, any bean editor is allowing comparable to that. Only that I don't use explicit beans and only that I don't use these extra files for beans describing the bean. Anyway, I made a map like this one:
[name:"field",view:"Feld",collection:{it.availableFields}]
to set a property/field named field, with the display name Feld and it is chooseable from a collection, that is returned by a closure {it.availableFields}. so basically I can choose here between predefined values returned by a property or whatver I use in the closure. then I thought that a simple collection is possibly not enough. What about numbers? so I added a "create" to the map, taking a closure that produces the object:
[name:"workload",view:"Auslastung in %",create:{new BigDecimal(it)}]
create and collection would be exclusive then, but I added no test for that.
I thought, ok, the let us have a popup menu, for the crud operations and put all that logic together in a method.
addPopup(
component:jlist,
producer:WorkloadEntry.class,
maping:[
[name:"workload",view:"Auslastung in %",create:{new BigDecimal(it)}] ,
[name:"field",view:"Feld",collection:{it.availableFields}]
],
newAction:{db.set(it)},
updateAction:{db.set(it)},
deleteAction:{db.delete(it)})
}
producer is the class use to create new instances from, mapping contains the fields/properties set for the new class and how they are displayed, and the *Actions are he database methods for the crud operations. Pretty easy, but the mapping tends to be big for man fields. I should think of something different here.
I don't want to show all of the code of that method here. The method itself is 100 lines long, as much as the rest of the program. A sure sign, that it is too complicated. But well, the basic parts:
I need to display the views:
args.maping.each { item ->
label(item.view+":",constraints:gbc1)
if (item.collection != null) {
def combo = comboBox(constraints:gbc1)
combo.actionPerformed = {query.setProperty(item.name, combo.selectedItem)}
combo.setModel(new ClosureQueryModel(query,item.collection))
} else if (item.model){
def combo = comboBox(constraints:gbc1)
combo.actionPerformed = {query.setProperty(item.name, combo.selectedItem)}
combo.setModel(new ChoiceModel(item.model))
} else {
selection << textField(constraints:gbc1)
}
}
saving it would look like:
args.maping.eachWithIndex { values,index ->
def sel = getViewComponent(index)
if (sel instanceof JComboBox){
obj.setProperty(values.name, sel.selectedItem)
} else {
def val = sel.text
if(values.create!=null) val = values.create.call(val)
obj.setProperty(values.name, val)
}
}
[...]
if (args.newAction!=null) args.newAction.call(obj)
args.component.model.addElement(obj)
args.component.model.fireContentsChanged(editWindow, args.component.selectedIndex, args.component.selectedIndex)
You can see the create closure here, called if set and if a JTextField was used to display it. Maybe giving the component instead of the text only would be also a improvement and would allow other components. But this was more of a case study, not a general purpose solution.
Even without the usage of the database, such a component should be thought through. I mean you need something like that quite often.
Conclusion:Groovy's Swing part currently consists only of the SwingBuilder. That helps a lot creating guis, but when it comes to models and listeners, the SwingBuilder lacks a huge amount of them. Models for spinner and tables are there, but not more. I think, developing Swing with such components can improve your productivity when it comes to make a prototype. You can alaways decide to change the class model and you can always change the gui itself. You could even think about a text, that you enter when a button is pressed and then that text is executed as script. Of course the combination with an plugin such as the eclipse groovy plugin would make things even more nice. But even without such self hosting features, it makes much more fun than plain Java, even if a WYSIWYG editor is involved. If I had more time for Groovy I would no go on and improve the Swing abilities of Groovy big time.. but maybe there is someone else who wants to do that. Groovy is open source and people doing such things are always welcome.
I hope I won't have to say that last part too often. Of course I would like to do all the ideas I have by myself, but I can only spend a few hours every week on Groovy and that is not quite enough to get issues fixed and to add new ideas.