Tutorial-Creating External Functions with CDEFs

by Mike Smith 
LAMIR Software Corporation

The hot word in programming these days is modularity. HyperCard is a good example of an application that is designed to use modules of code written in another language to extend the limited functionality of HyperTalk: hence the names of these modules (XCMDs, or external commands, and XFNCs, or external functions).

You can do much the same thing for any application through the dialog box calling method — adding code to documents in the form of CDEFs.

A sample CDEF, or Control Definition, follows, writ- ten in MPW Pascal (Listing 1). It adds a Speak function to any (or all) dialog boxes in an application using MacinTalk. If the dialog box contains text that changes while the dialog box is up, the new text is spoken on every change.

This involves a technique that allows a CDEF to per- form some function in the background without being called explicitly. The technique can be used to make a CDEF do about anything you like, going far beyond its normal function — drawing buttons and check boxes and responding to mousedowns. (The Control Manager chapter of Inside Macintosh, Volume I describes the standard CDEFs on which this article is based.)

But Why? ∆ 

The purpose for writing this kind of CDEF was to extend the programming language of Acknowl- edgeTM by somehow adding code resources to its doc- uments. There wasn’t time to add an external function capability in the first release, but Acknowledge does have good dialog box support, including the ability to read and write to any dialog box text item, even con- trol titles. Since the CDEF can monitor changes to the control’s title, this is a way to pass parameters from and to the application.

The CDEF example in Listing 1 effectively adds a “SPEAK” command to any application that provides a way to change a control’s title. In Acknowledge, this is accomplished by using the “Set DBox” command to bring up the dialog box invisibly, and the “ItemText” command to speak any string or string expression. Other applications can use this CDEF to simple speak what’s in the dialog box when it appears.

Standard CDEFs ∆ 

If you are a beginner, CDEFs are nice because they are small pieces of code you can under- stand without knowing all of Inside Macintosh. If assembly language is your bag, you can learn much by studying the source to the famous button and scroll bar CDEFs (CDEF 0 and 1 in the System file) available on

AppleLink and Compuserve. An added plus is that you get Andy Hertzfeld as your teacher — he is the author of these gems. If you want to know more about standard CDEFs, this is an excellent starting point.

The Control Manager chapter of Inside Macintosh describes the various functions of CDEFs. One param- eter passed to CDEFs is a selector that directs which routine to run. The example given here only deals with two of them, the initialization and dispose routines. Others, such as the drawing routine, are simply left out and ignored. This is because the main function of this CDEF (speaking) is setup by the initialization routine, and no traditional CDEF functions are handled here.

Speaking CDEF Example ∆ 

If the text in the dialog box changes, MacinTalk speaks the new text each time it changes. This was accomplished by patching GetNex- tEvent and EventAvail. Other approaches could have been taken, but this is better because simply adding the CDEF resource and companion CNTL to the otherwise mute dialog box will make it speak. I have since used this same technique in other CDEFs and had no problems.

The CDEF initialization routine creates a block on the heap, moves it high and locks it. Then the trap patch routines are moved into that block, and the data stored there is initialized. Then GetNextEvent
and EventAvail traps are patched to pass through the “speaking” code. The dispose routine restores the traps and disposes the block.

The routine containing the trap patch must be moved out of the CDEF because the CDEF is continu- ally unlocked by the dialog manager. This is not good, because the patch code must reside in a locked block between normal CDEF calls. During the time the dialog box is up, the patch code continually gets the strings from dialog box items, and concatenates them. If any- thing changes, the string is spoken using
MacinTalk.

Trap Patching ∆ 

According to Macintosh Tech Note 212, only patch a trap if you must, and if you must it is better to patch a trap so the return address is preserved. This is because some system routines look to see who called it and do different things depending on this information. Most examples I’ve seen on patching traps use a JSR, but this example uses the preferred JMP. The disadvantage is that if we wanted to see the parame- ter returned by the trap (the event record, in this case) we couldn’t. We jump into the trap never to return. However, we have the address of the real EventAvail routine, so we could call that privately to peek at the event record if we really needed to, although the tech note advises against even doing that.

MacinTalk Simplified ∆ 

For those who, like myself, have not used MacinTalk because the documentation looks like too much reading for the benefits gained, here is the “executive summary.” To make a string speak all you need is the following procedure:

{Speak the string "s"}
var
v: SpeechHandle; h: Handle;
Err: SpeechErr;
begin {SpeakString}
Err := SpeechOn(noExcpsFiles, v);
h := NewHandle(0);
Err := Reader(v, Ptr(ord4(@s) + 1), length(s), h);
Err := MacinTalk(v, h);
DisposHandle(h);
SpeechOff(v);
end; {SpeakString}

This code produces the standard MacinTalk voice we all know and love, no frills. Note that you must link with “SpeechIntf.p” file, which is also available on Apple Link and Compuserve. You’ll also find the latest Macin- Talk driver there if you don’t have it.

Installing the CDEF with ResEdit ∆ 

A CDEF is linked to a dialog box though a CNTL resource. A CNTL contains fields such as the max and min values of the control. Rather than hard-coding anything into the CDEF, these fields in the CNTL define what is spoken. The CNTL’s “max” field is the item number in the DITL to be spo- ken first, and the “min” field is spoken last. Zero means don’t speak. If you type something into the “title” text box, it will be spoken between the “min” and the “max” text, assuming one is non-zero. If both are zero, just the control’s title is spoken.

To add a CDEF to a dialog box, you must create a CNTL first, reference the CDEF in the CNTL, then refer- ence the CNLT in the dialogs DITL item list.

First you create a CNTL with “min” “max” and “title” the way you want, then enter the CDEF number in the “procID” field. For example, if your CDEF is numbered CDEF 131, the “procID” field in the CNTL must be 131 * 16 or 2096. The CDEF number is always multiplied by 16 when referenced by a CNTL.

Next, create a new item in the DITL and choose the “CNTL Resource” radio button. Type the number of the CNTL created above in the text box. ResEdit is quite buggy in this area, and I’ve found that it’s best not to close any windows at this point but to close the file’s window instead and say YES to “SAVE CHANGES?” This indirectly closes the associated resource windows. Now the CDEF is linked to the dialog box.

If you want to add the CDEF to a document file for a dialog box that resides in the application, put a copy of the application’s DITL in the document. Then add the CNTL reference to the DITL in the document. The dialog manager looks first in the application’s document, then in the application for dialog box resources, so the document’s DITL will override the application’s.

MPW Hints ∆ 

CDEFs are usually small and compile fast anyway, but if you’re used to the slow turn around time of MPW, program development can proceed at blazing speed if you use the following technique. Use ResEdit to install a CNTL -8192 resource that references the CDEF you are working on in the ImageWriter or Laser- Writer printer driver, depending on what your Chooser is set to. Add a new item to the “Page Setup” dialog box resource (DLOG -8192) and make it a control item that references the CNTL above. Then, during program development, link the CDEF directly to the printer driver. After each compilation simply pull down on “Page Setup…” and up pops the dialog box containing the new CDEF. You never have to leave the MPW environment. It makes for quite spirited program development, especially in a non-MultiFinder environment. In fact, a large RAM Cache is of more use than MultiFinder in this case.

Conclusion ∆ 

I think this example points the way to other interesting ways to use CDEFs. For example, a CDEF could be used to animate a dialog box, communicate user actions to other applications running in Multi- Finder, play an ‘snd,’ change the volume level setting and do many simple desk accessory tasks. Some INITs would be better as CDEFs, since the CDEF is brought into memory when in use and dumped when finished, rather than taking up space in memory.

Best of all, working with CDEFs is fun. If you’ve been looking for a new twist in Macintosh program- ming, this should fill the bill.

Mike Smith is Vice President of Software Development at LAMIR Software Corporation in Vacaville, California. He wrote the original program that was to become Acknowledge, a high-end communications program. He has followed its development through Apple and publication by SuperMac as chief software engineer. He is currently working on follow-up products to Acknowledge as well as enhancing Acknowledge itself.

Listing 1

Editor’s note: An ellipse (...) at the beginning of a line indicates that the line is a continuation of the previous line, and was broken only to fit into MTQs 2-column format.

Unit ExampleCDEF;
{MPW 2.0 version}

INTERFACE
uses
	{$LOAD MQOTP.d}
	Memtypes, Quickdraw, OSIntf, ToolIntf, 
		...PackIntf, SpeechIntf;

function SpeakCDEF(selector: Integer;
		theControl: ControlHandle;
		message: Integer;
		 param: LongInt): LongInt;

IMPLEMENTATION

{$R-}
{$D-}

type		
	cDataType = record
		GNETrap:			LongInt;		
			{GetNextEvent save}
		EAVTrap:			LongInt;		
			{EventAvail save}
		DPtr:				WindowPtr;	
			{Dialog Window}
		IdleCount:		integer;		
			{Wait count so dlog can draw}
		BeforeText:		integer;		
			{DITL item # of 1st text to speak}
		AfterText:		integer;		
			{item # of last text to speak}
		MiddleStr:		Str255;		
			{Middle string to speak}
		SpokenStr:		Str255;		
			{Concat of text last spoken}
		ProcHolder:		byte;			
			{Dynamically expanded during}
		end; {record}						
			{runtime to hold speaking code}
	cDataPtr = ^cDataType;
	cDataHdl = ^cDataPtr;

TrapSelect = (EvtAvail, GetNxtEvt);

{These must be declared forward because}
{the main procedure must}
{appear first in CDEF program source.}

function MyEventAvail(eventMask: integer;
				var theEvent: EventRecord): boolean;
					forward;
function MyGetNextEvent(eventMask: integer;
				var theEvent: EventRecord): boolean;
					forward;
function ActionProc(Sel:TrapSelect): LongInt;
	forward;
function SpeakCDEF(selector: Integer;
	theControl: ControlHandle;
					message: Integer;
					param: LongInt): LongInt;
	type
		HdlPtr = ^Handle;
		HdlHdl = ^HdlPtr;
		
	var
		h:		Handle;
		h2:	Handle;
		s:		Str255;
		L:		LongInt;
		Err:	SpeechErr;

begin {SpeakCDEF}
HLock(Handle(theControl));
with theControl^^ do begin

	case message of 
		
		initCntl:  if contrlData = nil then begin
			{Calculate the size of speaking code}
			{following this procedure}
			L := GetHandleSize(contrlDefProc)
				- (ord4(@MyEventAvail)
				- ord4(@SpeakCDEF));
			
			h := NewHandle(sizeof(cDataType) + L);
			
			MoveHHi(h);	
			{Helps keep heap from fragmenting}
			HLock(h);		
			{Must be locked}
			{contains GetNextEvent patch}
			contrlData := h;
			
			{Create handle we can get}
			{to from the patch code}
			h2 := GetNamedResource
			...(‘DATA’, ‘Speak CDEF data’);
			
			{This resource should be added when}
			{the CDEF is installed}
			{In case it’s not present it is added}
			{now, but this assumes}
			{the CDEF’s resource file is}
			{the current resource file.}
			if h2 = nil then begin
				h2 := NewHandle(4);
				AddResource(h2, ‘DATA’, 128,
					‘Speak CDEF data’);
				end; {if}
			
			HNoPurge(h2);
			HdlHdl(h2)^^ := h;
			with cDataHdl(h)^^ do begin
				blockmove(@MyEventAvail, 
					@ProcHolder, L);	{Move in patch}
				EAVTrap := GetTrapAddress($A971);
				SetTrapAddress(ord4(@ProcHolder), 
				...$A971);
				GNETrap := GetTrapAddress($A970);
				SetTrapAddress((ord4(@ProcHolder)
					+ (ord4(@MyGetNextEvent)
					- ord4(@MyEventAvail))), $A970);
				IdleCount := 4;	
		{4 passes thru GNE gets dialog box drawn}
				DPtr := DialogPtr(contrlOwner);
				BeforeText := contrlMax;
				AfterText := contrlMin;
				MiddleStr := contrlTitle;
				SpokenStr := ‘’;
				end; {with}
			end; {initCntl}
		
		dispCntl: if contrlData <> nil then begin
		{Any kind of handle?}
			h := contrlData;	
			{Cant use packed field}
			with cDataHdl(h)^^ do begin
				if GNETrap <> 0 then begin
					SetTrapAddress(GNETrap, $A970);
					{Restore trap}
					SetTrapAddress(EAVTrap, $A971);
					{Restore trap}
					end; {if}
				end; {with}
			DisposHandle(contrlData);{Toss handle.}
			end; {dispCntl}
		end;
	end; {case}
HUnlock(Handle(theControl));
end; {SpeakCDEF}


procedure CallTrap(ProcAddr: LongInt);
inline
	$205F,	{	MOVEA.L	(A7)+, A0	}
	$4E5E,	{	UNLK		A6				}
	$4ED0;	{	JMP		(A0)			}
	

function MyEventAvail(eventMask: integer;
				var theEvent: EventRecord): boolean;
	
begin {MyEventAvail}
CallTrap(ActionProc(EvtAvail));
end; {MyEventAvail}


function MyGetNextEvent(eventMask: integer;
				var theEvent: EventRecord): boolean;
	
begin {MyGetNextEvent}
CallTrap(ActionProc(GetNxtEvt));
end; {MyGetNextEvent}


function GetDString(DPtr: DialogPtr;
	ItemNo: integer): Str255;
{Get string from dialg box item}
{Debugged and enhanced by outstanding}
{cellist at Claris and Brian Schipper}
	
	var
		h: Handle;
		ItemType: integer;
		r: Rect;
		s: Str255;

begin {GetDString}
s := ‘’;
if ItemNo > 0 then begin
	GetDItem(DPtr, ItemNo, ItemType, h, r);
	{Get handle to dialog item}
	if h <> nil then begin
		ItemType := BAnd(ItemType, $7F);
		{Strip out disabled flag}
		if ItemType in [statText,EditText] then
			GetIText(h, s)	
			{Get String from text box}
		else if BAnd(ItemType, $04) 
			...= ctrlItem then
			GetCTitle(ControlHandle(h), s);
			{Get string from control}
		end; {if}
	end; {if}
GetDString := s;
end; {GetDString}


procedure SpeakString(s: Str255);
{Speak the string “s”}
	
	var
		v:		SpeechHandle;
		h:		Handle;
		Err:	SpeechErr;
		
begin {SpeakString}
Err := SpeechOn(noExcpsFiles, v);
if Err <> noErr then 
{ManinTalk Driver not found}
else begin
	h := NewHandle(0);
	Err := Reader(v, Ptr(ord4(@s) + 1),
	...length(s), h);
	if Err = noErr then
		Err := MacinTalk(v, h);
	DisposHandle(h);
	SpeechOff(v);
	end; {if}
end; {SpeakString}


function ActionProc(Sel:TrapSelect): LongInt;
	
	type
		HdlPtr = ^cDataHdl;
		HdlHdl = ^HdlPtr;
		
	var
		h:	Handle;
		b:	boolean;
		s:	Str255;
		
begin {ActionProc}
h:=GetNamedResource(‘DATA’,’Speak CDEF data’);
with HdlHdl(h)^^^^ do begin
		{Not a typo}
	if IdleCount > 0 then 
	...IdleCount := IdleCount - 1
	else begin
		s := concat(
			GetDString(DPtr, BeforeText),	
			{First part of string}
			MiddleStr,					{Middle part}
			GetDString(DPtr, AfterText));	
			{Last part}
		if s <> SpokenStr then begin
		{Speak whenever it changes}
			SpeakString(s);
			SpokenStr := s;
			end; {if}
		end; {if}
	case Sel of
		EvtAvail:		ActionProc := EAVTrap;
		GetNxtEvt:	ActionProc := GNETrap;
		end; {case}
	end; {with}
end; {ActionProc}

end. {unit}

In the scripts below the printer driver is used as the test bed.  The CDEF is created as CDEF 131 and the CNTL is -8192.  The fields in the CNTL are defined so pulling down on PAGE SETUP under FILE speaks “This is the LaserWriter (or ImageWriter) Page Setup.”

The linker is directed to link to the printer driver as described above.  Substitute your own target file if not following the same steps.

#####################################
#	MPW Script for Speaking CDEF
#####################################
set CDEFFolder “{BOOT}Speak CDEF:”
set TargetFile 
	...”{BOOT}System Folder:LaserWriter”
Directory “{CDEFFolder}”
echo “Compiling {CDEFFolder}
	...SpeakCDEF.p at ‘date -t‘“
Pascal “{CDEFFolder}SpeakCDEF.p”
echo “Linking {CDEFFolder}SpeakCDEF.p.o 
	...to {TargetFile} at ‘date -t‘“
Link -rt CDEF=131 ∂
	-sn ‘Main’=’DBox Speak’ ∂
	-m SPEAKCDEF ∂
	-o “{TargetFile}” ∂
	“{CDEFFolder}SpeakCDEF.p.o” ∂
	“{CDEFFolder}SpeechIntf.o” ∂
	“{Libraries}”Interface.o ∂
	“{PLibraries}”Paslib.o

If you are using the script above to compile and link then this is CNTL needs to be installed in the printer driver.  This CNTL is set up so the phrase “This is the” proceeds whatever is in item 3 of the Page Setup dialog box.  Item 3 is either “LaserWriter Page Setup” or “ImageWriter Page Setup.”

derez “{boot}System Folder:LaserWriter” 
	...-only ‘CNTL’ Types.r
resource ‘CNTL’ (-8192) {
	{0, 0, 20, 20},
	0,
	invisible,
	0,					#DITL item of first text, 
								...zero means none
	3,					#DITL item of last text 
								...to be spoken
	2096,				#131 * 16, where 131 is 
								...the CDEF resource num
	0,
	“This is the “	#String to be spoken 
								...before item 3 above
};
Please follow and like us:

About the Author

A.P.P.L.E.

The A.P.P.L.E. Website is run by the Apple Pugetsound Program Library Exchange Users Group and is open to all Apple and Macintosh fans and their friends.