Using Swing-Selectors to split up layout-style-logic in UIs with Groovy

Post date: Oct 05, 2015 9:13:50 PM

Introducing Swing-Selectors

Swing-Selectors is a project I've been working on that takes some pretty nice code from the Automaton testing framework (don't worry, it's not stealing, I wrote most of it) and makes it available as a separate library, which makes it easy to, as the name says, select items from a Swing UI.

This code is at the core of Automaton's Swinger and lets its users do things like click on something that has the text "click me", or drag the component with name "foo" to "bar", for example.

Making it a separate library is a good idea, I think, because Swing developers might want to use the selection mechanisms outside of the testing context, much like web developers use JQuery to select DOM elements (Automaton supports JavaFX selectors as well, but most of this functionality to select nodes is already provided by JavaFX out-of-box).

For example, here's how you can select all JLabels in a Swing UI and set their foregrounds to a default Color:

final S = new SwingSelector( root: frame )

S.selectAllWithType( JLabel ).each { label ->

   label.foreground = defaultForeground

}

The above code assumes there's a frame variable (a JFrame, the root of the UI) and a defaultForeground Color. The rest should be obvious...

It is also possible to select a single Component to, for example, change its font and foreground:

( S.selectWithName( 'query-field' ) as JTextArea ).with {

   font = new Font( 'sans-serif', Font.PLAIN, 14 )

   foreground = new Color( 55, 209, 55 )

}

The casting to JTextArea is not necessary, but affords code-completion/checking from sufficiently smart IDEs (IntelliJ, for example).

The with method is just a Groovy neat feature that allows setting several properties on an Object without repeating the object's identifier all the time. In this case, we set properties on the JTextArea that got selected.

Selecting Swing items

Swing-Selectors offers a few selector methods out-of-the-box - selectWithName, selectWithType and selectWithText - but you can use your own complex selectors with the select and selectAll methods, which just take a Closure that determines which item(s) should be selected. For example:

S.select { it instanceof JPanel && it.name == 'main-panel' } as JPanel

I like names for methods that describe well what the method does, that's why the methods in this library are so descriptive... but if you want to avoid the verbosity, notice that you can use Groovy meta-programming to alias some methods with shorter names... in fact, for your favourite method you need no name at all! Using the fact that the "dictionary" syntax ( variable['property'] ) is translated by the compiler to a call to the getAt method ( variable.getAt( 'property' ) ), you can do this:

final S = new SwingSelector( root: frame )

S.metaClass.getAt = S.&selectWithName

( S[ 'main-panel' ] as JPanel ).with {

   background = defaultBackground

}

( S[ 'current-panel' ] as JPanel ).with {

   background = defaultBackground

}

This is how you apply the "alias" only to this particular instance S, but you can also do this globally, so all instances of SwingSelector will get the new method... just add the following line where your program starts running (main function if possible):

SwingSelector.metaClass.getAt = { String name ->

   delegate.selectWithName( name )

}

Pretty powerful! But this could be dangerous in the wrong hands! So use with care.

Why use Swing-Selectors - A demo

This may seem like a nice, but not so useful library.... I would have to disagree. This can really change the way you program, making it possible to easily apply wholesale changes to a set of components (without having to change the look-and-feel of the application, which is error-prone and pretty complicated, or repeating yourself in countless different places) and clearly separate layout information from application logic and styling, similar to what we do in the web (HTML/CSS/JS) but much better as everything is in the same language (and everything, except the view - due to the dynamic nature of the builder pattern - can be type-checked - you can even use @CompileStatic if you want the speed of Java in some places)!

To show that, I wrote a little application using this concept.

I've recently posted about creating a REST client with Groovy without using any libraries, just using the URL and HttpURLConnection classes from Java, but with lots of Groovy sugar.

Using that code as the back-end, I wrote a little UI that uses Swing-Selectors to display weather information and forecast, and I think the result is really cool!

Before we look at the code, here's what the UI ended up looking like:

I "borrowed" the colors from the Yahoo! official documentation (but as I'm not a very artistic type, I couldn't make it look as good as the professionals, but just for a little example for this blog post, I think that's not too bad).

So here is the layout:

void createUI( Closure... nextActions ) {

   def tableColumns = { List<Map> columns ->

       columns.each { col ->

           builder.propertyColumn(

                   header: col.name,

                   propertyName: col.property,

                   editable: false )

       }

   }

   final defaultInsets = new Insets( 4, 4, 4, 4 )

   final cnst = { Map c ->

       builder.gbc( gridx: c.row, gridy: c.col, insets: defaultInsets,

               ipadx: c.ipadx ?: 0, ipady: c.ipady ?: 0,

               fill: c.fill ?: NONE,

               weightx: c.fill in [ HORIZONTAL, BOTH ] ? 1.0 : 0.0,

               weighty: c.fill in [ VERTICAL, BOTH ] ? 1.0 : 0.0 )

   }

   builder.edt {

       def jframe = frame( title: 'Yahoo! Weather API Client',

               size: [ 500, 400 ] as Dimension,

               defaultCloseOperation: JFrame.EXIT_ON_CLOSE,

               locationRelativeTo: null,

               show: true ) {

           borderLayout()

           scrollPane( constraints: BorderLayout.CENTER ) {

               panel( name: 'main-panel' ) {

                   gridBagLayout()

                   label( 'Y! query:',

                           constraints: cnst( row: 1, col: 1 ) )

                   textArea( name: 'query-field', rows: 5, columns: 30,

                           constraints: cnst( row: 1, col: 2, fill: HORIZONTAL ) )

                   button( 'Run', name: 'run-button', preferredSize: [ 100, 25 ] as Dimension,

                           constraints: cnst( row: 1, col: 3 ) )

                   label( name: 'status-label',

                           constraints: cnst( row: 1, col: 4, fill: HORIZONTAL ) )

                   panel( name: 'current-panel',

                           constraints: cnst( row: 1, col: 5, fill: HORIZONTAL ) ) {

                       borderLayout()

                       label( name: 'current-weather', constraints: BorderLayout.WEST )

                       label( name: 'current-icon', constraints: BorderLayout.EAST )

                   }

                   def t1 = table( name: 'forecast-table',

                           constraints: cnst( row: 1, col: 7, ipady: 4, fill: BOTH ) ) {

                       tableModel( list: forecastData ) {

                           tableColumns( [

                                   [ name: 'Date', property: 'date' ],

                                   [ name: 'Low', property: 'low' ],

                                   [ name: 'High', property: 'high' ],

                                   [ name: 'Conditions', property: 'text' ]

                           ] )

                       }

                   }

                   widget( constraints: cnst( row: 1, col: 6, fill: HORIZONTAL ), t1.tableHeader )

               }

           }

       }

       consumeNextAction jframe, nextActions

   }

}

In the first few lines, I just set up a couple of helper closures... and after that you can see the layout of the UI nicely built using the awesome SwingBuilder (part of the Groovy standard library).

Notice that there's no styling or business logic anywhere... pure layout!

This is really important if you want to write a maintainable app.

The consumeNextAction method is just a little helper method I wrote to allow chaining async methods as if they were synchronous... but that's not important.

Here's the styling code, which is the interesting part where we get to use the Swing-Selectors:

private void styleUI( JFrame frame, Closure... nextActions ) {

   def mainFont = new Font( 'Helvetica', Font.PLAIN, 14 )

   SwingNavigator.navigateBreadthFirst( frame ) { component ->

       ReflectionHelper.callMethodIfExists( component,

               'setFont', mainFont )

   }

   final S = new SwingSelector( root: frame )

   ( S.selectWithName( 'main-panel' ) as JPanel ).with {

       background = defaultBackground

   }

   ( S.selectWithName( 'current-panel' ) as JPanel ).with {

       background = defaultBackground

   }

   S.selectAllWithType( JLabel ).each { label ->

       label.foreground = defaultForeground

   }

   ( S.selectWithName( 'query-field' ) as JTextArea ).with {

       font = new Font( 'sans-serif', Font.PLAIN, 14 )

       foreground = new Color( 55, 209, 55 )

   }

   def status = S.selectWithName( 'status-label' ) as JLabel

   status.font = new Font( 'Helvetica', Font.ITALIC, 14 )

   ( S.selectWithName( 'run-button' ) as JButton ).with {

       background = new Color( 0, 191, 255 )

       foreground = Color.WHITE

   }

   consumeNextAction( frame, nextActions )

}

The SwingNavigator.navigateBreadthFirst method allows your code to visit each component in the UI breadth-first... here, we use that to set the font of all components in which a Font can be set... the ReflectionHelper class is also part of the swing-selectors library and allows us to do that trivially.

And finally, the actual logic of the application:

void setupUILogic( JFrame frame, Closure... nextActions ) {

   final S = new SwingSelector( root: frame )

   def queryField = S.selectWithName( 'query-field' ) as JTextArea

   queryField.text = queryForLocation( 'Stockholm, Sweden' )

   def runButton = S.selectWithName( 'run-button' ) as JButton

   runButton.addActionListener { event ->

       client.run( queryField.text ) { response ->

           handleApiResponse( response,

                   S.selectWithType( JTable ),

                   S.selectWithName( 'status-label' ) as JLabel,

                   S.selectWithName( 'current-weather' ) as JLabel,

                   S.selectWithName( 'current-icon' ) as JLabel )

       }

   } as ActionListener

   consumeNextAction( frame, nextActions )

}

void handleApiResponse( response, JTable table, JLabel status,

                       JLabel currentWeather, JLabel currentIcon ) {

   if ( response instanceof Throwable ) {

       builder.edt {

           status.text = response.message

           status.foreground = Color.RED

       }

   } else {

       Thread.start {

           // the html returned is not parseable, so we get the img link the hard way

           def html = response.description as String

           def imgLink = html.substring( html.indexOf( 'http://' ), html.indexOf( '.gif' ) + 4 )

           println imgLink

           def weatherIcon = new ImageIcon( ImageIO.read( new URL( imgLink ).newInputStream() ) )

           builder.edt { currentIcon.icon = weatherIcon }

       }

       builder.edt {

           status.text = response.title

           status.foreground = defaultForeground

           currentWeather.text = "Currently: ${response.condition.temp}C, ${response.condition.text}"

           forecastData.clear()

           forecastData.addAll response.forecast

           table.revalidate()

           table.repaint()

       }

   }

}

First, we select the weather query JTextArea and set its text to a predefined query for the "Stockholm, Sweden" location (because I happen to live there).

Next, we add an ActionListener to the run button which will actually run the query using a Yahoo! API client... The actual client code is in a separate class which is based on my previous post which I mentioned earlier...

Finally, the handleApiResponse method is shown. The response variable is an Object returned by Groovy's JsonSlurper, or an Exception in case of error. See my previous post for details about that.

If you're curious about how that slightly fancy code to call the async methods as if they were synchronous works, here it is:

static main( args ) {

   new DemoUI().show()

}

void show() {

   // async method chaining

   createUI this.&setupUILogic, this.&styleUI

}

private static void consumeNextAction( arg, Closure... nextActions ) {

   if ( nextActions ) {

       nextActions.first().call( arg, nextActions.tail() )

   }

}

Really simple, actually. It works with the limitation that any method in the chain must have the same signature (it takes an argument followed by an array with the next actions to execute).

Notice that this.&styleUI is a method reference in Groovy, the same as this::styleUI in Java 8.

The whole source for this demo is in this single runnable Groovy file, part of the Swing-Selectors project on GitHub.

Thanks for reading and I hope this can be useful to you!

Comment on Reddit!