Spinner: Alternative Spinner UI Implementations

This article discusses a couple new of SpinnerUI implementations. You can download this project here.

Context

A couple of years ago I made a dandy mechanism that helped automatically inject sample images in my javadoc. It dumped every screenshot here, using the simple classname to help name it. This made me aware that I had two components with the same name: there were two different classes (used for separate projects, in separate packages) with the name NavigationPanel:

A one-line set of three controls: a left button, a right button, and a label Two lines of 5 controls. The first line contains a large first, previous, next, and last button. The second line contains a slider.

Both are useful components, and they're obviously very distinct in appearance. I wanted to keep both, but somehow refactor them to share common logic.

My first response was to create an entirely new JComponent named the NavigationPanel and make unique ComponentUI's for it. This automatically required 3 classes: a ComponentUI, a data model, and the JComponent wrapping them all together.

On the one hand: the unique NavigationPanel component allowed me to custom-tailor a data model that exactly suited my needs. For example: with a spinner if you want to change the value and the maximum of a SpinnerNumberModel, that triggers two separate events. (Sure, you can get around this by suspending listeners... but that's an unnecessary hassle for such a simple idea.)

And if you're at the maximum of the number range, then things get weird. Imagine a list: A, B, C. You have C selected. You want to add an element D and select it. You have to do it in exactly that order (modify list, then select) or you'll get an exception. But if you want to remove element C and select B, then you have to do it in the opposite order (select B, then modify the list) or you'll get an exception.

A new data model can let us modify both properties in one call and keep the internal state consistent between events so it's all-around safer to use.

These models were also designed around integers, so the data model's method signatures reflected that. The SpinnerNumberModel is a little ambiguous and funky. (That is: it supports Doubles and Integers in a weird look-I-do-everything-at-once kind of way.) And besides that: at any point you can swap out your SpinnerNumberModel with a different SpinnerModel entirely. That's probably a fringe case I shouldn't worry about, but in a more ideal world: I can custom-design method signatures that build in better type-safety up front. (That is: I never have to cast a model to a SpinnerNumberModel.)

... but. On the other hand: using SpinnerUIs has a wider/simpler appeal, because every developer who knows what a Swing UI is has some notion of how to use JSpinners. Implementing this component as a SpinnerUI means other developers can just slap this on with a few lines of code and, if they don't like it, switch it back just as easily. So I'd lose a little type safety and elegance of design, but I'd gain a lot of usability (by developers).

Implementation

Ultimately I decided to implement this as a SpinnerUI. Hopefully that will be beneficial to both readers of this blog and to myself over time.

With that decision out of the way, the next step was to create an abstract parent UI. I name it the NavigationPanelUI to keep it significantly differentiated from your traditional concept of a "spinner". But the basic moving parts are still very similar. This extends the BasicSpinnerUI, and that already has a notion of the next/previous buttons. This class introduces the label and a few special listeners. (Also this implementation disregards the "editor" component. That might be a nice v2.0 feature someday, but it's far outside my current needs for now.)

The single-row implementation became the CompactNavigationPanelUI. The only unique logic it needed to add was its own FlowLayout-like layout of all the components.

The multiple-row implementation became the LargeNavigationPanelUI. This adds the slider and the first and last button.

The first drafts hardwired in the expectation that you'd only ever use this with a SpinnerNumberModel. And probably I should have stopped there and left it as-is (because feature creep will be the death of me)... but since I decided to write this up and share it: it made sense to clean it up at least a little. The current demo app looks like this:

Demo application showing both implementations, once using a number-based model and once using a list of states.

Here we see two clusters of three JSpinners that share the same model. (That is: when you just any of the first 3 spinners all the others also update. The same is true for the bottom three spinners.)

The number-based model is still the primary expected usage. And there's a helper class to automatically write the "Page X of Z" text. But if it's not number-based: then you can still use the label. In fact you have to use the label. In the LargeNavigationPanelUI: if you aren't using numbers the slider is replaced with the label.

In theory it MIGHT be possible to iterate over a SpinnerModel and count elements to determine the effective maximum number of entries. This way you can continue to represent this as a slider. However an abstract model doesn't have to have a finite number of entries. It's possible (but probably not likely) that you could use a spinner to represent, say, google search results. You cache a few, but if the user walks through the first 20, then you fetch the next 20. And on it goes, possibly forever (or close to it). The cost of grabbing the next element might be surprisingly high in some cases.

(If the Google example was far-fetched: consider instead walking through files on your OS. That might seem more innocent, but it can trigger significant IO bottlenecks. An iterator trying to walk through a folder of files to help format a JSlider could lock up your UI!)

Conclusion

Was it worth it? Maybe not. I'm pleased with the results, but honestly I was kind of OK with the simple naming collision I started with. I don't think I'm likely to create too many more variations on this theme. But I suppose if I do: the framework is ready for easy expansion now.