Modal Filter 2
Volume Number: | | 9
|
Issue Number: | | 8
|
Column Tag: | | Getting Started
|
Related Info: Dialog Manager Event Manager
The Modal Dialog Filter - Part II
Preprocessing events for a modal dialog.
By Dave Mark, MacTech Magazine Regular Contributing Author
By the time you read this, summer will be reaching towards the fall and MacWorld Expo will be on everybodys mind. If you read this before (or at) MacWorld expo, stop by and say hello. Ill be at the Addison-Wesley booth and at MacTech Live! Id love to hear from you. By the way, has anyone picked up a copy of Learn C++ on the Macintosh yet? If you have, drop me a line on CompuServe or AOL and let me know what you think.
Last month, we entered and ran a program called DLOGFilter. DLOGFilter implemented a dialog box containing two text-edit fields (see Figure 1). The first field (labeled Ten chars max:) limited input to a maximum of 10 characters. The second text-edit field (labeled Number (1-100):) limited input to numeric characters only.
Figure 1. The DLOGFilter dialog box.
The dialog box requires that you enter a number between 1 and 100 in the Number field. If you click the OK button before a legal number is entered, a warning message is displayed (see Figure 2) and the dialog sticks around.
Figure 2. Heres what happens when you click the OK button without entering a number.
The key to this program is the filter procedure, or filterproc, used to preprocess all events before they are passed on to the Dialog Manager. In this case, we are interested in intercepting all keyDown and autoKey events before the Dialog Manager interprets them as text-edit keystrokes. Youll see how to do this as we walk through the DLOGFilter code.
Walking Through the DLOGFilter code
DLOGFilter starts off with a host of #defines. Youll see these again as they are used throughout the code. I know this is obvious, but heres a word or two about the naming convention I use in my code. Start all constants with a lower-case k, with two exceptions. Menu and dialog items start with a lower-case i and menu resource ids start with a lower case m. Notice that the remainder of the constant name is spelled according to Pascal rules, as opposed to C rules. Pascal starts each word with an upper-case letter, while C traditionally uses all upper-case letters, separating each word by an underscore (_). I think the Pascal method is much easier to read. As it happens, most of Apples C code uses the Pascal convention as well.
/* 1 */
#define kDialogResID 128
#define kMBARid 128
#define kMessageAlertID 129
#define kSleep 60L
#define kMoveToFront (WindowPtr)-1L
#define kNULLFilterProc (ProcPtr)0L
#define kOn 1
#define kOff0
#define kEditItemExists true
#define kEventNotHandledYet false
#define kEventHandledtrue
#define kMaxFieldLength 10
#define kEnterKey3
#define kBackSpaceKey8
#define kTabKey 9
#define kReturnKey 13
#define kEscapeKey 27
#define kLeftArrow 28
#define kRightArrow29
#define kUpArrow 30
#define kDownArrow 31
#define kPeriodKey 46
#define kDeleteKey 127
#define iTenCharMaxText 4
#define iNumberText6
#define mApple 128
#define iAbout 1
#define mFile 129
#define iDialog 1
#define iQuit 3
As usual, weve declared the global gDone as a Boolean to tell us when to drop out of the main event loop. As always, we start global variables with the letter g. There are lots of other naming conventions for variables. For example, some folks start their variables with a letter indicating the type of the variable. This can come in handy if you are writing code that gets shared among a group of people.
/* 2 */
/*************/
/* Globals */
/*************/
Boolean gDone;
Heres the function prototypes for every single routine in the program. Get in the habit of providing function prototypes for all your routines. Since C++ requires function prototypes, this is a good habit to get into.
/* 3 */
/***************/
/* Functions */
/***************/
void ToolboxInit( void );
void MenuBarInit( void );
void EventLoop( void );
void DoEvent( EventRecord *eventPtr );
void HandleMouseDown( EventRecord *eventPtr );
void HandleMenuChoice( long menuChoice );
void HandleAppleChoice( short item );
void HandleFileChoice( short item );
void DoDialog( void );
Heres an unusual prototype. Look at the return type for the function DLOGFilter().
/* 4 */
pascal Boolean DLOGFilter( DialogPtr dialog, EventRecord *eventPtr, short
*itemHitPtr );
The pascal keyword tells the compiler that this routine should be called using Pascal, as opposed to C, calling conventions. Heres why this is important. When your code calls a regular C function, the compiler has no trouble using the C function-calling conventions. When you call a Toolbox function, the rules change a bit. Since the Toolbox was originally written in Pascal, all calls to it are made using the Pascal calling conventions. When you call a Toolbox function from your code, the compiler is smart enough to use the Pascal convention to pass parameters and return the return value to your code. The compiler knows to do this because the Toolbox function prototypes use the pascal keyword.
Where things get tricky is when you write a function in C that youd like to be called by a Toolbox function. For example, in this program, were creating a function that will be called, periodically, by ModalDialog(). Which conventions do we use, C or Pascal? As it turns out, we yield to the Toolbox and declare our function using the pascal keyword. Its not important to understand the difference between C and Pascal calling conventions, just as long as you remember this rule: If your routine is designed to be called by the Toolbox, be sure to declare it using the pascal keyword.
Heres the rest of the function prototypes.
/* 5 */
Boolean ScrapIsOnlyDigits( void );
Boolean CallFilterProc( DialogPtr dialog, EventRecord
*eventPtr, short *itemHitPtr );
short CurEditField( DialogPtr dialog );
short SelectionLength( DialogPtr dialog );
void Message( Str255 messageStr );
Heres some routines that arrived too late to be added to the THINK C 5 header files. They are part of System 7. The first three should be familiar to you from previous columns. The fourth will be used to retrieve the address of the default ModalDialog() filter procedure. Youll see how this is used later on in the program.
/* 6 */
/* see tech note 304 */
pascal OSErr SetDialogDefaultItem( DialogPtr theDialog,
short newItem )
= { 0x303C, 0x0304, 0xAA68 };
pascal OSErr SetDialogCancelItem( DialogPtr theDialog,
short newItem )
= { 0x303C, 0x0305, 0xAA68 };
pascal OSErr SetDialogTrackCursor( DialogPtr theDialog,
Boolean tracks )
= { 0x303C, 0x0306, 0xAA68 };
pascal OSErr GetStdFilterProc( ModalFilterProcPtr *theProc )
= { 0x303C, 0x0203, 0xAA68 };
main() initializes the Toolbox and menu bar, then enters the main event loop. Notice the new spelling of ToolboxInit(). (I used to spell it ToolBoxInit() - Aack!)
/* 7 */
/************************************* main */
void main( void )
{
ToolboxInit();
MenuBarInit();
EventLoop();
}
Nothing new here...
/* 8 */
/************************************* ToolboxInit */
void ToolboxInit( void )
{
InitGraf( &thePort );
InitFonts();
InitWindows();
InitMenus();
TEInit();
InitDialogs( NULL );
InitCursor();
}
Not too much new here. This time I added a constant for the MBAR resource id.
/* 9 */
/************************************* MenuBarInit */
void MenuBarInit( void )
{
Handle menuBar;
MenuHandle menu;
menuBar = GetNewMBar( kMBARid );
SetMenuBar( menuBar );
menu = GetMHandle( mApple );
AddResMenu( menu, 'DRVR' );
DrawMenuBar();
}
Same old, same old...
/* 10 */
/************************************* EventLoop */
void EventLoop( void )
{
EventRecordevent;
gDone = false;
while ( gDone == false )
{
if ( WaitNextEvent( everyEvent, &event, kSleep, NULL ) )
DoEvent( &event );
}
}
Since our program only supports menus and a single dialog, well only handle a few events. The dialog window events will be handled by the Dialog Manager.
/* 11 */
/************************************* DoEvent */
void DoEvent( EventRecord *eventPtr )
{
char theChar;
switch ( eventPtr->what )
{
case mouseDown:
HandleMouseDown( eventPtr );
break;
case keyDown:
case autoKey:
theChar = eventPtr->message & charCodeMask;
if ( (eventPtr->modifiers & cmdKey) != 0 )
HandleMenuChoice( MenuKey( theChar ) );
break;
}
}
Last months version of HandleMouseDown() included code for dragging a window around on the screen. Since we dont have any windows, I took the liberty of deleting the offending lines. Sorry about the extra typing.
/* 12 */
/************************************* HandleMouseDown */
void HandleMouseDown( EventRecord *eventPtr )
{
WindowPtrwindow;
short thePart;
long menuChoice;
thePart = FindWindow( eventPtr->where, &window );
switch ( thePart )
{
case inMenuBar:
menuChoice = MenuSelect( eventPtr->where );
HandleMenuChoice( menuChoice );
break;
case inSysWindow :
SystemClick( eventPtr, window );
break;
}
}
Pretty standard menu handling code. HandleMenuChoice() dispatches the menu selection...
/* 13 */
/************************************* HandleMenuChoice */
void HandleMenuChoice( long menuChoice )
{
short menu;
short item;
if ( menuChoice != 0 )
{
menu = HiWord( menuChoice );
item = LoWord( menuChoice );
switch ( menu )
{
case mApple:
HandleAppleChoice( item );
break;
case mFile:
HandleFileChoice( item );
break;
}
HiliteMenu( 0 );
}
}
HandleAppleChoice() handles selections from the menu. Feel free to add an about alert of your own design.
/* 14 */
/************************************* HandleAppleChoice */
void HandleAppleChoice( short item )
{
MenuHandle appleMenu;
Str255 accName;
short accNumber;
switch ( item )
{
case iAbout:
SysBeep( 20 );
break;
default:
appleMenu = GetMHandle( mApple );
GetItem( appleMenu, item, accName );
accNumber = OpenDeskAcc( accName );
break;
}
}
HandleFileChoice() handles the File menu selections. The first item in the File menu is the Dialog... item, which brings up our filtered dialog. The dialog is handled by the routine DoDialog().
/* 15 */
/************************************* HandleFileChoice */
void HandleFileChoice( short item )
{
switch ( item )
{
case iDialog:
DoDialog();
break;
case iQuit:
gDone = true;
break;
}
}
DoDialog() starts by loading the DLOG resource using GetNewDialog().
/* 16 */
/************************************* DoDialog */
void DoDialog( void )
{
DialogPtrdialog;
BooleandialogDone = false;
short itemHit, iType;
Handle iHandle;
Rect iRect;
Str255 numberStr;
long number;
Strictly speaking, you should check the value returned by GetNewDialog(). This will keep you from an embarassing crash if the DLOG resource couldnt be loaded for some reason (like theres no more memory left, or the darn thing just wasnt there).
/* 17 */
dialog = GetNewDialog( kDialogResID, NULL, kMoveToFront );
As usual, we make the dialog visible, make it the current port, then call the three new System 7 routines.
/* 18 */
ShowWindow( dialog );
SetPort( dialog );
SetDialogDefaultItem( dialog, ok );
SetDialogCancelItem( dialog, cancel );
SetDialogTrackCursor( dialog, kEditItemExists );
Check out the tech note that describes these new routines. You can find them on the developer CDs and, Ill bet, on-line somewhere. If you cant find it, write to Neil and maybe hell include them on the next MacTech CD.
// Be sure to read tech note #304 which covers
// these three routines
With the dialog window visible, we can enter the main dialog loop. Well drop out of the loop once dialogDone is set to true.
/* 19 */
while ( ! dialogDone )
{
The loop consists of a call to ModalDialog() and a switch to interpret the result returned in itemHit. The first parameter is a pointer to a filter function. Our filter function is named DLOGFilter. Note the lack of parentheses in the function name. If we included the parens, the function would be called in place and the return value would be passed as the first parameter to ModalDialog(). Well get to DLOGFilter() in a minute.
/* 20 */
ModalDialog( DLOGFilter, &itemHit );
switch( itemHit )
{
If the OK button was clicked, well call GetDItem() and GetIText() to retrieve the text in the Number (1-100): text-edit field.
/* 21 */
case ok:
GetDItem(dialog, iNumberText, &iType, &iHandle, &iRect );
GetIText( iHandle, numberStr);
If the field is empty, well print a message asking the user to enter a number in the field.
/* 22 */
if ( numberStr[ 0 ] == 0 )
{
Message("\pYou must enter a number in the number field!");
}
Otherwise, well convert the text in the field into a number, then test to see if the number is between 1 and 100. If so, we can drop out of the loop.
/* 23 */
else
{
StringToNum( numberStr, &number );
if ( (number >= 1) && (number <= 100) )
dialogDone = true;
If the number is not in the required range, well print the appropriate message, then highlight all the text in the field.
/* 24 */
else
{
Message("\pPlease enter a number between 1 and 100..." );
SelIText( dialog, iNumberText, 0, 32767 );
}
}
break;
How can we be sure that the text in the field is a number? As youll see, thats part of the job of the filter procedure. DLOGFilter() makes sure that only numeric characters are entered in the number field.
If the Cancel button was pressed, well drop out of the dialog loop.
/* 25 */
case cancel:
dialogDone = true;
break;
}
}
Once out of the loop, well free up the memory occupied by the dialog, then print an appropriate message.
/* 26 */
DisposDialog( dialog );
if ( itemHit == ok )
Message( "\pYour number was valid!!!" );
else
Message( "\pDialog cancelled..." );
}
DLOGFilter() gets called every time ModalDialog() encounters an event. Pointers to the dialog and event are passed as the first two parameters. The third parameter allows DLOGFilter() to set the value of itemHit.
If the event is handled by DLOGFilter() (and we want ModalDialog() to ignore it) well return a value of true, being sure to set the value of itemHit first (via itemHitPtr). If we didnt handle the event, well return false, asking ModalDialog() to process the event.
/* 27 */
/************************************* DLOGFilter */
pascal Boolean DLOGFilter( DialogPtr dialog, EventRecord *eventPtr,
short *itemHitPtr )
{
char c;
short iType;
Handle iHandle;
Rect iRect;
Str255 textStr;
long scrapLength, scrapOffset;
short selecLength;
In this program, were only interested in keyDown and autoKey events. Feel free to add whatever events you like to the switch.
/* 28 */
switch ( eventPtr->what )
{
case keyDown:
case autoKey:
If the key pressed was one of those in the if clause, well call the default filter procedure (the one ModalDialog() calls if we dont provide one), returning the result returned by the default filterproc.
/* 29 */
c = (eventPtr->message & charCodeMask);
if ( (c == kReturnKey) || (c == kEnterKey) ||
(c == kTabKey) || (c == kBackSpaceKey) ||
(c == kEscapeKey) || (c == kLeftArrow) ||
(c == kRightArrow) || (c == kUpArrow) ||
(c == kDownArrow) || (c == kDeleteKey) )
{
return(CallFilterProc( dialog, eventPtr, itemHitPtr ));
}
Otherwise, well check to see if the edit cursor is inside the Ten chars max: field.
/* 30 */
else if ( CurEditField( dialog ) == iTenCharMaxText )
{
If so, well retrieve the text from that field.
/* 31 */
GetDItem(dialog, iTenCharMaxText, &iType,
&iHandle, &iRect);
GetIText( iHandle, textStr);
Next, well find out how many characters in that field are currently selected.
/* 32 */
selecLength = SelectionLength( dialog );
If the current event represents the key sequence V, well check to make sure the text in the clipboard wont push us over our ten character limit.
/* 33 */
if ( ( (eventPtr->modifiers & cmdKey) != 0) &&
(c == 'v') )
{
First, well call GetScrap() to find out how many characters are in the clipboard. To programmers, the clipboard is known as the scrap, thus the name GetScrap(). Since the scrap can contain many types of data, we need to specify that we are interested in TEXT data (as opposed to PICT data, for example).
/* 34 */
scrapLength = GetScrap( NULL, 'TEXT', &scrapOffset );
If the text thats in the current field, plus the length of the scrap, minus the selection length (remember, the selection will be replaced by whatever is pasted) exceeds the 10 char limit, well beep and return true.
/* 35 */
if (textStr[0]+ scrapLength - selecLength > kMaxFieldLength)
{
SysBeep( 20 );
*itemHitPtr = iTenCharMaxText;
return( kEventHandled );
}
Otherwise, well let the default filterproc handle the paste.
/* 36 */
else
return(CallFilterProc( dialog, eventPtr, itemHitPtr) );
}
If the field is full and no characters are selected (and thus replaced by the typed character), well beep and return false.
/* 37 */
if ((textStr[ 0 ] == kMaxFieldLength) && (selecLength == 0))
{
SysBeep( 20 );
return( kEventHandled );
}
Otherwise, well let the default filterproc handle the event normally.
/* 38 */
else
return( CallFilterProc( dialog, eventPtr, itemHitPtr ) );
}
So much for the Ten chars max: field. Now well handle an event that occurs when the current field is the Number (1-100): field.
/* 39 */
else if ( CurEditField( dialog ) == iNumberText )
{
This next line is superfluous. Feel free to delete it.
/* 40 */
GetDItem( dialog, iNumberText, &iType,
&iHandle, &iRect );
Once again, well retrieve the length of the current selection.
/* 41 */
selecLength = SelectionLength( dialog );
Next, well check to see if a V was typed. By the way, the Dialog Manager will convert a selection of Paste from the Edit menu to a V for us. Try it. This code should still work.
/* 42 */
if ( ( (eventPtr->modifiers & cmdKey) != 0) &&
(c == 'v') )
{
If V was typed, well check to be sure the scrap contains only digits. If so, well let the default filterproc handle the paste.
/* 43 */
if ( ScrapIsOnlyDigits() )
{
return( CallFilterProc( dialog, eventPtr,
itemHitPtr ) );
}
Otherwise, well beep and return.
/* 44 */
else
{
SysBeep( 20 );
*itemHitPtr = iNumberText;
return( kEventHandled );
}
}
If the character typed was not a digit, and the command key was not held down, well beep and return.
/* 45 */
else if ( ((c < '0') || (c > '9')) &&
( (eventPtr->modifiers & cmdKey) == 0) )
{
SysBeep( 20 );
*itemHitPtr = iNumberText;
return( kEventHandled );
}
If the character typed was a digit, or if a command-key sequence of any type was entered, well let the default filterproc handle it.
/* 46 */
else
{
return( CallFilterProc( dialog, eventPtr, itemHitPtr ) );
}
}
break;
}
If anything else slips through, well let the default filterproc handle it.
/* 47 */
return( CallFilterProc( dialog, eventPtr, itemHitPtr ) );
}
You may have noticed that we started the filter off by checking for characters like return, enter, tab, delete, and the arrow keys. These are context-free keys. In other words, their importance is not related to the current field. We try to get those out of the way first, before we start checking for input related to a specific field.
ScrapIsOnlyDigits() checks the contents of the scrap to make sure each character is a digit, between 0 and 9.
/* 48 */
/************************************* ScrapIsOnlyDigits */
Boolean ScrapIsOnlyDigits( void )
{
Handle textHandle;
long scrapLength, scrapOffset;
BooleanonlyDigits = true;
unsigned short i;
First, well allocate a new, minimum-sized handle. The Macs Memory Manager will allocate the minimum size block of memory, then return a pointer to a pointer to the block. Well get into handles in a later column. For the moment, just bear with me.
/* 49 */
textHandle = NewHandle( 0 );
We pass that handle to GetScrap(), asking it to retrieve data of type TEXT from the scrap, resizing the handled block to a size appropriate to hold the retrieved text.
/* 50 */
scrapLength = GetScrap( textHandle, 'TEXT', &scrapOffset );
If the scrap was empty, or if text couldnt be retireved (scrapLength was negative), well return false.
/* 51 */
if ( scrapLength <= 0 )
return( false );
Since we are about to singly dereference the handle, we have to lock it. Once again, well talk about this in a future column.
/* 52 */
HLock( textHandle );
Next, well walk through the text, checking for non-digits.
/* 53 */
for ( i=0; i<scrapLength; i++ )
{
if (((*textHandle)[i] < '0') || ((*textHandle)[i] > '9'))
onlyDigits = false;
}
Next, unlock the handle, release the memory, and return the result.
/* 54 */
HUnlock( textHandle );
DisposHandle( textHandle );
return( onlyDigits );
}
CallFilterProc() makes use of the fourth System 7 routine mentioned at the very top of the program.
/* 55 */
/************************************* CallFilterProc */
Boolean CallFilterProc( DialogPtr dialog, EventRecord *eventPtr, short
*itemHitPtr )
{
ModalFilterProcPtrtheModalProc;
OSErr myErr;
GetStdFilterProc() retrieves the default filterproc. The default filterproc takes care of things like drawing the outline around the default button, checking for the cancel key-equivalents, and changing the cursor to the i-beam when the cursor is over a text-edit field.
/* 56 */
myErr = GetStdFilterProc(&theModalProc);
if (myErr == noErr)
return( theModalProc( dialog, eventPtr, itemHitPtr ) );
else
return( kEventNotHandledYet );
}
CurEditField() peeks into the dialogs data structure and returns the adjusted value hidden in the editField field.
/* 57 */
/************************************* CurEditField */
short CurEditField( DialogPtr dialog )
{
return( ((DialogPeek)dialog)->editField + 1 );
}
SelectionLength() starts by retrieving the current TEHandle from the specified dialog. The TEHandle is a handle to the struct describing the current text-edit field. The selEnd and selStart fields describe the position of the end and beginning of the text selection.
/* 58 */
/************************************* SelectionLength */
short SelectionLength( DialogPtr dialog )
{
TEHandle teH;
teH = ((DialogPeek)dialog)->textH;
return( (**teH).selEnd - (**teH).selStart );
}
Message() puts up an alert containing the specified text string.
/************************************* Message */
void Message( Str255 messageStr )
{
short unused;
ParamText( messageStr, "\p", "\p", "\p" );
unused = NoteAlert( kMessageAlertID, kNULLFilterProc );
}
Till Next Month...
As an exercise, try changing the program so the OK button is dimmed unless a legal number is entered. Youll need to check and adjust the OK button inside the filterproc.
Where to next month? Im not sure yet. Im vaccilating between a column on user interface design and one on memory management. Till then, enjoy MacWorld, and let me know what you think of Learn C++ on the Macintosh.