The Artima Developer Community
Sponsored Link

Agile Buzz Forum
How To Create A Custom Widget - Events - Validate Date Entry

0 replies on 1 page.

Welcome Guest
  Sign In

Go back to the topic listing  Back to Topic List Click to reply to this topic  Reply to this Topic Click to search messages in this forum  Search Forum Click for a threaded view of the topic  Threaded View   
Previous Topic   Next Topic
Flat View: This topic has 0 replies on 1 page
James Robertson

Posts: 29924
Nickname: jarober61
Registered: Jun, 2003

David Buck, Smalltalker at large
How To Create A Custom Widget - Events - Validate Date Entry Posted: Nov 24, 2004 6:11 PM
Reply to this message Reply

This post originated from an RSS feed registered with Agile Buzz by James Robertson.
Original Post: How To Create A Custom Widget - Events - Validate Date Entry
Feed Title: Pollock
Feed URL: http://www.cincomsmalltalk.com/rssBlog/pollock-rss.xml
Feed Description: Pollock - the next VW GUI
Latest Agile Buzz Posts
Latest Agile Buzz Posts by James Robertson
Latest Posts From Pollock

Advertisement

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:

	Calendar class>>constructEventsTriggered
		^super constructEventsTriggered
			add: #clicked;
			add: #rightClicked;
			add: #doubleClicked;
			add: #aboutToGetFocus;
			add: #gettingFocus;
			add: #aboutToLoseFocus;
			add: #losingFocus;
			add: #tabbed;
			add: #backTabbed;
			yourself

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:

	CalendarTest>>eventValue: aSymbol 
		"This in effect traps for events being sent twice!"
		eventValue := eventValue notNil 
			ifTrue: [(eventValue , aSymbol) asSymbol] 
			ifFalse: [eventValue := aSymbol]
===
	CalendarTest>>testClicked
		self openWindowWithCalendar.
		calendar when: #clicked send: #eventValue: to: self with: #clicked.
		self pressAndReleaseRedButtonAt: calendar origin + 2.
		self should: [eventValue == #clicked]
===
	CalendarTest>>testDoubleClicked
		self openWindowWithCalendar.
		calendar when: #doubleClicked send: #eventValue: to: self with: #doubleClicked.
		self doubleClickRedButtonAt: calendar origin + 2.
		self should: [eventValue == #doubleClicked]
===
	CalendarTest>>testRightClicked
		self openWindowWithCalendar.
		calendar when: #rightClicked send: #eventValue: to: self with: #rightClicked.
		self pressAndReleaseYellowButtonAt: calendar origin + 2.
		self should: [eventValue == #rightClicked]
===
	CalendarTest>>testTabbed
		self openWindowWithCalendar.
		calendar when: #tabbed send: #eventValue: to: self with: #tabbed.
		self pressKey: Character tab.
		self should: [eventValue == #tabbed]
===
	CalendarTest>>testBackTabbed
		self openWindowWithCalendar.
		calendar when: #backTabbed send: #eventValue: to: self with: #backTabbed.
		self pressShiftedKey: Character tab.
		self should: [eventValue == #backTabbed]
===
	CalendarTest>>testGettingFocus
		self openWindowWithCalendar.
		calendar when: #gettingFocus send: #eventValue: to: self with: #gettingFocus.
		self pressKey: Character tab.
		self should: [eventValue == #gettingFocus]
===
	CalendarTest>>testLosingFocus
		self openWindowWithCalendar.
		calendar when: #losingFocus send: #eventValue: to: self with: #losingFocus.
		self pressKey: Character tab.
		self should: [eventValue == #losingFocus]
===
	CalendarTest>>testAboutToLoseFocus
		self openWindowWithCalendar.
		calendar when: #aboutToLoseFocus send: #eventValue: to: self with: #aboutToLoseFocus.
		self pressKey: Character tab.
		self should: [eventValue == #aboutToLoseFocus]	
===
	CalendarTest>>testAboutToGetFocus
		self openWindowWithCalendar.
		calendar when: #aboutToGetFocus send: #eventValue: to: self with: #aboutToGetFocus.
		self pressKey: Character tab.
		self should: [eventValue == #aboutToGetFocus]
===
	CalendarTest>>testAboutToGetFocusVeto
		self openWindowWithCalendar.
		calendar when: #aboutToGetFocus send: #raiseVeto to: self.
		calendar when: #gettingFocus send: #eventValue: to: self with: #gettingFocus.
		self pressKey: Character tab.
		self should: [eventValue isNil]	
===
	CalendarTest>>testAboutToLoseFocusVeto
		self openWindowWithCalendar.
		calendar when: #aboutToLoseFocus send: #raiseVeto to: self.
		calendar when: #losingFocus send: #eventValue: to: self with: #losingFocus.
		self pressKey: Character tab.
		self should: [eventValue isNil]

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:

createDisplayPart
	displayPart := InputField new.
	displayPart frame: (FractionalFrame 
		fractionLeft: 0
		top: 0
		right: 1
		bottom: 1).
	displayPart frame rightOffset: [self artist buttonWidth negated].
	displayPart interiorDecoration: nil.
	displayPart setEnclosingPane: self.
	self isOpen
		ifTrue: [displayPart setupKeyboardFor: self]
		ifFalse: [self configure: #setupKeyboardFor: for: displayPart argument: self].
	displayPart when: #clicked send: #triggerEvent: to: self with: #clicked.
	displayPart when: #rightClicked send: #triggerEvent: to: self with: #rightClicked.
	displayPart when: #doubleClicked send: #triggerEvent: to: self with: #doubleClicked.
	displayPart when: #aboutToLoseFocus send: #triggerEvent: to: self with: #aboutToLoseFocus.
	displayPart when: #losingFocus send: #triggerEvent: to: self with: #losingFocus.
	displayPart agent doNotIncludeInTabList.
	self when: #gettingFocus send: #activate to: displayPart.
	self when: #losingFocus send: #deactivate to: displayPart

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:

	CalendarTest>>openWindowWithCalendarAndButton
		| button |
		self openWindowWithCalendar.
		button := Button new.
		button frame origin: 50 @ 50.
		button frame extent: 50 @ 50.
		window addComponent: button

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:

	initialize
		super initialize.
		self interiorDecoration: self getInteriorDecoration.
		self createDisplayPart.
		self updateDateDisplay.
		self setupValidationEvents.

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.


And So It Goes
Sames

Read: How To Create A Custom Widget - Events - Validate Date Entry

Topic: And the crowd goes wild! Previous Topic   Next Topic Topic: cargo cult coding practices

Sponsored Links



Google
  Web Artima.com   

Copyright © 1996-2019 Artima, Inc. All Rights Reserved. - Privacy Policy - Terms of Use