Wednesday, December 15, 2010

The scala.swing package in 2.8 and beyond

Scala.swing is a Scala wrapper library for Java Swing and select AWT classes. For interoperability reasons, our wrappers are transparent in the sense that clients can get the underlying Java Swing component (the peer) of every wrapper. As a consequence, wrappers must not rely on additional state unless it is kept in sync with the peer's state.

Scala's swing API has undergone a few changes since Scala 2.7. Besides the obvious bugfixes, we added more wrappers, a more comprehensive component caching mechanism, and revised the window base hierarchy. I'll go into details of the most important changes in 2.8 below, assuming you are familiar with Java Swing and the basics of scala.swing.

Window hierarchy

This is how the Java AWT/Swing window hierarchy looks like:



While AWT's Frame and Dialog share the common base class Window, the Swing extensions have no subclass relationship or extend a common interface even though they share common functionality not present in AWT. In an attempt to straighten things up a little, we redesigned the window base hierarchy in scala.swing which now looks as follows:


I've put the corresponding peer type next to each class. Note the Mixin trait, which extracts common functionality of the Swing window classes. As of now, it contains the following methods:

def getJMenuBar: JMenuBar
def setJMenuBar(b: JMenuBar)
def setUndecorated(b: Boolean)
def setTitle(s: String)
def getTitle: String
def setResizable(b: Boolean)
def isResizable: Boolean

This design lets us factor common wrapper code into class RichWindow.

Dialogs

The Dialog companion object in 2.8 wraps message utilities from javax.swing.JOptionPane. Together with named and default arguments, dialog code becomes much more readable. Consider the following snippet from the Java Swing tutorials which opens a multiple choice dialog:

val options = Array[Object]("Yes, please", "No, thanks", "No eggs, no ham!")
JOptionPane.showOptionDialog(parent,
"Would you like some green eggs to go with that ham?",
"A Silly Question",
JOptionPane.YES_NO_CANCEL_OPTION,
JOptionPane.QUESTION_MESSAGE,
null,
options,
options(2))

It is a little tedious to remember the order and the meaning of those arguments. In particular, what is the title and what is the question message above? What is the parameter we set to null? In scala.swing, you can now write

Dialog.showOptions(parent,
message = "Would you like some green eggs to go with that ham?",
title = "A Silly Question",
messageType = Dialog.Message.Question,
optionType = Dialog.Options.YesNoCancel,
entries = Seq("Yes, please", "No, thanks", "No eggs, no ham!"),
initial = 2)

Now it's clear what's the title and what's the message. The null argument (which specifies the icon BTW) does not appear since the icon parameter is set to null by default. Note that named arguments are not only more descriptive but their order at call site is also arbitrary .

Events

Scala swing components publish key events now. As usual, the event classes are a straightforward adaption of the Java classes and can all be found in package scala.swing.event. Another important feature we were missing in 2.7 was the possibility to consume input events. Fortunately, that has been fixed now. Moreover, ticket #1442 taught us that some Swing components behave differently depending on whether certain listeners are installed or not. Therefore, scala.swing publishers install their Swing/AWT listeners lazily now. This means we install Java listeners only when a scala.swing reaction is added to the corresponding publisher and uninstall listeners when the reaction is removed. Consequently, we are little more efficient but more importantly avoid to spuriously trigger side-effects that Swing relies on in the first place (this is one of the few places where we actually do save additional state - the reactions - and by not rigorously synchronizing the wrapper's with the peer's state we got what we paid for).

Wrapper caches

The following is a change under the hood which most users won't notice but for those who are interested, here are a few details.

As I said earlier, we can get the peer of every wrapper. The other way works as well. Every peer is associated with a wrapper if scala.swing created one already and as long as it hasn't been reclaimed by the garbage collector. This backward association is established by a wrapper cache. So why do we cache wrappers in the first place? Note that duplicate wrappers for the same Java Swing component are not a problem for equality, since scala.swing.UIElements are instances of scala.Proxy and hence redirect equals and hashCode to their peer. The main reason we do cache components is that we don't have control over every creation site. Consider a scroll panel for an example. It internally creates a set of scroll bars that clients can obtain a handle to. In contrast to wrappers created by user code, we have no control over the creation of those components. Since we don't want to create a new wrapper every time the client obtains a handle, we store them in a cache of weak references (we might wanna use soft references in the future). Because of subtyping, there can be wrappers of different types for the same Java Swing component. Right now, we create wrappers based on the static type of a Java Swing component, which in some situations is not very useful. We might change that to a more dynamic approach in the future, that's why the caching API is still subject to change.

Scala 2.9 and beyond

The big news of this post is that we are opening up the development process for scala.swing. We are now using git for a distributed collaboration model. You can find the main repository on http://github.com/ingoem/scala-swing. The master branch is a two-way mirror of SVN trunk. The incub branch contains work in progress and welcomes your contributions. Once certain features in incub reach a stable state, we will promote them to the master branch and hence to SVN trunk which means they will eventually become part of the next Scala release. If you want to become a contributor, just go ahead and fork. We already have two new wrappers thanks to the excellent work of Andreas Flierl and Ken Scambler!

6 comments:

Manfred Schäfer said...

Good news. Is there some progress on the Tree Component?

siryc said...

That's interesting. Are you planning to add some GUI builders?

Pedro Furlanetto said...

This is great news! Thanks for scala.swing!

Ken Scambler said...

Great job Ingo, scala.swing keeps getting better!

Manfred -
The tree control already works nicely for many (most?) common use cases, which I've described here.

There's some tricky design decisions ahead around how best to handle mutable trees, inserting, updating, editing and so forth. I've been sidetracked the last month, but I'll get back to it in earnest after Christmas.

Feedback is always welcome -- I'll respond quickly to any needs you have in using it.

goddard said...

just an idea, but: would it be possible to create a scenegraph versionof swing? something like java scenario project on java.net. also, it would be nice to have a dsl like javafx for swing (scengraph, animations, effects).

HRJ said...

I always enjoy working with Scala Swing. Thanks!

I was wondering if the named and default arguments in Scala 2.8+ could be put to use in the Scala Swing API.

 
t>