2010-02-07

Simplifying the API

Using Woopsi’s keyboard as an input device has been needlessly complicated. To output keyboard input, a programmer would need to:

  • Create a TextBox as the output gadget;
  • Create a WoopsiKeyboard as the input gadget;
  • Create a class that inherits from KeyboardEventHandler that will receive input from the keyboard and direct the output to the textbox;
  • Add the new class as an event handler to the keyboard.

This pattern would need to be repeated every time the keyboard needed to be used.

Clearly this is a bad idea, so instead I’ve made the TextBox and MultiLineTextBox gadgets into KeyboardEventHandlers. Using a keyboard now requires these steps:

  • Create a TextBox as the output gadget;
  • Create a WoopsiKeyboard as the input gadget;
  • Add the textbox as an event handler to the keyboard.

Missing out the middleman event handler reduces the amount of code necessary to achieve keyboard input from two dozen lines and an extra class down to around three lines.

Whilst on a simplifying drive, I was looking at the click(), release(), doubleClick() and similar functions in every gadget class. These were complicated to work with. Overriding them is essential if one is subclassing Gadget in order to make a new gadget, but it is very easy to break the methods by overriding them incorrectly.

Consider the click() method. This method contains around two dozen lines of code, all of which is absolutely crucial for the gadget to function properly when clicked. Subclasses had to call the base class’ click() method as the overridden method’s first step or the gadget wouldn’t work. The code worked like this:

click() method
    check that this is a valid click
        check that gadget is enabled
            run custom behaviour
            return true
    return false

Forgetting any of these steps resulted in a non- or semi-functional click() method.

Most of the time the click() method is overridden it is to add some trivial extra functionality, so having to write out a dozen lines of boilerplate code just to add a redraw() call or something similar is absurd.

The Gadget class now has a handful of stub methods that can be overridden in subclasses to avoid this problem. Instead of overriding click() - which is still possible, if really bizarre behaviour is required - developers should now override the new onClick() method. This function is called when the gadget determines that the click really is valid, so all that needs to go into the onClick() method is any code that is relevant to that method.

For example, if a gadget should redraw when clicked, this is the code needed:

void onClick(s16 x, s16 y) {
    redraw();
}

Conversely, an old-style click() method to do the same looks like this:

void click(s16 x, s16 y) {
    if (Gadget::click(x, y)) {
        if (isEnabled()) {
            redraw();
            return true;
        }
    }
    return false;
}

The new-style approach is considerably terser and has no vital steps that can be accidentally missed.

The new stub methods are as follows:

  • onClick() - called when the gadget is clicked.
  • onDoubleClick() - called when the gadget is double-clicked.
  • onShiftClick() - called when the gadget is shift-clicked (ie. when the shoulder button is held down; this triggers the context menu).
  • onDragStart() - called when a dragging starts on a gadget.
  • onDrag() - called when a gadget is dragged.
  • onDragStop() - called when a dragging stops.
  • onRelease() - called when a clicked gadget is released within its boundaries.
  • onReleaseOutside() - called when a gadget is released outside its boundaries.

I have switched to this new approach throughout the Woopsi library and it has made a lot of the gadget classes shorter and easier to understand.

Related to this, Gadget::click() no longer includes a call to setDragging(). If a gadget should respond to stylus drags, it should call this method in its onClick() override. I must get around to renaming that to “startDragging()” or something similar…

In other news, ScrollingPanel no longer includes a raiseScrollEvent() method. I’ve moved this into the GadgetEventHandlerList class. The delete key on the keyboard deletes characters in front of the cursor in the TextBox and MultiLineTextBox gadgets. Lastly, I’ve removed the parameters from the GadgetEventHandlerList::raiseActionEvent() method.