Today we show how the SpinButton works and some of its more interesting features.
Design
We'll start off with a SpinButton of course, and add some radio buttons, one each for having no top or bottom limit on the value, one each for bouncing at a top or bottom limit, and finally a single check box to tell the spin button to wrap around, or not.
As always, we start off with our UserInterface subclass:
Smalltalk defineClass: #SpinButtonWork
superclass: #{Widgetry.UserInterface}
indexedType: #none
private: false
instanceVariableNames: 'spinButton noTopLimit noBottomLimit bounceTop bounceBottom wrapTop wrapBottom '
classInstanceVariableNames: ''
imports: 'private Widgetry.*'
category: '(none)'
Now here is our #createInterface will all the widgets laid out:
createInterface
spinButton := SpinButton new.
spinButton frame: (FractionalFrame
fractionLeft: 0.05
right: 0.95
top: 0
bottom: 0).
spinButton frame
topOffset: 10;
bottomOffset: 40.
self addComponent: spinButton.
noTopLimit := RadioButton withLabelString: 'No Top Limit'.
(noTopLimit frame)
inset: 10 @ 80;
extent: 75 @ 30.
self addComponent: noTopLimit.
noBottomLimit := RadioButton withLabelString: 'No Bottom Limit'.
(noBottomLimit frame)
inset: 100 @ 80;
extent: 75 @ 30.
self addComponent: noBottomLimit.
bounceTop := RadioButton withLabelString: 'Bounce Top'.
(bounceTop frame)
inset: 10 @ 110;
extent: 75 @ 30.
self addComponent: bounceTop.
bounceBottom := RadioButton withLabelString: 'Bounce Bottom'.
(bounceBottom frame)
inset: 100 @ 110;
extent: 75 @ 30.
self addComponent: bounceBottom.
wrapAround := CheckBox withLabelString: 'Wrap Around'.
(wrapAround frame)
inset: 50 @ 140;
extent: 75 @ 30.
self addComponent: wrapAround.
Nothing very special there... And if we execute SpinButtonWork open, we see this:
Lance Burton
There is some basic behavior that automatically comes with a SpinButton. If you press the up or down buttons it automatically does number conversions, and adds or subtracts 1 from the current value. Holding down a button makes it repeat of course. Finally, if you enter some non numeric text and you then hit the up or down button, it takes that and quietly ignores it and starts as if your text were '0'.
Under the hood, there is a TextConverter in the model object of the SpinButton. All TextConverters have default #increment and #decrement methods. As an example, #increment in TextStringConverter calls #incrementBy: with the value of 1 as the argument, and that is written as:
incrementBy: anInteger
^value ifNotNil: [value := (value asNumber + anInteger) printString].
And this is why it takes non digit text and turns it into a '0'. If we entered something like 54.9454, and then hit the up button, this is what we'd see:
Match Game
We want to use numbers, not textified numbers, so first we'll start with changing the model our SpinButton uses:
hookupInterface
spinButton model: 0 asTextConverter.
Now let's hookup our RadioButtons with fancy all at once coolness, as well as our check box and its update method:
hookupInterface
| topModel bottomModel |
spinButton model: 0 asTextConverter.
topModel := [spinButton maximumValue: nil] asObservedValue.
noTopLimit model: topModel.
bounceTop model: topModel.
bounceTop selectValue: [spinButton maximumValue: 4].
noTopLimit selectValue: topModel value.
topModel when: ValueChanged do: [topModel value value].
bottomModel := [spinButton minimumValue: nil] asObservedValue.
noBottomLimit model: bottomModel.
bounceBottom model: bottomModel.
bounceBottom selectValue:
[spinButton minimumValue: -4].
noBottomLimit selectValue: bottomModel value.
bottomModel when: ValueChanged do: [bottomModel value value].
wrapAround when: ValueChanged send: #updateWrapAround to: self
updateWrapAround
spinButton wrapAround: wrapAround model value.
wrapAround model value ifTrue:
[spinButton minimumValue: -4.
spinButton maximumValue: 4]
As we stated in our Fun With RadioButtons post, we can do really cool things with RadioButtons besides just telling it that the #selectValue: is a symbol. It can be anything that the system can match with #='. Here, we use blocks which happily use identity when executing #='. Our trick is to then, when the change happens is to send #value to the value in the ObservedValue.
This sometimes takes a minute to get your mind around. Basically, we have an ObservedValue. To get at the underlying object that is being observed, we send #value to that. Since the underlying object is a block, when we send #value to a block, it executes itself. So, value value means: Get The Value And Then Execute The Block (which is the object we got from the first #value).
Not MOMA
Basically, #wrapAround: takes a Boolean and tells the SpinButton that if there is a maximum value for the top then go to the minimum value when you reach the maximum value when asked to increment. Similarly, if there is a minimum value for the bottom, then go to the maximum value when you reach the minimum value when asked to decrement.
By setting a minimum or maximum value, without setting wrapAround to true, you are in bounce mode. Simply stated, when you reach the maximum and do an increment, it doesn't. Similarly, when you reach the minimum and do a decrement, it doesn't.
You can play with the RadioButtons and the CheckBox and see what you get.
Extra Credit
You can use the SpinButton with Time objects, Date objects, Timestamp objects and even true and false.
Instead of:
0 asTextConverter
Try one of the following:
Time now asTextConverter
Date today asTextConverter
Timestamp now asTextConverter
true asTextConverter
Isn't that pretty?
And So It GoesSames