Dr G.T. von Kit or: How I Learned to Stop Worrying and Love SpinButton

As developers, sometimes we need to take input from the people using our software. In many cases, the choice of component to use when capturing it is obvious. If you need some text, you use a GtkEntry, if there’s a set of discrete values to choose from, a GtkComboBox is the ideal choice, and so on…

Some datum can be specific, but possibly ambiguous in representation. To illustrate with a contrived example; 1.23 metres is the same as 123cm, you could also say that’s just over 48 inches and again there’s steps up and down in magnitude and alternative ways of writing the same value.

This post deals specifically with Time, in the context of my Google Summer of Code project, as I’d like to specify when Banshee should try to synchronise my playlist. So, what component is the hot enchilada? The obvious choice is the GtkSpinButton. Here’s some highlights, it:

  • supports real numbers and integers;
  • allows the user to type for quick entry;
  • provides handy increment and decrement buttons;
  • can optionally wrap the value beyond the upper and lower bounds;
  • has customisable step and page amounts and, finally;
  • It. Looks. Fantastic.

And the following are my first two attempts at using it for our purpose. While the first is easy enough to use it’s a bit clumsy because it separates the two fields of our input data across two different controls, perhaps more importantly it is not immediately apparent that the controls are representing time or even that the values relate to one another apart from through proximity. The second suffers from a similar problem, what’s in that box doesn’t smell of time!

Time Input SpinButtons: Bad

What to do?! Readers of previous entries can probably guess that I’ll take a slightly circuitious route, known affectionately as scenic. In this instance that involves stepping back a little to GtkEntry, since it conveniently has areas designated for Primary and Secondary buttons. Here’s how that version turned out:

Time Input SpinButton: Good

Not too shabby, it looks pretty good and, apart from the bound wrapping ability, works in an equivalent way to the SpinButton. First some functions:

let inline (|Match|_|) rex text =
    let m = Regex.Match(text, rex)
    if m.Success then
        Some [ for i in m.Groups -> i.Value ]

let rex = @"^(\d{1,2}):(\d{2})$"

This is nothing new. If the regular expression is a match then the result is Some<string list>, otherwise None. For use with the Active Pattern is a regular expression to match two integers separated by a colon, allowing us to extract the hours and minutes from the user’s input, as shown in the following:

let inline to_value (hr,mn) = float hr * 60.0 + float mn

let inline from_text text =
    match text with
    | Match rex [full;hr;mn] -> to_value (hr,mn) |> Some
    | _ -> None

let inline from_value v =
    let hr = uint32 v / 60u
    let mn = uint32 v % 60u

let inline to_text (hr,mn) = sprintf "%d:%02d" hr mn

These four functions convert the decimal value stored in the GtkAdjustment to its text representation and back again. In our GtkEntry subclass they are connected to the signals emitted when the text entry area loses focus and when the value of the GtkAdjustment changes. Astute readers will notice that it would be fairly easy to include support for other time formats to the from_text function.

The code for the widget weighs in at around 40 lines. I’m not going to display it here, it’s long, boring and a lot was concerned with starting and stopping GLib timeouts to make the value scroll when one of the buttons was held down. Furthermore, the scrolling behaviour just wasn’t the same as SpinButton and required some tweaking of the repeat timings, in addition to a slightly hacky function to ensure that the appropriate timeout stops when the value reached either bound. Quite a bit of work for something that only sort of works like a SpinButton…

There is a better way, the Input and Output signals of SpinButton are designed for conversion purposes. This is the result:

Time Input SpinButton: Better

It’s simple, short (13 lines, three of which are semantic constants, with even one superfluous extra for no other reason than completeness) and customisable. This, folks, is How I Learned to Stop Worrying and Love SpinButton.

type FormatSpinButton(adjust: Adjustment, climb, digits, get, set) as this =
    inherit SpinButton(adjust, climb, digits)
    let TRUE = 1
    let FALSE = 0
    let GTK_INPUT_ERROR = -1
    do this.Input.Add(fun o -> 
         match this.Text |> get with
         | Some v -> o.NewValue <- v
                     o.RetVal <- TRUE
         | None -> o.NewValue <- this.Value
                   o.RetVal <- GTK_INPUT_ERROR)
       this.Output.Add(fun o ->
         this.Text <- this.Value |> set
         o.RetVal <- TRUE)

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s