|The connundrum of Widgetry Sugar|
March 25, 2007, 12:00:17 am
As I've been putting together some simple UIs using Widgetry to learn how it works, I've been trying to simplify the complexity of building the UIs programmatically.
The basically are simple enough, you want to make a widget, add it to another widget and give it an ID and then give it a frame. Sounds simple enough but it can take three lines of code, or two depending on how tricky you want to get. The point is, this is something you're going to do over and over again - a lot. It'd be nice if you only had to write one line:
myNewButton := myPane addButton: #myShinyNewButton.
Okay, so where's the connundrum come in to the picture? Well, there are three places you want this API: UserInterface, Form and PaneWindow. These three classes do not share a common hierarchy.
Traits would solve the problem right away - easy peasy, put the sugar in to a trait and import the trait in to those three classes. No worries!.. however, traits is not something I'm game to load in just to spruce up Widgetry.
So there are a few alternatives..
I think it's worthwhile exploring the third option for a moment. The first option is clearly a no-go.. it'd cost too much to extend and maintain. The second option seems like a good idea to me, except we're throwing away power on PaneWindow for no good reason.
So let's say we have a createInterface method like I do in Twitter:
self inputField: #input readOnly: false. self button: #sendButton image: self sendImage whenClick: #sendStatus. self button: #settingsButton image: self settingsImage whenClick: #openSettings. self tabs: #tabs. self inputField: #status readOnly: true.
The method has more to it including layout stuff which I'll get to later, but you get the general jist. I need to change this API because it is tightly bound to UserInterface. What would this method look like if all the sugar API was class based?
InputField id: #input on: self readOnly: false. Button id: #sendButton on: self image: self sendImage whenClick: #sendStatus.
So I can stop right there. There's now an extra parameter for InputField and Button. I certainly loath passing the focus object in as a parameter - it's like writing C code. And that's the crux of this approach, we're making procedural calls not OO calls. We need a way to focus what we're doing.
Let's look at option four before we resign ourselves to option two. Let's see how that might look:
self builder inputField: #input readOnly: false; button: #sendButton image: self sendImage whenClick: #sendStatus
That's enough of that. It seems well enough. Is it really a feasable approach though? Every time we want to "build" a UI, we'll need a builder. So if I want to make a button in my UserInterface that contains an Image.. there'll be two builders involved. It's not really as nice as I'd hoped.. I don't like having intermediate objects just to get around dodgy duplicate hierarchy problems.
Okay, so the main argument against option number two is this: We have to stick a Form inside the Window instead of using the Window to its full potential. Is that so bad? Well, no, not when you look at the problem from a different angle.
Say you make a UI component and you want to reuse it or you want to put it in its own window.. you want to be able to do both. That sounds like a good goal. So in that scenario, you'd implement it as a Form and have APIs on Form to wrap the Form up inside a Window with some code on your Form subclass to customize the window, such as #hookupWindow.
That means you automatically get the benefits of reuse just by being a Form, we don't need any APIs on PaneWindow for our sugar and we don't actually need UserInterface, because Form has all the same APIs as a UserInterface does already.
The downside? .. you're subclassing off a semi-deep hierarchy. Form is also a crap name. But apart from that, everything else is an upside.
With that in mind, I'm going to head in direction number two with my Widgetry-Sugar package in public store that I'm currently using for Twitter and we'll see where things go from there. If it turns out well, that's great. If not, no harm done, it can be thrown away and we can start again using one of the other three approaches.