Today we make it possible to enter a date in the input field portion of our Calendar, and have it change the underlying date model... But first, a bug fix
EditingAgent>>clearDropDown
"The ding close is a cute way of making sure that we don't just open another popup on the same pane on a second
mouse click. We do a simple assignment below to keep the darned compiler from complaining in the Transcript while loading "
| dingClose |
pane triggerEvent: #popupMenuClosed.
keyboardProcessor ifNotNil:
[keyboardProcessor currentConsumer = self paneController ifFalse: [keyboardProcessor currentConsumer: self paneController].
pane isOpen ifTrue: [self activate]].
menuWindow ifNotNil: [menuWindow releaseWindowManager].
menuWindow := nil.
Cursor normal show.
dingClose := nil.
dingClose :=
[(Delay forMilliseconds: 200) wait.
menuJustOpened := false.
dingClose terminate] fork.
Design
We need to understand a little bit about how input fields work in relation to their model objects. Upon every change you make to an Input Field, the model for that pane is updated. In the old Wrapper framework, this behavior was called continuous accept. A set of changing events is triggered with each change. First, an #aboutToChange event, which is vetoable, then a #changing event before the actual change occurs if the #aboutToChange isn't vetoed, and finally, a #changed event. If we were to put validation and update code on those events, we would be in deep trouble. Consider the use case where the user saw something like "11/12/2003" and selected the "12", and hit the delete key. This would result in a change that wouldn't result in a good date. Not accepting that change would be wrong since the user is possibly going to enter a number between the "/"s.
Instead of validating at each input field change, it is more appropriate for us to do our validation and updates when the user attempts to exit the focus of the pane. This will happen even if the Calendar is the only pane on the window, since trying to close the window will cause the pane to attempt to lose focus.
Similar to the events triggered on changing the input field, focus events have a pair of events. First, #aboutToLoseFocus is triggered, and this is vetoable. Then if the #aboutToLoseFocus isn't vetoed, a #losingFocus event is triggered as a notification. We'll use both of these in our Calendar. First, we'll validate on the #aboutToLoseFocus event, testing the input field's value to see if we can get a valid date out of it. If we can't, we'll veto, forcing the focus not to change. We'll then update thee actual Calendar date on the #loseFocus event.
Before we can do this, we need to tell the Calendar what events it can trigger, otherwise all of our work will be for naught.
Events
We can have our pane trigger any number of events, but if someone wants to subscribe to them, we have to tell the pane what events it triggers, otherwise the pane will reject, at runtime, any such subscription. We tell a pane (or any object) what events it triggers by creating a class method named #constructEventsTriggered. Here's the one for Calendar:
These are pretty much standard events for most panes that can get focus and react to mouse clicks. Now that we've told the pane what events it can trigger, let's write tests to see if they work. Here they are, with the support methods for testing events, without much comment:
Now, if we run our tests... well, not so good. Three failed! The reason is that the input field, while itself will properly trigger clicked events, doesn't by itself forward them to the enclosing Calendar pane. So, we add a bit of code to the creation of the input field:
Our tests now run successfully to completion. Note that we also forward the important focus events to the Calendar. We also tell the display part, (Input Field) to not be part of the tab list, otherwise, it will be an extra tab to get from the "Calendar" to it's embedded InputField. Finally, we reverse forward the getting and losing focus events of the Calendar itself to activate and deactivate the InputField. All in a days work.
While we're at it, we'll create another tester, that has both a Calendar and a Button, so we can see what happens when we do validation:
We'll start off with the validation of the date that may be entered. We'll write that code in one method we'll call #setupValidationEvents:
Calendar>>setupValidationEvents
self when: #aboutToLoseFocus send: #validateInput to: self
We now need to write a #validateInput method:
Calendar>>validateInput
[Date readFrom: self displayPart model value readStream]
on: Error
do: [:error | VetoAction raise]
Here, we just quietly refuse to lose focus if the date, if read from the model of the InputField isn't valid. Here we're relying on the behavior of the #readFrom: of the Date class to throw an exception if the date string isn't valid.
Lastly, we need to add the call to #setupValidationEvents to our initialize method:
Now we can open our new tester and see what happens:
CalendarTest new openWindowWithCalendarAndButton
If we click in the input field portion of our calendar, then hit tab, we see focus changes to the button. If we delete the day or month portion, and then tab, we see that the focus stays right where it was. In fact, we can enter any kind of date format that the Date class allows, so entering "November 12, 1982" is valid (on the US Locale at least). But entering "Bobby Loves Alice" isn't.
Updating
If the validation passes, we want to update the Calendar's date. We start off by adding a subscription to the losingFocus event:
Calendar>>setupValidationEvents
self when: #aboutToLoseFocus send: #validateInput to: self.
self when: #losingFocus send: #updateDate to: self.
Then we write our updateDate method:
Calendar>>updateDate
model value: (Date readFrom: self displayPart model value readStream).
self updateDateDisplay.
Note here how we call our #updateDateDisplay method. Thus, if someone enters "Nov 12, 2001" it changes to the "11/12/01" display.
The above is published as version 1.16 in the Package named "Pollock-Calendar" on the Cincom public repository.
Next time we'll start dealing with the issues of using this Pane in a MacOSX look and a Motif look.