XCMD Libraries
Volume Number: | | 5
|
Issue Number: | | 6
|
Column Tag: | | HyperChat
|
XCMD Corner: XCMD Libraries
By Donald Koscheka, Arthur Young & Co., MacTutor Contributing Editor
Modularity and Coupling
As you to explore XCMDs, you should continue to discover new and interesting properties of these routines. Two properties are particularly noteworthy because they lead to code that is usable outside of Hypercard. They are strong modularity and weak coupling.
You determine the modularity of a routine by counting the number of tasks it performs. Strongly modular routines handle one task. If you were writing a spreadsheet program, you would most likely separate the routine that calculates the value of a cell from the routine that displays that value. Combining these functions results in weak modularity. Weakly modular code is hard to debug because you can easily lose track of what the routine is doing at a given point.
Coupling describes how the program interacts with the outside world. Weakly coupled routines operate only on data passed as formal parameters. Weak coupling eliminates harmful side-effects caused by relying on global memory. Weakly coupled programs force you to think over how and when to pass data back and forth.
Well written XCMDs exhibit both strong modularity and weak coupling. Most XCMDs perform one task (strongly modular XCMDs), typically to add a missing feature to Hypertalk. XCMDs interface to Hypertalk using a very strict protocol, passing parameters via the command block record. This protocol coerces weakly coupling in your code.
Software Breadboard
Strong modularity and weak coupling suggest that you can create libraries of useful routines for use both in Hypercard and in programs of your own design. Think of Hypercard as a Software Breadboard, a test platform that you plug code into for checkout and debugging. Once working, the routine can be dropped into any application.
An example of a set of routines that play well in this scenario are the calls to the File Manager. Every (non-trivial) application needs to call on the file manager to open a file, access information in the file and then close it. I decided to build my case around accessing a file because Hypercard itself doesnt handle files according to Hoyle. Hypercard violates two precepts of the user interface guidelines when opening a file: (1) The user is required to know the full pathname of a file if it is not in the current working directory and (2) the OpenFile command performs an unexpected action; if it cannot find the file, it creates it!
In order to properly open a file, you first present the user with a dialog that contains the list of files in the current working directory. This dialog allows the user to flip from drive to drive as well as change directories. The standard file package provides you with the vanilla get file dialog. When the user selects a file and clicks the Open button, your code queries the reply record to discover the files name, working directory id and file type.
The working directory id pinpoints which folder contains the document. If you made a killing on Macintosh software and have been away on some deserted island for the past several years, the working directory assumes the role traditionally held by the volume reference number.
Given the file name and working directory, you can reconstruct and return the full pathname to Hypercard so that it can be used by Hypercards open command (pathname reconstruction is the subject of a future article). However, its just as easy to open the file ourselves to ensure that it opens in whatever manner is expected on the Macintosh. The XCMD that opens the file is left as an exercise.
GetFileName XFCN
The XFCN, GetFileName (Listing 1), displays strong modularity - its sole task is to get the name of a file from the user. By relying only on data passed to it formally, it also demonstrates weak coupling. This XFCN provides a general purpose technique for getting the name of a file from the user. GetFileName calls on a routine Called GetFileNametoLoad in HyperUtils.c (listing 2). The prototypes for HyperUtils are found in HyperUtils.h (listing 3). HyperUtils contains a set of re-usable utilities that do not presume the existence of Hypertalk. These routines arent encumbered with the XCmdBlkPtr interface.
GetFileNameToLoad accepts as its inputs a list of file types (up to 4) to filter out in the open dialog. If you were writing a text import routine, you might want to present the user with only files of type TEXT. From Hypercard, you would then invoke GetFileName as:
--1
Put GetFileName( TEXT ) into it; IF it is not empty then -- User
selected a file put item 1 of it into FileName put item 2 of
it into FolderID end if
With little effort, weve created a re-usable, general purpose XCMD. Pass the name and the FolderID to any File Manager call that requires the files name and volume reference number (aka working directory id). I will call upon this XCMD in future editions of this column so youll have ample opportunity to see it action.
An obvious addition to this list an xcmd that asks the user for the name of a file to save by calling SFPutFile. This is a simple modification to GetFileNameToLoad so Ill leave it as yet another exercise.
After creating enough XCMDs, youll soon view them as general purpose routines with a specific interface (XCmdBlkPtr) to HyperTalk. With this in mind, you should have no trouble creating XCMD libraries that can be used in other applications. Get in the habit of separating the Hypertalk interface, including any callbacks, from the action code. The XCMD itself then becomes an interface between HyperTalk and your code.
Listing 1: GetFileName.c
/********************************/
/* File: GetFileName.c */
/* */
/* Using Standard File Package*/
/* query the user for the name*/
/* of a file to open, and return*/
/* the name along with the */
/* working directory id of the*/
/* file.*/
/* Paramters: */
/* paramCnt = number of types */
/* params[0..3] = the types to */
/* filter for (see Inside Mac.*/
/* I-523 for details */
/* ---------------------------- */
/* To Build:*/
/* (1) Create a project using */
/* this file as well as the */
/* XCMD.Glue.c file. (Set */
/* project type to XCMD (or */
/* XFCN) from the Project menu. */
/* */
/* (2) Bring the project up to*/
/* date.*/
/* (3) Build Code Resource. */
/* (4) Use ResEdit to copy the */
/* resource to your stack.*/
/********************************/
#include<MacTypes.h>
#include<OSUtil.h>
#include<MemoryMgr.h>
#include<FileMgr.h>
#include<ResourceMgr.h>
#include<pascal.h>
#include<strings.h>
#include HyperXCmd.h
#includeHyperUtils.h
pascal void main( paramPtr )
XCmdBlockPtr paramPtr;
{
short FileWDID;
short numTypes;
short i;
SFTypeList typs;
char FileName[256];
char WDIDString[32];
char comma[2];
if( !paramPtr->paramCount )
numTypes = -1; /* select all since no type specified */
else{
numTypes = paramPtr->paramCount;
for( i = 0; i < numTypes; i++ )
BlockMove( *(paramPtr->params[i]), &typs[i], 4L );
}
*FileName = \0;
if( GetFileNameToOpen( typs, numTypes, FileName, &FileWDID ) ){
NumToStr( paramPtr, (long)FileWDID, &WDIDString );
PtoCstr( WDIDString );
comma[0] = ,; /* for you MPW folk */
comma[1] = \0;
strcat( FileName, comma );
strcat( FileName, WDIDString );
CtoPstr( FileName );
}
paramPtr->returnValue = PasToZero( paramPtr, FileName );
}
Listing 2: HyperUtils.c
/****************************/
/* HyperUtils.c */
/* A collection of useful */
/* routines... */
/****************************/
#include<MacTypes.h>
#include<OSUtil.h>
#include<MemoryMgr.h>
#include<FileMgr.h>
#include<ResourceMgr.h>
#include<StdFilePkg.h>
#include HyperXCmd.h
#include HyperUtils.h
void CenterWindow( wptr )
WindowPtrwptr;
/***************************
* Center a window in the current
* screen port. Note: Does not
* attempt to work with multi-screen
* systems.
*
* This code is inspired by a
* similar routine written by Steve
* Maller in MPW Pascal. Thanks Steve.
***************************/
{
short hWindSize = wptr->portRect.right - wptr->portRect.left;
short vWindSize = wptr->portRect.bottom - wptr->portRect.top;
short hSize = wptr->portBits.bounds.right - wptr->portBits.bounds.left;
short vSize = wptr->portBits.bounds.bottom - wptr->portBits.bounds.top;
MoveWindow( wptr,
( hSize - hWindSize ) / 2,
( vSize - vWindSize + 20) / 2,
false
);
}
void Concat( str1, str2 )
char *str1;
char *str2;
/*****************************
* Append string 2 to the end of
* string 1. Both strings are
* pascal-format strings.
*
* str1 must be large enough to hold
* the new string and is assumed to
* be of Type Str255 (a pascal string)
*****************************/
{
short len1 = *str1; /***number of chars in str 1***/
short len2 = *str2++;/***number of chars in str 2***/
char *temp; /*** string pointer ***/
*str1 += len2 + 1;/*** add sizes to get new size***/
temp = str1 + len1 + 1;/*** move to end of str 1***/
while( len2 ){
*temp++ = *str2++;/*** add char to temp & move***/
--len2;/*** until all characters are added***/
}
}
void CopyPStr( pStr1, pStr2 )
char *pStr1;
char *pStr2;
/****************************
* Copy the contents of pstr1 into
* pstr2. The strings are assumed
* to be of type STR255 (length byte
* precedes data
*
****************************/
{short i;
char *tstr;
tstr = pStr2;
for( i = 0; i <= *pStr1; i++ )
*tstr++ = *pStr1++;
}
short GetFileNameToOpen( typs, typCnt,theName, theWDID )
SFTypeList typs;
short typCnt;
char *theName;
short *theWDID;
/*****************************
* Invokes SFOpenFile to query the
* user for the name of a file to
* open.
*
* In: List of types of files to
*filter for (up to 4)
* Out: fileName if picked in theName
*working directory in theWDID
*nil otherwise
*the files volum ref num.
* ( Note that the space for the
* string must be allocated by the
* caller).
*****************************/
{
Point where;
char prompt[1];
SFReplyreply;
GrafPort *oldPort;
WindowPtrdlogID;
prompt[0] = \0;
/*** Get and put up the standard file ***/
/*** dialog. You will only see the file***/
/*** types that you filtered for. If ***/
/*** you filtered for no files, then ***/
/*** all files will display***/
GetPort( &oldPort );
dlogID = GetNewDialog( (short)getDlgID, (Ptr)NIL, (Ptr)UPFRONT );
SetPort( dlogID );
CenterWindow( dlogID );
where.h = dlogID->portRect.left;
where.v = dlogID->portRect.top;
LocalToGlobal( &where );
SFGetFile( where, prompt, (Ptr)NIL, typCnt, typs, (Ptr)NIL, &reply );
DisposDialog( dlogID );
SetPort( oldPort );
/*** If the user selected a file, lets ***/
/*** get the information about it ***/
if (reply.good){
*theWDID = reply.vRefNum;
PtoCstr( (char *)&reply.fName );
strcpy( theName, &reply.fName );
}
return( reply.good );
}
Listing 3: HyperUtils.H
/********************************/
/* HyperUtils.H */
/* Header file for HyperUtils.c */
/* routines... */
/********************************/
#define NIL 0L
#define UPFRONT -1L
void CenterWindow( WindowPtr wptr );
void Concat( char * str1, char * str2 );
void CopyPStr( char * pStr1, char * pStr2 );
short GetFileNameToOpen(SFTypeList typs,short typcnt, char *theName,
short *theWDID);
NOTE: From Jul 89 Jorg Column
Note on a bug in last months article:
Last months XFCN, GetFileName, incorrectly returns the working directory id of the file selected from SFGetFile. Change the call to GetFileNameToOpen to Read:
if( GetFileNameToOpen(typs,numTypes,FileName,&FileWDID ) ){
temp = (long)FileWDID & 0xFFFF;
NumToStr( paramPtr, temp, &WDIDString );
PtoCstr( WDIDString );