DA for Mac C
Volume Number: | | 2
|
Issue Number: | | 4
|
Column Tag: | | C Workshop
|
A DA for Mac C without Desk Maker ![](img001.gif)
By Don Melton, Staff Artist, Orange County Register
Write your Mac C DA's Direct!
There's a definite problem in developing desk accessories with the Consulair Mac C environment: DeskMaker!
DAs are difficult enough to write without having to spend your time with a flaky-pseudo-second-linker to create a Font/DA Mover compatible file. While most development systems provide compiler and/or linker options to create DAs, Mac C must rely on DeskMaker.
For those of you using assembler, Pascal or perhaps another brand of C, let me explain. After you compile and link a Mac C program that is to become a desk accessory, you must run DeskMaker. Using a standard file dialog, DeskMaker asks you for the name of a desk control file. This file is a bit like a linker directive, but it specifies such things as the DA name, the map file name from the linker, ID, flags and other such stuff. After much disk access a DA is created.
This works but DeskMaker will not function correctly with the Exec, it gets confused about source and destination volumes (worse than RMaker), it does not filter TEXT file names in its standard file dialog, you have to create yet another directive (aren't link and job files bad enough?), and it's one more step to slow down development!
Mac C is a very good development system, and Consulair is constantly upgrading and improving their compiler and linker/librarian. They even recently released a disk full of useful development utilities. But as of this writing, they have not updated or improved DeskMaker.
I knew there was probably an easier way to develop DAs in Mac C, so in January of 1986, I started working on the problem. On the following pages I'll show you my alternative to DeskMaker and how I developed it. Plus, I'll take you step by step through the source code of a real desk accessory to show you valuable techniques I learned (the hard way) about such things as re-entrancy, menus, memory mangement, modal dialogs, and much more.
This information is applicable to any development system, but be warned -- some knowledge of 68000 assembler is needed to fully understand this text.
![](img002.gif)
Figure 1: Clock DA with window, menu, dialog box
Before we get technical, however, I'd like to acknowledge a few people whose help made this article and my sanity possible during development. While attending MacWorld Expo in San Francisco, Fred Huxham and David Burnard (two of the authors of an excellent tutorial and reference book called Using the Macintosh Toolbox with C) convinced me my crazy idea would work and provided invaluable clues about global variables.
Alan Wooton solved many of my problems before they began with his article A Resource Utility DA with TML Pascal in MacTutor vol. 1, no. 12. He also answered many of my silly questions during late-night phone marathons.
And when I wasn't talking to Alan, Bob Denny told me more than I ever wanted to know about device drivers. He also confirmed my suspicions about Finder 5.1 (more on this later).
Although I've never met the gentleman, Mike Schuster helped shed some crucial light on fooling the compiler.
Mr. Consulair himself, Bill Duvall, and Jay Friedland of technical support were a great source of information and confidence, even on Monday mornings.
Thank you all. Anyway
First, a review of the essentials
A desk accessory is basically one code segment called a DRVR resource, although it may own other resources such as DLOGs, DITLs and MENUs.
It's a special type of device driver but, like all drivers, it begins with a header of information. This header has nine elements, each one word in length (a total of 18 bytes). It contains, in order:
a special 16-bit flag,
a rate for how often the DA is called by the system,
an event mask,
a space for a menu ID,
and five words of offsets to routines inside the single code segment: open, control close, prime, & status.
Of the five driver routines, only three of these routines are usually ever used in a DA: open, control and close. Status and prime, the other two routines, are used only by device drivers. Open initializes the DA, control is its main loop, and close removes it.
Open is called when the DA is selected from the Apple menu. Control is executed while the DA is active, during an application's periodic call to SystemTask. Close is called when the DA's close box (or cancel button) is clicked or an application calls CloseDeskAcc.
In addition to the DRVR code resource, there are two pieces of data associated with each DA: the parameter block and the device control entry.
The parameter block is a structure allocated by the system on the stack, and filled with generally useless information except for the csCode and csParam (also called csp). These two data are used by the DA to determine what it should do during its control routine. A pointer to the parameter block (PB) is passed in A0 whenever the DA routines are called.
The device control entry is a structure allocated by the system on the system heap, and it's actually very similar to the DA header. In fact, the system reads the DA header to initialize certain elements of the device control entry (DCE) everytime the DA open routine is called. A pointer to the DCE is passed in A1 whenever the DA routines are called.
If all this sounds unfamiliar, you might want to review the Desk and Device Managers from Inside Macintosh before you continue. [See also the Assembly Lab article in this same issue for more detailed information on the values of csParam and csCode. -Ed.]
DeskMaker internals
Now a few more words about DeskMaker and what it actually does (without complaining about the way it works). Very simply, DeskMaker takes the code created by the linker and adds to the beginning of it, in order: a header, the DA ID and name, a table of offsets, and a set of glue routines to the five possible Mac C functions (open, control, etc.). At the end of the code, it adds space for any global variables and initializes them. It gets the information for the offsets to the Mac C functions and the size of the global variable segment from the linker map file. The other information, such as the flag word of the header, it gets from the desk control file.
By the way, the glue routines are very necessary because the system can't just jump into a normal Mac C function. Mac C passes parameters in data registers but on entry a pointer to the PB is in A0 and a pointer to the DCE is in A1, so an intermediate step is needed to transfer A0 to D0 and A1 to D1. Also the control, status and prime routines exit differently than open and close, so the glue must provide for this.
The alternative
When I started this project, I realized that to effectively erase DeskMaker I would have to find a way to create the header and glue routines during compile and/or link time. I then discovered the header and glue could be placed at the beginning of a code segment by using inline assembly at the beginning of a C source file. This way DC (define constant) directives could create the header and a small assembly language segment would be the glue.
So I studied the glue routines Alan Wooton wrote for TML Pascal in last November's MacTutor, and with a few modifications (Pascal expects parameters on the stack) I got his code to work with Mac C. Of course, his code had to be included at link time, whereas C allowed this during compilation.
Now you might think that with the marvelous Consulair Linker/Librarian I should have begun working on a library to be included at link time rather than do everything in the C source file, but my next discovery prevented this.
As I was staring at my early inline assembly source, it struck me that I could place a RESOURCE directive in the beginning to change the all code produce by the linker into a DRVR of a specified name and ID! Heck, I could even preset it as purgeable this way. When balanced with a /Resources command in the linker directive, it worked perfectly almost.
Because the RESOURCE directive will not allow a null byte (ASCII zero) to precede the name parameter, the format of the name was not correct for a desk accessory. Inside Macintosh says all drivers must have a single character preceding their name to prevent confusion with filenames. This character is a period for device drivers and a null byte for DAs (hex zero).
However, I was delighted to discover that if a DA has this incorrect name format, Font/DA Mover will add the null byte whenever it's moved into another file. Also, ResEdit will allow you to fix the name. Various DA sampling utilities, such as Loftus Becker's DA Key, don't mind it being incorrect.
Even though this deficiency in the RESOURCE directive seemed a moot point, I mentioned it to Bill Duvall. He told me it will be modified sometime, probably this year.
Anyway, now I could create a DA in a single C source file and linker directive. By using the /Type command in the linker I could even make it a Font/DA compatible file. The next big problem to tackle was global variables.
Applications reference their globals via negative offsets to A5. DAs should NEVER use A5 for their globals. So Mac C has a compiler option to set the global index register, and when using DeskMaker, you set this to A4. Before the glue routines created by DeskMaker call any C functions, A4 is set to point to the end of your code where the globals were added. This is nice because you get to declare your globals in a desk accessory the same way you would in an application. The thing I don't like about it is where the storage is placed. Not only is modifying data in a code segment (even if its at the end of that segment) a dangerous practice, but large uninitialized global structures allocated inside the code waste space.
Since I very rarely initialized any of my globals during compilation, I decided that I should allocate them at runtime. At first I adapted Alan Wooton's method.
During my C open function I created a relocatable structure on the heap and stored a handle to it in the dCtlStorage field of the device control entry. That's one of the suggested uses for dCtlStorage anyway. Remember, all new relocatable structures are not purgeable, so there was very little chance of my globals disappering on me. Whenever I needeed to reference the globals, I retrieved the handle via the DCE, locked it, dereferenced it into a pointer, and then passed that pointer to any subroutines that needed to access the globals. When it was no longer needed I simply unlocked it, and on exit, my close routine disposed of the storage.
I liked the idea of breaking the DA into smaller pieces so they could float around the heap when inactive (In case you didn't know, the DA code itself is usually locked on the heap by the system whenever the DA is called, and unlocked when the DA returns control to the system). This allocation technique also allowed the globals to be as big as 32K in memory and exactly 0K in the system file.
However, the Macintosh is hard enough to program without having to induce even more pointer indirection inside the C source. Not only is it maddening to remember to get pointers to pointers, but the extra typing alone will give you arthritis. All this plus passing an extra parameter to subroutines generated a lot of extra code. Maybe this is palatable in Pascal but I needed a better way to reference my global storage in C.
I wanted to be able to declare global variables as you would in any C program. Since DeskMaker set up A4 to point to the globals in its glue routines, I figured on trying the same strategy. Of course this also meant I'd have to allocate my storage during the glue and use the compiler option to set the index register to A4.
This worked fine but everytime I changed the size of my variable structure, I had to modify the assembly source which called NewHandle with a specific size constant. As you can imagine, this got to be a real problem when I'd forget to change it or simply not calculate the constant correctly.
I wanted a way to have the compiler to calculate the value of the global size. As it turned out though, I had to trick the compiler and use the linker to make the calculation. The technique is a little strange but it works.
First, you have to remember that a global variable declaration creates an offset value to whatever index register is used as a global pointer. The actual offset values for globals are completely unknown to the compiler, as are any other addressing offsets. The correct offset values are resolved and inserted in the code by the linker. Variable offsets are resolved as negative values, and the first variable declared has the largest offset.
Since the assembly language equivalent of a Mac C global declaration is achieved with the DS directive, I used this in my header/glue source to declare a zero length variable called globalSize before declaring any globals in my C source. Having no length, globalSize was resolved as the same value as the first C global variable. This meant I could simply move globalSize into a register, negate it to make it a positive value, pass it to NewHandle, lock the storage, dereference the handle to a pointer in A4, and add globalSize to A4 which then decremented A4. Simple? Right.
Unfortunately the compiler generated all sorts of rude errors when I included the following instruction:
MOVE.W #globalSize,D0
I was emotionally crushed. Two days later I happened to be thumbing through Mike Schuster's nifty article on the Laser Print DA in MacTutor vol. 2 no.2, when I noticed he had a listing for a modified version of some DA glue routines for Megamax C. Incredible! He was doing the very same thing, but he had managed to get it by the compiler. I used his technique to do this:
DC.W $303C
; This is the value of the machine instruction for a MOVE.W
; to place the following word in memory into D0.
DC.W globalSize
; This (the following word in memory) is resolved by the linker!
Sure, it may be a kludge but it always works.
Later I was told that a LEA globalSize,An instruction would also get the value. Not only does this take a few more bytes in memory to transfer the value from an address register into D0, it has the severe handicap of not working. All it does is get the PC relative offset to the beginning of the header. Suprisingly, the first time I tried this my DA didn't crash, instead it allocated over 20K on the heap for two pointers and an integer.
By the way, the linker directive needs to set global allocation to -0 or similar allocation problems can occur.
After finally getting globals to work, I realized I ought to optimize the glue routines to get rid of repetitive instructions. Instead of having a separate glue routine for open, control and close, I wrote one main code segment that is used in every call. This is similar to Bill Duvall's method used in the DeskMaker glue.
Open and close don't need to preserve as many registers as control, but since they all travel the same road, I saved D4-D7/A0/A1/A4-A6 on entry and restored them on exit. Mac C will actually preserve A5 and A6, but it's possible an assembly language subroutine could trash them. It never hurts to be safe.
Originally I wrote a lot of error checking for the glue, but in the end I decided to just exit open if I couldn't allocate the space for globals. The reason I don't check for errors, after I use HLock and HUnlock on the storage, is because there's not much you can do if some other task has messed up the globals. If the heap gets that damaged, a system error is inevitable.
DAs should return a result code in D0 after every call. This result is read by the system, but unfortunately it's not preserved for the current application. To get around this bug I placed the result in the ioResult field of the parameter block on exit.
After finishing the header and glue I realized they were taking up about two pages at the beginning of my C source. It was when I decided to include them as a separate file that I got the idea for writing a macro. Wouldn't it be nice to configure the DA name, ID, flags and other stuff with just one line of text inside a Mac C source file? Now you can do this!
If you take the time now to examine listings 1 and 2, you'll see the final outcome of my project.
Listing 1 is a file called DeskAccessory.c. This is included in the beginning of a Mac C source file along with all the other headers. It contains a single assembly language macro called DeskAccessory. This macro configures and then includes the file shown in listing 2, DAHeader.asm. You can invoke the macro using this format:
#asm
DeskAccessory 'Name',ID,Flags,Rate,EvtMask,Globals
#endasm
That's all there is to it!
As you can see, the macro has six parameters. The first (always enclosed in single quotes) defines the name of the desk accessory and the second its resource ID (12-31 inclusive). The third, fourth and fifth parameters define constants in the DA header (more on these later). The sixth parameter is a conditional request for global variables to be allocated at runtime, and it takes only two arguments: NeedGlobals or NoGlobals.
If you examine DAHeader.asm in listing 2, you'll notice it has conditional assembly statements. This conditional assembly is dictated by the sixth parameter of the macro. The reason I did this is to save code space. Why mess with handles and pointers if you don't need globals?
So, if you want the code to handle global variables in your DA, specify NeedGlobals as the sixth parameter of the DeskAccessory macro. If you're not going to use any global storage, specify NoGlobals. Simple.
However, there are four very important things to remember if you use this macro:
Always invoke the macro before you declare any global variables or define any Mac C functions! If you do specify NeedGlobals, use the compiler option to set the global address register equal to A4 or a system error will occur at runtime. If NeedGlobals is specified and no global variables are declared in the following C source, a system error is very likely at runtime. Finally, all global variables are initialized to zero.
The flags, delay and event mask parameters can be written in whatever number format you're used to, but remember that C and assembler number formats are not the same.
Although the 16-bit flags contain bits that can be set to enable read, write and status calls, these bits are always cleared in DAHeader.asm. I did this because these three bits are only relevant to device drivers with status and prime routines. The header expects to find three functions in the C source: open, control and close (all in lowercase), and it will not recognize status and prime.
In conclusion
Well, I managed to duplicate most of the functions of DeskMaker exept its ability to test the DA after linking. Shucks. I use Loftus Becker's DA Key for this purpose -- a utility I recommend highly. It's also more stable than DeskMaker's testing function anyway. I also recommend testing any DAs you might write while operating ResEdit, a very harsh environment. ResEdit does many interesting things to the heap, like moving nonrelocatable blocks.
Now take some time and read listing 3. It contains the complete source to a clock DA plus an incredible number of useful comments. The listing is designed to be read from start to finish, and the comments are placed in front of each function to make the source itself more legible. A few of these comments repeat themes discussed by Alan Wooton in previous issues of MacTutor, however they are repeated here for completeness and clarity, and many of them have been expanded.
Using the clock DA as a model, you can write a desk accessory which will survive the most brutal test that I know: running under TMON with heap check, scramble and purge all enabled! Many applications can't even stand that.
Don wins our program of the month award for this outstanding contribution to the Mac C community and $50 from MacTutor!
/* Filename: DeskAccessory.c compiled with Mac C 4.0 */
/*
-----------------------------------------------------------------------------------
D E S K A C C E S S O R Y M A C R O
version 02/26/86
Copyright (C)1986 by Don Melton, all rights reserved.
This file is included in a Mac C source file in order to invoke the DeskAccessory
macro later in that source file. This macro must be invoked before declaring
any global variables or defining any Mac C functions. */
#asm
noGlobals SET 0
needGlobals SET 1
MACRO DeskAccessory da1,da2,da3,da4,da5,da6 =
daName SET {da1}
daID SET {da2}
daFlags SET {da3}
daServiceRate SET {da4}
daEventMask SET {da5}
needGlobals SET {da6}
INCLUDE DAHeader.asm |
#endasm
Listing 2
; Filename: DAHeader.asm originally compiled with Mac C 4.0
; ---------------------------------------------------------------------------------
; D E S K A C C E S S O R Y H E A D E R
; version 02/26/86
; Copyright (C)1986 by Don Melton, all rights reserved.
; This file is configured and included in a Mac C source file by
; invoking the macro called DeskAccessory defined in
; DeskAccessory.c.
; ---------------------------------------------------------------------------------
; EQUATES
.TRAP _NewHandle $A122 ; defined in SysTraps.txt
.TRAP _DisposHandle$A023
.TRAP _HLock$A029
.TRAP _HUnLock $A02A
clear SET $200
dCtlStorage SET $14 ; defined in SysEqu.d
dCtlWindowSET $1E
ioResultSET $10
jIODone SET $8FC
daFlagMaskSET $F4FF
; ---------------------------------------------------------------------------------
; GLOBAL VARIABLE DECLARATION
globalSizeDS0
XREF globalSize
; ---------------------------------------------------------------------------------
; LINKER DIRECTIVE
RESOURCE'DRVR' daID daName 32
; ---------------------------------------------------------------------------------
; DESK ACCESSORY HEADER
DC.W daFlags & daFlagMask
; clear dReadEnable, dWritEnable and dStatEnable
DC.W daServiceRate
DC.W daEventMask
DC.W 0 ; space for menu ID
DC.W daOpen
DC.W 0 ; no prime
DC.W daControl
DC.W 0 ; no status
DC.W daClose
; ---------------------------------------------------------------------------------
; GLUE ROUTINES TO MAC C FUNCTIONS
; On entry A0 contains *PB (pointer to parameter block) and
; A1 contains *DCE (pointer to device control entry).
daOpen:
PEA open; Mac C function
BRA.S main
daClose:
if needGlobals
PEA disposeGlobals ; intercept routine
else
PEA close ; Mac C function
endif
BRA.S main
daControl:
MOVE.L jIODone,-(SP); jump vector
PEA control ; Mac C function
main:
if needGlobals
MOVEM.LD4-D7/A0/A1/A4-A6,-(SP) ; save registers
MOVEA.LdCtlStorage(A1),A0; get handle
MOVE.L A0,D0 ; empty handle?
BNE.S lockGlobals; no, lock globals
; allocateGlobals
CLR.L D0; clear high word
; since MOVE.W #globalSize,D0 causes an error ...
DC.W $303C ; kludge instruction
DC.W globalSize ; resolved by linker
NEG.W D0; make it positive
_NewHandle ,clear
BEQ.S initGlobals; if no error, init
MOVE.W #$-1,D0 ; error result
BRA.S exit
initGlobals:
; since NewHandle might trash *DCE in A1 ...
MOVEA.L20(SP),A1; restore *DCE
MOVE.L A0,dCtlStorage(A1); save handle
lockGlobals:
_HLock
; Mac C expects A4 to be pointer to global variables
MOVEA.L(A0),A4 ; dereference handle
; since MOVE.W #globalSize,D0 causes an error ...
DC.W $303C ; kludge instruction
DC.W globalSize ; resolved by linker
SUBA.W D0,A4 ; add to globals ptr
; Mac C expects *PB in D0 and *DCE in D1
MOVE.L 16(SP),D0; get *PB from stack
MOVE.L 20(SP),D1; get *DCE
MOVEA.L36(SP),A0; get offset and ...
JSR (A0); do Mac C routine
; unlockGlobals
; only Mac C open and control routines return here
BSR.S findGlobals; get handle
_HUnlock
restoreResult:
MOVE.L D7,D0 ; restore result
exit:
MOVEM.L(SP)+,D4-D7/A0/A1/A4-A6 ; restore registers
MOVE.W D0,ioResult(A0) ; force result
ADDQ.L #4,SP ; burn function ptr
; open and close return to the trap dispatcher
; control goes to jIODone
RTS
disposeGlobals:
; intercept Mac C close routine to dispose globals
JSR close
ADDQ.L #4,SP ; burn return address
BSR.S findGlobals; get handle
_DisposHandle
MOVE.L #0,dctlStorage(A1); mark it empty
BRA.S restoreResult
findGlobals:
; since HUnlock and DisposHandle trash result in D0 ...
MOVE.L D0,D7 ; save result
; since Mac C routine might trash *DCE in A1 ...
MOVEA.L24(SP),A1; restore *DCE
MOVEA.LdCtlStorage(A1),A0; get handle
RTS
else
; main routine if no global variables are needed
MOVEM.LD4-D7/A0/A1/A4-A6,-(SP) ; save registers
; Mac C expects *pb in D0 and *dce in D1
MOVE.L 16(SP),D0; get *pb from stack
MOVE.L 20(SP),D1; get *dce
MOVEA.L36(SP),A0; get offset and ...
JSR (A0); do Mac C routine
MOVEM.L(SP)+,D4-D7/A0/A1/A4-A6 ; restore registers
MOVE.W D0,ioResult(A0) ; force result
ADDQ.L #4,SP ; burn function ptr
; open and close return to the trap dispatcher
; control goes to jIODone
RTS
endif
/* Filename: Clock.c originally compiled with Mac C 4.0 */
/*
-----------------------------------------------------------------------------------
C L O C K version 02/26/86
Copyright (C)1986 by Don Melton, all rights reserved.
Clock is a desk accessory which opens a window displaying the current
time in hours, minutes and seconds. It has a menu allowing the choice
of displaying the time or date, or an "About " dialog.
This is an example of how to create a desk accessory with Consulair
Mac C without relying on the DeskMaker application. The source code is
provided as a reference for Macintosh software developers. The clock
desk accessory itself may be freely distributed as long as the copyright
notice remains intact.
-- Don Melton, CIS: 74166,1006 */
/*
-----------------------------------------------------------------------------------
MAC C COMPILER OPTIONS
Setup A4 as the index to global variables and inhibit floating point.
*/
#Options R=4 Z
/*
-----------------------------------------------------------------------------------
HEADER FILES */
#include <MacDefs.h>
#include <QuickDraw.h>
#include <Font.h>
#include <Window.h>
#include <TextEdit.h>
#include <Dialog.h>
#include <Menu.h>
#include <Events.h>
#include <Device.h>
#include <Packages.h>
#include <DeskAccessory.c>
/*
-----------------------------------------------------------------------------------
MODIFIED DEFINITIONS
OSIO.h is not included because the OpParamType union structure (as
defined) does not provide access to the menu item. It is redefined here
to include menuData and the event pointer.
Note: Other alternate elements of the OpParamType union structure
are not defined here!
The CntrlParam structure also must be defined because OSIO.h is not
included. However, it remains unaltered. */
union __OP
{
struct
{
short menuID;
short menuItem;
} menuData;
Ptr event;
};
#define OpParamType union __OP
struct __CP
{
struct __CP *ioLink;
short ioType;
short ioTrap;
Ptr ioCmdAddr;
ProcPtr ioCompletion;
short ioResult;
char *ioNamePtr;
short ioVRefNum;
short ioRefNum;
short CSCode;
OpParamType csp;
};
#define CntrlParam struct __CP
/*
-----------------------------------------------------------------------------------
DEFINITIONS NOT IN MAC C HEADER FILES */
typedef struct
{
char typeName[4];
} ResType;
/*
-----------------------------------------------------------------------------------
CONSTANT DEFINITIONS */
#define NIL 0
#define FALSE 0
#define TRUE 1
#define FREE_BLOCK_SIZE 0x1000
#define FRONT_WINDOW -1
#define ABOUT_DLOG 1
#define DISPLAY_ITEM 1
#define TIME_ITEM 1
#define DATE_ITEM 2
#define ABOUT_ITEM 4
#define CLOCK_MENU 0
#define TIME 0x020C
#define WANT_SECONDS 0x0100
#define TIME_SELECTION 2
#define DATE_SELECTION 0
/*
-----------------------------------------------------------------------------------
SETUP DA HEADER AND GLUE ROUTINES
The assembly language macro from DeskAccessory.c is invoked here.
The order of parameters is:
'Name',ID,Flags,ServiceRate,EventMask,GlobalsRequest
The DA event mask is set here to mouseDown, update and activate,
but DAs will receive these events even if the mask is set to $0000. Since
this is yet another undocumented feature, it's best to be safe and
use the correct mask because future versions of the ROMs might behave
differently. */
#asm
DeskAccessory 'Clock',12,$2400,$0010,$0142,NeedGlobals
#endasm
/*
-----------------------------------------------------------------------------------
FORWARD REFERENCES */
doModal();
/*
-----------------------------------------------------------------------------------
GLOBAL VARIABLES */
DialogPtr clockDialog;
short ownedID;
MenuHandle hClockMenu;
long oldDateTime;
Str255 oldDTString;
Handle hDisplay;
Rect displayRect;
short textLeft, textBase;
short clockFormat, clockSelection;
short clockDirty;
short oldWidth;
/*
-----------------------------------------------------------------------------------
OPEN CLOCK
Since the FontDA/Mover renumbers all the IDs of any DA's resources
whenever it moves a DA into another file, the new IDs must be calculated
at runtime. The resource ID of a DRVR can be in the range 12-31, inclusive.
Its owned resources are numbered differently. In this text, the global
variable ownedID must contain the owned resources base ID number. The
resource ID of the clock DRVR is initially 12, so its ownedID will be
-16000. The ownedID can easily be derived from the dCtlRefNum field of
the device control entry. [See the Assembly Lab in this issue for the
formula for this. -Ed]
Setting the dCtlMenu equal to the ownedID is done before checking
whether the DA is already open, because an open() call can come while
the DA is still active. The Desk Manager resets dCtlMenu to whatever
is in the DA header (in this case zero) every time a DA is opened, so
dCtlMenu must be reinitialized.
It's possible to change the DA header at runtime to reflect the correct
menu ID, but certain situations could actually cause the DRVR resource
to be temporarily purged while the DA window was still open, thereby
invalidating the patch. Besides, it's an ugly business to patch what
is essentially a code resource.
It's unecessary to recalculate the ownedID every time open() is called,
but placing it outside the conditional saves code space.
The MENU resource is made unpurgeable here because other events may
cause heap compaction, purge the menu, and invalidate the menu handle.
The menu is released from the heap during the DA close() function.
All owned resources (except possibly a MENU) including the DRVR itself
should be preset as purgeable. Please note that this must be done with
ResEdit, because RMaker does not allow this option at compilation.
The windowKind field of the primary DA window should always be set
equal to the dCtlRefNum field of the device control entry. The system
needs a negative number in this field to recognize the window as belonging
to a DA, and a specific number identifies a specific DA.
The clock dialog is defined as invisible in the resource directive,
Clock.r, because on exit of open(), the Desk Manager will always bring
the DA window to the front of the desktop and make it visible. The advantage
to making the window initially invisible is purely aesthetic. When GetNewDialog()
is called, the Dialog Manager draws the window frame, reads the DITL
resource into memory, makes a copy of it, and then begins drawing the
items. If the dialog contains a complex item list, waiting until the
Desk Manager makes it visible will cause the dialog to be drawn faster.
Setting the port to the clock dialog is done here because the TextMode
of the grafPort is set to scrCopy. Getting and restoring the old port
is also a good idea, however everything works properly if this is not
done.
To keep the DA window pointer away from other nonrelocatable blocks
at the bottom of the heap, a 4K temporary space is allocated before the
DA window pointer is created. Later it is disposed of during close(),
or on an error of open(). This prevents possible heap fragmentation.
An application can allocate more nonrelocatable blocks while a DA is
active, and therefore can create a hole to small to reallocate when the
DA is closed and its window pointer is removed from the heap. This DA
also causes Pack6 to be loaded on to the heap. Pack6 is a locked resource
which will remain on the heap until the heap is reinitialized.
It is unnecessary for this function to draw the clock display or
insert the clock menu because update and activate events are generated
when a DA is first opened. */
short open(pb, dce)
CntrlParam *pb;
DCEntry *dce;
{
GrafPtr oldPort;
Ptr freeBlock;
ResType dummyType;
FontInfo theFontInfo;
drvrID = 0xC000 - (32 * (1 + dce->dCtlRefNum));
dce->dCtlMenu = ownedID;
if (!dce->dCtlWindow)
{
GetPort(&oldPort);
if (!(freeBlock = NewPtr(FREE_BLOCK_SIZE)))
return -1;
if (!(clockDialog =
(DialogPtr) NewPtr(sizeof(DialogRecord))))
{
DisposPtr(freeBlock);
return -1;
}
clockDialog = GetNewDialog(ownedID, clockDialog,
FRONT_WINDOW);
((WindowPeek) clockDialog)->windowKind =
dce->dCtlRefNum;
dce->dCtlWindow = (Ptr) clockDialog;
SetPort((GrafPtr) clockDialog);
TextMode(srcCopy);
hClockMenu = GetMenu(ownedID);
HNoPurge(hClockMenu);
GetDItem(clockDialog, DISPLAY_ITEM, &dummyType,
&hDisplay, &displayRect);
GetFontInfo(&theFontInfo);
textLeft = displayRect.left;
textBase = displayRect.top + theFontInfo.ascent;
clockFormat = WANT_SECONDS;
clockSelection = TIME_SELECTION;
DisposPtr(freeBlock);
SetPort(oldPort);
}
return 0;
}
/*
-----------------------------------------------------------------------------------
CLOSE CLOCK
The MENU resource must be released from the heap here because an
error will always occur during the next call to GetMenu() in the open()
function (when the DA is selected again from the Apple menu) only during
the operation of Finder versions 5.0 and above on the older 64K ROMs.
As of this writing, no other application environments produce the error.
Normally, the MENU resource would only need to be made purgeable.
When GetMenu() is invoked it calls several other ROM routines including
GetResource(), CountTypes(), CalcMenuSize(), GetItem(), MenuSelect()
and LoadResource(). During the conditions mentioned above, on exit of
GetItem(), a JSR (A0) instruction will produce an address error at a
location above the 64K ROMs if the DA menu is on the heap. This is because
the newer Finders jump directly in and out the 128K ROMs at absolute
locations, so they expect ROM code at specific memory addresses.
If the DA menu is not on the heap when GetMenu() is invoked during
this environment, no error will occur.
Apple Computer does not recommend using the newer Finders on the
older ROMs. However, since many users have HD20s hooked to their older
Macs, they have to use the newer Finders. It's a good idea to prepare
for these circumstances since there's no gain in code space, and the
only operational difference is that the MENU resource must be reloaded
every time the DA is opened. */
short close(pb, dce)
CntrlParam *pb;
DCEntry *dce;
{
deActivate(dce);
ReleaseResource(hClockMenu);
DisposeDialog(clockDialog);
dce->dCtlWindow = NIL;
return 0;
}
/*
-----------------------------------------------------------------------------------
CONTROL CLOCK
Contrary to popular practice, it's unnecessary to check the dCtlWindow
field of the device control entry to determine whether the DA exists
during control(). If a DA receives a call to control(), the DA had better
exist! The only reason to check this field during control() is to determine
whether the DA is the frontmost window. This is unnecessary in this DA.
Setting the port to the clock dialog is done in drawDisplay() rather
than here, since drawDisplay() is the only routine drawing anything in
the grafPort. */
short control(pb, dce)
CntrlParam *pb;
DCEntry *dce;
{
switch (pb->CSCode)
{
case accEvent:
doEvent((EventRecord *) pb->csp.event);
break;
case accMenu:
doMenu(pb->csp.menuData.menuItem, dce);
break;
default:
drawDisplay();
}
return 0;
}
/*
-----------------------------------------------------------------------------------
MAIN EVENT LOOP */
doEvent(theEvent)
EventRecord *theEvent;
{
switch (theEvent->what)
{
case updateEvt:
updateClock();
break;
case activateEvt:
if (theEvent->modifiers & activeFlag)
activate();
else
deActivate();
break;
default:
drawDisplay();
}
}
/*
-----------------------------------------------------------------------------------
UPDATE CLOCK WINDOW
Normally, if a dialog contains items such as buttons or text, a call
to DrawDialog() is used between the calls to BeginUpdate() and EndUpdate().
In the case this DA, a call to DrawDialog() is not only unnecessary,
it will also cause an annoying flicker in the time/date display. This
is because the empty static text item used to position the display will
erase the dispaly again during the update. */
updateClock()
{
BeginUpdate(clockDialog);
clockDirty = TRUE;
drawDisplay();
EndUpdate(clockDialog);
}
/*
-----------------------------------------------------------------------------------
ACTIVATE CLOCK MENU
Because older versions of the Font/DA Mover don't correctly reset
the menuID of a MENU resource (not always the same as the resource ID!)
when a DA is moved into another file, it must be patched here at runtime
before the menu is inserted in the menubar. */
activate()
{
(*hClockMenu)->menuID = ownedID;
InsertMenu(hClockMenu, CLOCK_MENU);
DrawMenuBar();
}
/*
-----------------------------------------------------------------------------------
DEACTIVATE CLOCK MENU */
deActivate()
{
DeleteMenu(ownedID);
DrawMenuBar();
}
/*
-----------------------------------------------------------------------------------
DO MENU
Choosing "About " in the "Clock" menu will invoke ModalDialog().
Since one of the first things ModalDialog() does is call SystemTask(),
the DA control routine can be called again. In the case of this DA, this
problem of re-entrancy will not cause ModalDialog() to be invoked again
because it can't be selected from the menu once the modal dialog is active.
Remember that the code in DAHeader.asm which calls control(), locks
the global variables on the heap on entrance and then unlocks them on
exit. The Desk Manager does this same thing to the DRVR code resource
before and after the DA is called.
To avoid the possibility of ModalDialog() causing heap compaction
and moving either the DRVR code resource or the globals while unlocked,
the device control entry of this DA is modified before ModalDialog()
is invoked. If the DRVR was allowed to move during a call to ModalDialog(),
the ROM routine could return to a memory address that no longer contained
the DRVR code.
First, to prevent control() from being called by SystemTask(), the
dCtlEnable bit of the dCtlFlags field in the dce is cleared. This makes
certain all global variables remain locked on the heap, because control()
will not exit
until the modal dialog routine is completed and the dCtlEnable bit is
reset.
Second, the dNeedLock bit of the dCtlFlags word is set to make certain
the DRVR code resource remains locked on the heap. If the dNeedLock bit
is set in the actual flags of the DA header, this step is unnecessary.
However, using this technique makes presetting the dNeedLock bit unneccesary
in most situations.
Some programmers prefer to allow a DA to receive calls to control()
while a modal dialog is active so the DA can still be performing certain
tasks in the background. This is accomplished by using a complex technique
which checks, clears or sets the dNeedLock bit on entrance to control()
and then performs similar actions on exit. This techniques works OK if
globals are handled differently than in DAHeader.asm. However, it is
much more confusing to constantly check the status of dNeedLock, than
to use the simple techniques presented in this text, which still allow
this DA to work while the modal dialog is active. */
doMenu(menuItem, dce)
short menuItem;
DCEntry *dce;
{
short theItem;
DialogPtr aboutDialog;
switch (menuItem)
{
case TIME_ITEM:
if (clockSelection != TIME_SELECTION)
clockDirty =TRUE;
clockFormat = WANT_SECONDS;
clockSelection = TIME_SELECTION;
drawDisplay();
break;
case DATE_ITEM:
if (clockSelection != DATE_SELECTION)
clockDirty =TRUE;
clockFormat = shortDate;
clockSelection = DATE_SELECTION;
drawDisplay();
break;
case ABOUT_ITEM:
dce->dCtlFlags &= 0xFBFF; /* clear dCtlEnable */
dce->dCtlFlags ^= 0x4000; /* set dNeedLock */
aboutDialog = GetNewDialog(ABOUT_DLOG +
ownedID, NIL, FRONT_WINDOW);
do
ModalDialog(doModal, &theItem);
while
(theItem > oK);
DisposeDialog(aboutDialog);
dce->dCtlFlags ^= 0x0400; /* set dCtlEnable */
dce->dCtlFlags &= 0xBFFF; /* clear dNeedLock */
}
HiliteMenu(CLOCK_MENU);
}
/*
-----------------------------------------------------------------------------------
MODAL DIALOG FILTER FUNCTION
Since the DA control routine is disabled before ModalDialog() is
invoked, and SystemTask() can no longer call control() which then calls
drawDisplay(); this function, called by ModalDialog(), draws the clock
display. It must also check the modal dialog event record for a keypress
character code equal to Return or Enter, and return a result to ModalDialog().
Since Mac C passes most parameters in registers, doModal() must be
written in assembly language.
The Lisa Pascal format for a dialog filter function is:
PROCEDURE MyFilter(theDialog: DialogPtr; VAR theEvent:
EventRecord; VAR itemHit: INTEGER) : BOOLEAN;
On entry the stack contains (in descending order):
space for boolean result (word)
pointer to modal dialog (long)
pointer to dialog event record (long)
pointer to dialog item hit (long)
return address (long)
On exit:
boolean result (word)
return address (long)
The correct method of addressing parameters passed to a subroutine
on the stack is to define them in a stack frame via a LINK An instruction
on entrance and UNLK An on exit. However, the correct method takes
more code space and is
not especially any more legible than the implementation here.
If Return or Enter characters have been generated from the keyboard,
doModal() must set itemHit equal to 1 and return a result of TRUE. If
not, it must return a result of FALSE so ModalDialog() will handle the
event.
Everything works correctly without saving any registers before calling
drawDisplay(), but saving registers is always a good idea. */
doModal()
{
#asm
MOVE.L (SP)+,D0 ; save return address
MOVEA.L(SP)+,A0 ; save item hit ptr
MOVEA.L(SP)+,A1 ; save event record ptr
MOVE.L D0,(SP) ; restore return address
; and trash dialog ptr
MOVE.W (A1),D0 ; get evtNum
CMPI.W #3,D0 ; keyDwnEvt?
BNE.S noKeyEvent
MOVE.W 4(A1),D0 ; get evtMessage (low word)
; check the character code, NOT the key code!
CMPI.B #3,D0 ; Enter character?
BEQ.S setItemHit
CMPI.B #13,D0 ; Return character?
BNE.S noKeyEvent
setItemHit:
MOVE.W #1,(A0) ; first item is hit
MOVE.W #$0100,4(SP) ; result is TRUE (high byte)
RTS ; skip drawDispaly()
noKeyEvent:
CLR.W 4(SP) ; result is FALSE
MOVEM.LD3-D7/A3-A4,-(SP) ; save registers
JSR drawDisplay
MOVEM.L(SP)+,D3-D7/A3-A4 ; restore registers
; RTS is inserted by the compiler after "}"
#endasm
}
/*
-----------------------------------------------------------------------------------
DRAW CLOCK DISPLAY
Setting the port to the clock dialog is done here rather than in
control(), because this function is also called by doModal(). However,
everything works properly if the port is set only in control() and not
here. Setting the port here is just a good idea. Getting and restoring
the old port is also a good idea, however everything works properly if
this is not done either.
The current time is fetched from the low-memory system global "Time"
($020C) using C typecasting and indirection. Since the current time is
not needed in many different places in this source, this technique is
faster and takes less code than writing an assembly language function.
Also there's no equivalent in Mac C to the Lisa Pascal routine:
PROCEDURE GetDateTime(VAR secs: LONGINT);
However, a function could be defined similar to this procedure but
returning a long result, rather than having a variable passed to it.
For example:
long getDateTime()
{
#asm
MOVE.L $020C,D0 ; Time
; RTS is inserted by the compiler after "}"
#endasm
} */
drawDisplay()
{
GrafPtr oldPort;
long newDateTime;
Str255 newDTString;
short newWidth;
if ((clockDirty) || ((newDateTime = *((long *) TIME)) !=
oldDateTime))
{
GetPort(&oldPort);
SetPort((GrafPtr) clockDialog);
oldDateTime = newDateTime;
dTimeToString(clockFormat, &newDTString,
clockSelection);
if ((clockDirty) || ((newWidth =
StringWidth(&newDTString)) < oldWidth))
{
oldWidth = newWidth;
EraseRect(&displayRect);
clockDirty = FALSE;
}
MoveTo(textLeft,textBase);
DrawString(&newDTString);
SetPort(oldPort);
}
}
/*
-----------------------------------------------------------------------------------
CONVERT DATE OR TIME TO STRING
This is a variation on two Lisa Pacal procedures contained in the
international utilities package. There are no equivalents to these procedures
in Mac C. Here the dateTime parameter is not used. Instead, the current
time is fetched and placed on the stack, and a new parameter allows selection
between date and time.
DA for Mac C 2
Continued from - DA for Mac C
The first parameter determines the format of the output. This is
either the constants WANT_SECONDS or FALSE for time; or shortDate, longDate
or medDate, for the date.
WANT_SECONDS is defined as $0100 because it is a boolean TRUE, and
therefore bit 1 of the high byte must be set. Actually any bit set in
the high byte will work but setting bit 1 is the proper method.
A pointer to the string which will contain the time or date characters
is the second parameter.
The third parameter is the selector for the Pack6 trap, either TIME_SELECTION
or DATE_SELECTION.
The Lisa Pascal format for the original two routines is:
PROCEDURE IUDateString(dateTime: LONGINT; form:
DateForm; VAR result: Str255);
PROCEDURE IUTimeString(dateTime: LONGINT;
wantSeconds: BOOLEAN; VAR result: Str255); */
dTimeToString(theFormat, theStr, theSelector)
short theFormat;
Str255 *theStr;
short theSelector;
{
#asm
MOVE.L $020C,-(SP); Time
MOVE.W D0,-(SP) ; wantSeconds or dateForm
MOVE.L D1,-(SP) ; theStr
MOVE.W D2,-(SP) ; routine selector
DC.W $A9ED ; _Pack6
; RTS is inserted by the compiler after "}"
#endasm
}
DA:Clock.rsrc
TYPE DLOG
,-16000
Clock
48 209 72 303
Invisible GoAway
4
0
-16000
,-15999
103 82 239 430
Visible NoGoAway
1
0
-15999
TYPE DITL
,-16000
1
StaticText Disabled
4 6 20 88
,-15999
5
Button
108 268 126 338
OK
StaticText Disabled
10 10 26 338
Clock
StaticText Disabled
34 10 50 338
©1986 by Don Melton, all rights reserved.
StaticText Disabled
66 10 82 338
A demonstration desk accessory designed for
StaticText Disabled
82 10 98 338
MacTutor magazine and Consulair Corp.
TYPE MENU
,-16000
Clock
Time
Date
(-
About
From Volume 2 Number 5:
Clock DA source correction
Don Melton
Santa Ana, CA
For those of you typing in the Clock DA I wrote for the April 1986 issue of MacTutor, be warned, there is a typo in the magazine source listing. It's my fault, not David Smith's -- probably my old brain tumor acting up again. Thanks go to MacScotty of the MouseHole BBS for finding this error. Turn to page 46, and in the 'open' function you'll find it also. Here's the problem line and the correction:
INCORRECT SOURCE:
drvrID = 0xC000 - (32 * (1 + dce->dCtlRefNum));
CORRECT SOURCE:
ownedID = 0xC000 - (32 * (1 + dce->dCtlRefNum));
It seems I accidently used an older variable name from a previous version of my source code. This mistake does NOT appear on the source code disk available from MacTutor.
There is also a non-destructive typo later in the same source. It does not cause errors but I thought you all might want to know about it anyway. Thanks go to Katz of the MouseHole BBS for pointing out this one. Turn to page 48, and in the 'doMenu' function you'll find these four lines:
QUESTIONABLE SOURCE:
dce->dCtlFlags &= 0xFBFF; /* clear dCtlEnable */
dce->dCtlFlags ^= 0x4000; /* set dNeedLock */
:
dce->dCtlFlags ^= 0x4000; /* set dCtlEnable */
dce->dCtlFlags &= 0xFBFF; /* clear dNeedLock */
BETTER SOURCE:
dce->dCtlFlags &= 0xFBFF; /* clear dCtlEnable */
dce->dCtlFlags |= 0x4000; /* set dNeedLock */
:
dce->dCtlFlags |= 0x4000; /* set dCtlEnable */
dce->dCtlFlags &= 0xFBFF; /* clear dNeedLock */
BEST SOURCE (but be careful using it):
dce->dCtlFlags ^= 0x4400;
/* clear dCtlEnable and set dNeedLock*/
:
dce->dCtlFlags ^= 0x4400;
/* set dCtlEnable and clear dNeedLock*/
I accidently typed an XOR to manipulate the dNeedLock bit instead of an ordinary OR. An XOR works but it is not what I intended. Of course, as the 'BEST' example shows, you can manipulate both bits with an XOR. However, I did not include this example in the original source because I thought it might be confusing.
Please pass this information on to your local BBS. And feel free to ask me any questions about the Clock DA or the DA Header source via MacTutor, Compuserve (74166,1006) or Delphi (DONMELTON). Thanks.