 |
Swing Application Framework Hacks Unleashed For Smarty Pantses
Posted by diverson on April 02, 2007 at 01:06 PM | Comments (19)
Join me now as we explore the murky depths of the Swing Application Framework.
Ok, there is nothing murky about the app framework. Hans is fond of saying that one of the primary goals of the framework is to be able to explain it to someone in one hour. It's a noble goal. In fact, I think Hans'
presentation does it pretty well.
Even better than the one-hour goal, in my opinion, is the one-day goal. With the app framework as it stands today, you can sit down and read through the source code for one day and know everything there is to know about it. How each feature is implemented, the way they work together, where you need to look if something goes wrong. Transparency is one of my favorite features in a framework and the application framework is a veritable cornucopia of transparency.
This is really important for a framework that you intend to use as the foundation for your entire application. Maybe the code monkeys (and I use that term affectionately) can get away with the one hour explanation. But if the buck stops at your keyboard, you really need a deeper understanding. We don't want any surprises down the road.
So let's get to it!
One of the coolest things about the app framework is it's support for resource injection. It can be used to inject resources like text, fonts, and colors into our Swing components and actions. How does it work? Magic? Unfortunately it's not that fantastical. It turns out that all we need is a good map.
Uhm No, Not That Kind Of Map
|
No, application frameworks are way more mundane than that. What we need is a ResourceMap. So how do we get one? Well, like most other application frameworks, the Swing app framework features an Application class as its main abstraction. We will be using one of Application's subclasses, the SingleFrameApplication class since it provides a few extra house keeping services for us.
package playground.swing.framework.actions
// import this.and.that
public class ActionGame1 extends SingleFrameApplication
{
protected void startup( String[] args )
{
System.out.println( "Ahhhh! I've been launched!" );
}
public static void main( String[] args )
{
Application.launch( ActionGame1.class, args );
}
}
When we call Application.launch( ActionGame1.class, args ), the framework performs some initialization steps, creates an instance of our application object (based on the class we passed it in the launch method), and calls that new instance's startup method. The call to the startup method is done on Swing's event dispatch thread (otherwise known as the EDT). Therefore it is safe to construct Swing components within the friendly confines of our overridden startup. So all we've done here is write a program that prints a cutesy message on the console and quits. As Romeo might say: Woo who.
What about the resource map?
One of those initialization steps that the framework performes before creating an instance of our application class is to build the application ResourceMap. A ResourceMap is really just a wrapper around a regular old Java ResourceBundle which is itself just a wrapper around a .properties file. The convention is that the properties file is named ClassName.properties and is located in a "resources" subpackage of its class' package. The application ResourceMaps are arranged hierarchically (each ResourceMap has a "parent" property and a getParent method) and mirrors the Application class hierarchy.
So for this case we end up with the following structure for our application-level ResourceMap (where I use the ResourceBundle filename to represent the ResourceMap):
ResourceMap Hierarchy
|
Conceptually, our resources are layered on top of the standard Application class resources. Since resources are searched for from the bottom up, this allows a resource in ActionGame1 to override a resource in Application. Which, by the way, is how you would localize the names of the default actions defined by the framework's Application class.
Note that technically our Application class hierarchy includes the SingleFrameApplication class. Since this class does not have a resource bundle associated with it, it does not appear in our ResourceMap hierarchy.
So much for ResourceMap 101
Now that we know where to put our resource files, we just need to know what to put in them. A Java properties file is just a series of key/value pairs.
# The format of a properties file
someKey=Some Value
In order to inject a resource into a Swing component, all we need is some way to tie the value of the resource (identified by its key) to a particular component. The Swing app framework uses the component's name to make this connection.
com/acme/coyote/order/form/Anvil.java
package com.acme.coyote.order.form;
...
JLabel description = new JLabel();
description.setName( "anvilDescription" );
...
com/acme/coyote/order/form/resources/Anvil.properties
anvilDescription.text=Anvils are heavy, black, and can be dropped on the head of Tastyus Supersonicus.
anvilDescription.font=Arial-BOLD-24
anvilDescritpion.background=255,255,255
Now your resources are not only localizable but are injected into your component automatically. But wait! How do resources like "Arial-BOLD-24" and "255,255,255" get turned into the component's font and background color? Is there some magic there (we ask hopefully; clinging to our last shred of child-like wonderment)?
Nope, sorry (we answer ourselves; heartlessly dashing all remaining childish hope and, frankly, worrying ourselves a bit for asking and answering our own questions). It's actually very simple. The framework looks at the name of the property we are trying to set (like "text" or "font") and uses reflection to look up the corresponding property of the component (like "setText" or "setFont"). When it finds the property, it looks at what type of object the set method should be called with (like a Color for setBackground). Once it knows the type of object it needs, it just queries its list of ResourceConverters looking for one that will do the job.
The app framework comes with several standard resource converters and you can easily write your own as well. Converters exist for all of the basic types, of course: int, float, double, short, byte, boolean. In the case of the boolean converter, the words "true", "on", and "yes" evaluate to true. Everything else evaluates to false.
There are also a few more complex converters which are listed below along with the string formats they accept.
| ResourceConverter |
Format |
Example |
Note |
| ColorStringConverter | #RRGGBB | #FF0000 | Red |
| #AARRGGBB | #80FF0000 | Half transparent red |
| R, G, B | 0, 255, 0 | Green |
| R, G, B, A | 0, 0, 255, 128 | Half transparent blue |
| FontStringConverter |
fontName-style-size |
Arial-BOLD-12 |
This converter is just a wrapper around the Font#decode method
|
| fontName style size | times italic 18 | Dashes are optional, case is insensitive |
| IconStringConverter |
filename.[png,jpg,gif] |
new_file.png |
Any of the usual image formats Java supports |
Injection Junction, what's your function?
Let's try a simple example.
playground/swing/framework/actions/ActionGame1.java
package playground.swing.framework.actions;
// import a.bunch.of.stuff
public class ActionGame1 extends SingleFrameApplication
{
protected void startup(String[] args)
{
JButton btn = new JButton();
btn.setName( "startCountingBtn" );
JPanel panel = new JPanel();
panel.add( btn );
show( panel );
}
public static void main( String[] args )
{
Application.launch( ActionGame1.class, args );
}
}
playground/swing/framework/actions/resources/ActionGame1.properties
Application.title=Action Game 1
Application.id=ActionGame1
startCountingBtn.text=Start Counting
startCountingBtn.font=times italic 18
startCountingBtn.foreground=0,0,255
Here we've created a button and added it to a panel. We then display the panel in a JFrame by calling the show method of SingleFrameApplication. In addition to constructing and displaying our frame, the show method also causes our resources to be injected by calling the ResourceMap#injectComponents method and passing our new JFrame as the argument. The results are simply stunning.
Simply Stunning!
|
Hey! What about actions?
So what happens when you press the button? That's right! Absolutely nothing.
So let's add an action to our button. Now, in the bad old days (circa 2 months ago), that meant having to create an action listener and add it to the button. But these days the sun is shining, the birds are singing, and we have the @Action annotation.
playground/swing/framework/actions/ActionGame1.java
package playground.swing.framework.actions;
// import a.bunch.of.stuff
public class ActionGame1 extends SingleFrameApplication
{
@Action
public void startCounting( ActionEvent ev )
{
System.out.println("Starting to count...1, 2, 3, 4");
}
protected void startup(String[] args)
{
ApplicationActionMap aMap = ApplicationContext.getInstance().getActionMap( getClass(), this );
JButton btn = new JButton();
btn.setName( "startCountingBtn" );
btn.setAction( aMap.get( "startCounting" ) );
JPanel panel = new JPanel();
panel.add( btn );
show( panel );
}
public static void main( String[] args )
{
Application.launch( ActionGame1.class, args );
}
}
playground/swing/framework/actions/resources/ActionGame1.properties
Application.title=Action Game 1
Application.id=ActionGame1
startCountingBtn.font=times italic 18
startCountingBtn.foreground=0,0,255
startCounting.Action.text = &Start Counting
Now when we push the button we see our message printed on the console. All we had to do was add the @Action annotation to a method. Then we used our ApplicationContext to get the action map for our class. We used that action map to get the action that calls our startCounting method. And finally, we gave that action to the button.
So we saved a few lines of code, big deal.
Wait for it.
When counting is allowed, counting aloud can be rude. But counting aloud while blocking the EDT is downright obnoxious. The problem is that launching a background thread to do a simple task like counting is a huge pain in the neck. We'll do it anyway. Put on a brave face, threads can smell fear.
The first thing we'll need is a background task. The Swing app framework comes with a class made just for this very purpose. It's called Task, oddly enough. Creating a new one is fairly easy.
public class CountingTask extends Task<Void, Void>
{
/** Creates a new instance of CountingTask */
public CountingTask()
{
super(CountingTask.class);
}
public Void doInBackground()
{
System.out.println("In the background task and counting...");
for( int i=0; i<5; ++i )
{
System.out.println( String.valueOf( i ) );
try
{
Thread.sleep( 250 );
}
catch (InterruptedException ex)
{
System.out.println("I hate being interrupted while counting! Now where was I?");
}
}
return null;
}
}
The really slick part is that launching a background task is completely trivial with the app framework. We just need to make two changes to our action method.
@Action
public CountingTask startCounting( ActionEvent ev )
{
return new CountingTask();
}
Yep, all we have to do is create and return the task. That's it. Finished. The framework takes care of launching it for us using a thread pool that it creates and manages (actually a ThreadPoolExecutor for you concurrency geeks out there).
Putting it all together.
Since that was too easy, let's dress up the interface just a bit with a label as well.
playground/swing/framework/actions/ActionGame1.java
package playground.swing.framework.actions;
// import a.bunch.of.stuff
public class ActionGame1 extends SingleFrameApplication
{
@Action
public CountingTask startCounting( ActionEvent ev )
{
return new CountingTask();
}
protected void startup(String[] args)
{
ApplicationActionMap aMap = ApplicationContext.getInstance().getActionMap( getClass(), this );
ResourceMap rMap = ApplicationContext.getInstance().getResourceMap( getClass() );
JLabel label = new JLabel();
label.setName( "titleLabel" );
JButton btn = new JButton();
btn.setName( "startCountingBtn" );
btn.setAction( aMap.get( "startCounting" ) );
JPanel panel = new JPanel();
panel.setBorder( BorderFactory.createTitledBorder( rMap.getString( "countingDemo" ) ) );
panel.setLayout( new BorderLayout() );
panel.add( label, BorderLayout.NORTH );
panel.add( btn, BorderLayout.CENTER );
show( panel );
}
public static void main( String[] args )
{
Application.launch( ActionGame1.class, args );
}
}
playground/swing/framework/actions/resources/ActionGame1.properties
Application.title=Action Game 1
Application.id=ActionGame1
countingDemo=Counting Demonstration
countingIsAllowed=Push the button to start counting.
titleLabel.text=${countingIsAllowed}
titleLabel.font=Arial-BOLD-16
startCountingBtn.font=Arial-BOLD-24
startCountingBtn.foreground=0,0,255
startCounting.Action.text = &Start Counting
startCounting.Action.shortDescription = Start counting in the background
Let's go through these changes real quick:
- We included the change to the
startCounting action.
- A label has been added whose text is retrieved from the resource bundle.
- The label's text in the .properties file is a gratuitous example of how one property can refer to another.
- A tooltip for the startCounting action was added.
- We created a titled border and used
BorderLayout to dress things up a bit.
- The text for the titled border is retrieved from the resource bundle just like a typical piece of localized text would be.
- We've added a mnemonic to the action's text using the '&' character.
- We added some Application resources to the .properties file in order to specify the title of the application and an ID string (which is used when storing user preferences and such)
This is what our stunning application looks like now.
Simply Stunning-er!
|
And this is what we see when the magical button is pushed.
In the background task and counting...
0
1
2
3
4
Some of you who have done this a time or two are probably anxious to point out that there is a major piece missing here. What happens if we quickly push the button twice?
In the background task and counting...
0
1
In the background task and counting...
0
2
1
3
2
4
3
4
My, what a mess. When performing background tasks we usually have to be careful to disable the interface while a background task is running. Then we have to figure out when the task is finished so we can re-enable it. This can be an error-prone process but the framework gives us an easy way out here too!
There's just one more thing.
With one trivial addition to our @Action annotation, this whole problem just disappears.
@Action( block = Block.COMPONENT )
public CountingTask startCounting( ActionEvent ev )
{
return new CountingTask();
}
We simply tell the action to block our component while the task is running. Now the framework handles all of the details of disabling our button when the task begins and re-enabling it when the task ends. Personally, I love frameworks that do more work so I can listen to more Java Posse...oops, I mean be more productive with other things.
This is what we now see after the button has been pushed.
Simply Blocked
|
Hey, boy! How come that don't look like no Mac program?
You have probably noticed that I haven't been using the normal Aqua look and feel even though I'm obviously developing this on a Mac. What you're seeing is the new Nimbus look and feel.
Like most of the other things I've covered, using this with the Swing app framework is a simple one line addition. Is that a coincidence or am I just lazy? You decide. Regardless, if you would like to use a specific look and feel in your app framework application, all you have to do is....
You know what? I'm not going to tell you. Magicians don't divulge all of their secrets!
That's lame!
Very true. But that hasn't stopped me yet in this post!
Oh, alright. Just download the Nimbus jar file and put it in your class path. Then add one extra line to the ActionGame1.properties file:
...
Application.lookAndFeel=org.jdesktop.swingx.plaf.nimbus.NimbusLookAndFeel
...
There you have it - no more secrets.
Bookmark blog post: del.icio.us Digg DZone Furl Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment
-
How about support for DPI-aware font sizes?
Posted by: kirillcool on April 02, 2007 at 01:09 PM
-
One case i'm not seeing in the app framework, is what happens if the properties that i want to persist can be modified on runtime? The app framework doesn't magically replace the text in the resource bundle does it? In a not hyphotetical example, what whapens if the property is something map based like KeyStrokes that are utterly bitchy to serialize (can't must serialize must toString and reconstruct on load) are utterly bitchy to rebind (a map property and reflection) and really not designed for it at all (even thought inputmap tries to be a map, it doesn't implement the interface, and that means its values is not accessible, so you can't save it like a collection directly). However setting the keyStrokes(shortcuts) for a action in a program is a very common option and must be saved.
Posted by: i30817 on April 02, 2007 at 01:25 PM
-
Nice post. Here's a question that has been on my mind ever since the Swing App Framework has been released. Maybe you can answer it. Since one of the Swing App Framework most important roles, just like any other app framework, is like you mentioned, support for resource injection, be it text, font, colors, actions, LnF, etc., why hasn't it been built on top of a Dependency Injection (DI) container? Why do we have to extend the Application class instead? DI based frameworks have shown to be the best approach for this sort of resource injection. They are well proven in the industry as far scalability, power, and flexibility, and can be made very simple to use. Why did the Swing App Framework have to go it its own way, rather then leverage one these well designed DI containers out there (Guice, etc.)? Extending the Application class to write your application is so last century in case you haven't been checking.
Posted by: mikeazzi on April 02, 2007 at 02:25 PM
-
Very nice!
I have a couple concerns:
Can anything be done to simplify maintaining the string literal references to component names and Action method names? What happens when I use a component name that's not in the resource file? Maybe a build-time verifier.
I like the property file format as a self-explanatory format ... but perhaps there is a more appropriate format? CSS would be a nice way to skin the application and build the resource map at the same time.
#titleLabel {
text: "Counting Demonstration";
font: bold 16pt Arial;
}
Posted by: jsando on April 03, 2007 at 08:03 AM
-
Dean,
This writeup was great. It was funny (kept things light hearted, so I didn't feel like I was reading a API spec) and so well detailed. You covered all the beginner questions I had and then dug down further into the "what ifs" that I was sure to have after getting started with the framework. You really nailed it.
Thanks for putting things together.
Riyad
Posted by: rkalla on April 03, 2007 at 08:08 AM
-
mikeazzi: You, I and many others have been wondering the same. I have actually wired a Swing app together with Spring. And I've recently wondered how much more interesting it would be to do so with Guice. Interestingly, an IOC solves a lot of intricate Swing dependency issues, removes the need to write component and/or action glue code and allows for extremely well-factored Swing applications. Sigh....
Posted by: javawerks on April 03, 2007 at 08:10 AM
-
Nice article, and very useful.
Any hints on how you might set a particular theme for a LookAndFeel.
For example, to use one of the JGoodies LookAndFeel's:
Application.lookAndFeel=com.jgoodies.looks.plastic.PlasticXPLookAndFeel
However, how would you then set a theme like ExperienceBlue for this LookAndFeel?
Posted by: cforker on April 03, 2007 at 08:36 AM
-
I think I need to clarify something that I probably should have made clear right up front in the blog post: I am not in any way affiliated with or responsible for the development of JSR 296.
I happen to agree with the stated goals of the framework so I checked it out when it was released. The design resonated with me for some reason so, like a few other people, I started to dig into the code and liked what I saw. Writing about what I am learning is a useful exercise for me because it helps me organize my own thoughts about a new piece of knowledge. The only real question is whether I should actually publish these semi-coherent and silly ramblings. But I figured what the heck, public humiliation is good for you. Builds character.
My point is that I cannot attempt to speak for Hans or any of the members of the expert group. I will definitely try to answer all of the questions I can, but I strongly encourage anyone who is curious to speak up on the users mailing list for the framework. The archive is here.
Posted by: diverson on April 03, 2007 at 10:33 AM
-
i30817: Saving user preferences is definitely something that most (not all, I suppose, but most) applications need to do. You are correct, the framework does not magically replace text in the resource bundles (thank goodness). What it does do is provide a LocalStorage class that helps a bit by giving you a place to store your stuff.
On Unix this means "user_home_dir/.${application_id}" where application ID is the same one that I mentioned in the .properties file above. On OS X it is "user_home_dir/Library/Application Support/${applicationID}" and on Windows it is "user_home_dir\Application Data\${vendor_id}\${application_id}". So if your application is running on Windows it is also a good idea to include an entry for Application.vendor in your .properties file.
LocalStorage works by calling the save method and passing it a bean and a filename. The bean is serialized to the filename you give which is located, of course, in one the locations above. The serialization is handled by XMLEncoder/Decoder.
So the bottom line is that you have to get your custom key bindings into a bean that can be serialized. It's all down hill from there.
Posted by: diverson on April 03, 2007 at 10:50 AM
-
mikeazi and javawerks: I'm not sure why a real injection container was not used. My guess would be that Hans wants a small, light framework and is therefore trying to avoid any extra dependencies since this work is all targeted for inclusion in the JDK eventually.
But it could be that right now dependency injection is one of those things that every programmer has to write themselves. Kind of like the string class was in the early days of C++. Maybe Hans is just doing his part. :-)
But seriously, I would love to see examples of a Swing app written this way. Usually these IOC frameworks fall in the non-transparent category for me. It is too hard to dig in and see what is actually going on. Or maybe that is just an excuse because I've lacked the proper motivation in the past. Maybe you guys could point me to an example to get me started?
Posted by: diverson on April 03, 2007 at 11:01 AM
-
jsando: Using a CSS-like format is an interesting idea. I don't think it would be as easy for a beginner to understand. Since beginners are a major target audience for the app framework, I'm sure that factored into the decision to use a simple format like the .properties file. The fact that it was already well supported by the JDK probably was a big factor as well.
Posted by: diverson on April 03, 2007 at 11:04 AM
-
cforker: There is no direct support for setting a LAF theme. And since a look and feel is not a component, writing a new resource converter is not really an option either. I think you would be stuck with doing something like the following inside your startup method:
protected void startup(String[] args)
{
...
LookAndFeel laf = UIManager.getLookAndFeel();
if( laf instanceof PlasticXPLookAndFeel )
((PlasticXPLookAndFeel)laf).setMyCurrentTheme( new ExperienceBlue() );
...
}
Or something like that. It's not pretty, but I think it would work.
Posted by: diverson on April 03, 2007 at 11:22 AM
-
mikeazzi: Sorry, I forgot the other part of your question re: subclassing Application.
Actually subclassing Application has nothing to do with resource injection. What we gain by subclassing Application is support for the lifecycle callbacks: startup, ready, and shutdown (along with the ExitListeners associated with shutdown).
This brings up one of the things about the framework's design that really resonated with me. All of it's major features are pretty much orthogonal. You can use one without being forced to use them all.
If you don't like resource injection, you can still use the @Action annotation and the background task support. Or if you have your own way of handling actions, you can just use the resource injection or the background tasks separately. In the case of the former, you just call ResourceMap#injectComponent and for the latter you get the TaskService from your ApplicationContext and call its execute method in order to launch your own background tasks.
It all does hinge on subclassing Application, though. That part is required. But doing so makes it a no-brainer to construct your components safely (on the EDT) and manage your app's life cycle, so why wouldn't you want to take advantage of that?
Posted by: diverson on April 03, 2007 at 12:03 PM
-
jsando: I forgot a part of your question too. Sorry about that. This is what happens when I start to rush. It's pretty sorry when most of the comments on a blog are from the author. :-)
Anyway, I completely agree with your concerns on the subject of maintaining the string literal references. Strings are easy to mess up and your language-aware IDE won't be able to tell you a thing about it. Usually this is where tools come to our rescue and this is no exception. Joshua Marinacci is working on adding JSR 296 support to Netbeans. I would assume that part of that support will help us with managing the connection between our components and their resources.
Unfortunately, if you're not using Netbeans you are probably stuck with managing these things by hand. Although not ideal, I still think the rest of the conveniences of the framework outweigh this drawback.
Posted by: diverson on April 03, 2007 at 12:27 PM
-
This seems nice, but how about Actions that are outside of any individual UI component? eg. both a menu item and toolbar item delegate to the same action, or Save As action delegates to Save action for its file write, or changing the properties of an action like a context sensitive tooltip, or binding the icon to the action rather than the component. Under the current system of using Action objects multiple components can be bound to the same action and they will all update to reflect that action object's state. How do you achieve that with this new framework?
Posted by: r_nagappan on April 03, 2007 at 07:47 PM
-
r_nagappan: Those are thoughtful, insightful questions. Thanks for asking. Next question, please!
Just kidding. First, you can use the @Action annotation on methods in pretty much any class. All you need is to have a reference to the object in order to look up the action when you need it. Since you are annotating plain old methods, you can have them call each other to delegate part of their work (like saveAs calling save, for example).
The thing to realize is that all of the @Action annotated methods are converted into normal Actions when the ActionMap for your class is created. So you can do all of the normal Action-y things to them. Things like giving them to multiple components and having those components reflect the state of the Action, changing their properties, and setting their icons. Although not shown in this post, you can set all of the normal Action properties from the .properties file. To use the startCounting action as an example, the complete set of properties you could set in ActionGame1.properties would be:
startCounting.Action.icon
startCounting.Action.text
startCounting.Action.shortDescription
startCounting.Action.longDescription
startCounting.Action.smallIcon
startCounting.Action.largeIcon
startCounting.Action.command
startCounting.Action.accelerator
startCounting.Action.mnemonic
startCounting.Action.displayedMnemonicIndex
Posted by: diverson on April 04, 2007 at 03:04 PM
-
The latest release of AppFramework doesn't include the ApplicationContext.getInstance method instead you can get the context from Application class with getContext method. How can I get the context within a class that doesn't have access to Application?
Can you update the examples ?
Why the method wasn't set as deprecated by the developers group of AppFramework?!!!
Posted by: calocoj on August 31, 2007 at 11:16 AM
-
In the latest version of the framework, the main singleton has been changed from ApplicationContext to Application itself. So now you write
Application.getInstance().getContext();
Note that in the latest version, Hans has made it is possible to call Application.getInstance() without having defined and launched an application first. In this case, getInstance returns an application subclass called NoApplication. But it allows access to the ApplicationContext and the ResourceMap.
Posted by: diverson on September 02, 2007 at 08:46 AM
-
Regarding how to set a theme within a Look and Feel, it is quite straight forward. Define your own class, in my case I call it Alloy.
import com.incors.plaf.alloy.AlloyLookAndFeel;
import com.incors.plaf.alloy.themes.bedouin.BedouinTheme;
/**
* Setting up Alloy Look and Feel with a BedouinTheme.
*
* @authour Skjalg Bjørndal
* @since Nov 2, 2007, 11:53:15 AM
*/
public class Alloy extends com.incors.plaf.alloy.AlloyLookAndFeel {
static {
AlloyLookAndFeel.setProperty("alloy.isLookAndFeelFrameDecoration", "true");
AlloyLookAndFeel.setProperty("alloy.licenseCode", "yourLicenseCode");
}
public Alloy() {
super(new BedouinTheme());
}
}
Now in the properties file, you put a reference to the class you created:
Application.lookAndFeel = com.somecompany.project.Alloy
Posted by: skjalgb on November 02, 2007 at 09:53 AM
|