Sunday 19 July 2009

Adapting Swing

In a post of mine a while ago I mentioned an idea I'd had about making the separation of the controller and view classes in swing a bit less blurry while also simplifying the swing component classes. After that post I kind of forgot about it until today when it just popped into my head again so I thought I'd try to expand on some of my ideas.

What we have now

Swing currently follows a modified model-view-controller pattern to separate it's data from it's presentation. The reason it's modified is that the controller and view are basically strongly coupled, I mean strongly coupled in the sense that the particular instance of the view doesn't matter but generally the controllable (no pun intended) aspects of the view are defined by the controller.

This is good because the user only needs to use methods on the controller to change the look of the component, but is bad because it limits the flexibility of the view to only those properties that are already defined (and even increases the complexity of the view by making it support properties that may not make sense). One of the big things that this means is that a general developer will never use the view classes at all, basically reducing the view class to an implementation detail for the controller which just seems like a bit of a waste to me.

In order to get around the problem of not being able to add extra properties to the view swing uses the clientProperty approach which allows you to store a name-value pair along with a component instance that can be monitored by the view. The problem with this is that you don't know what is supported, it's not type safe and it's very very dirty.

To get around the required complexity level imposed on the view swing basically says that properties on the controller may affect the view but there are no guarantees. So again the controller is kind of not type safe, in the sense that calling a method may not do what you expect.

If you don't know what I'm talking about here's a couple of examples:

Say you have a component called ImagePanel, it's sole purpose is to show an image, ImagePanel extends JComponent and as such exposes methods like setFont. Now what exactly does setFont mean in the context of a component that shows no text? Beats me!

OK from the other side of it say you make or use a look and feel that is based on painters (like Nimbus) the only way to expose these painters to the user is via clientProperties which generally aren't particularly well documented, aren't checked by the compiler and won't show up in your IDE's auto complete lists which means they aren't very discoverable.

Where can we go from here

Well for a start why not move the view related properties to the view, that way you don't need to worry about supporting all those non-needed properties and you can even define properties specific to a particular view implementation if done right.

Personally to accomplish this I would use an adapter pattern to allow the view to say what it supports in a type safe way. For example here's how you might change the font size of a label:

JLabel label = new JLabel("Adaptive");
TextView view = label.getView(TextView.class);
if (view != null) {
view.setFontSize(12.0);
}

Now that may look a little complicated for something that is fairly simple at the moment. When this approach comes into it's own is when your look and feel decides to provide more functionality. Have a look at these use cases:

// Set the background painter if the look and feel  supports it
// otherwise try to set the background colour
JPanel panel = new JPanel();
BackgroundPainterView bgpView =
panel.getView(BackgroundPainterView.class);
if (bgpView != null) {
bgpView.setPainter(new MyPainter());
} else {
BackgroundColorView bgcView =
panel.getView(BackgroundColorView.class);
if (bgcView != null) {
bgcView.setColor(Color.RED);
}
}

// Use specific features of a look and feel
JButton but = new JButton("Large button");
NimbusScaledView nsView = but.getView(NimbusScaledView.class);
if (nsView != null) {
// set the scale for the component
nsView.setScale(NimbusScaledView.LARGE);
}

JTabbedPane tabs = createTabPaneAndAddSomeTabs();
SubstanceTabView stView = tabs.getView(SubstanceTabView.class);
if (stView != null) {
stView.setTabPreviewsActive(true);
}

As you can imagine there are a lot of examples that I could list, from menu bar searching to animation settings and because the separate APIs are grouped into related interfaces more details can be exposed, like with the text view example, to help the user accomplish what they want.

What can I say

It’s worth mentioning that I haven’t actually tried implementing any of this functionality yet so I can’t say for certain if it will work (or feel right) at all. having said that I think it would be an interesting experiment to see if it makes life easier for both the user and developer of the component.