TweetFollow Us on Twitter

DA for Mac C
Volume Number:2
Issue Number:4
Column Tag:C Workshop

A DA for Mac C without Desk Maker

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.

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:

DeskAccessory 'Name',ID,Flags,Rate,EvtMask,Globals

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. */

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 |
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.

; ---------------------------------------------------------------------------------

.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

; ---------------------------------------------------------------------------------

XREF    globalSize

; ---------------------------------------------------------------------------------


; ---------------------------------------------------------------------------------

 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

; ---------------------------------------------------------------------------------
; On entry A0 contains *PB (pointer to parameter block) and
; A1 contains *DCE (pointer to device control entry).

 PEA    open; Mac C function
 BRA.S  main

 if needGlobals
 PEA    disposeGlobals  ; intercept routine
 PEA    close    ; Mac C function
 BRA.S  main

 MOVE.L jIODone,-(SP); jump vector
 PEA    control  ; Mac C function

 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

 ; since NewHandle might trash *DCE in A1 ...
 MOVEA.L20(SP),A1; restore *DCE
 MOVE.L A0,dCtlStorage(A1); save handle

 ; 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

 MOVE.L D7,D0    ; restore result

 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

 ; intercept Mac C close routine to dispose globals
 JSR    close
 ADDQ.L #4,SP    ; burn return address
 BSR.S  findGlobals; get handle
 MOVE.L #0,dctlStorage(A1); mark it empty
 BRA.S  restoreResult

 ; 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

; 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

/* 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 */

    Setup A4 as the index to global variables and inhibit floating point. 

#Options R=4 Z


#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>

    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
    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


typedef struct
  char typeName[4];
  } ResType;


#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

    The assembly language macro from DeskAccessory.c is invoked here. 
The order of parameters is:


    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. */

DeskAccessory 'Clock',12,$2400,$0010,$0142,NeedGlobals




DialogPtr clockDialog;
short ownedID;
MenuHandle hClockMenu;
long oldDateTime;
Str255 oldDTString;
Handle hDisplay;
Rect displayRect;
short textLeft, textBase;
short clockFormat, clockSelection;
short clockDirty;
short oldWidth;

    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 
    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)

    if (!(freeBlock = NewPtr(FREE_BLOCK_SIZE)))
      return -1;

    if (!(clockDialog =
      (DialogPtr) NewPtr(sizeof(DialogRecord))))
      return -1;
    clockDialog = GetNewDialog(ownedID, clockDialog,
    ((WindowPeek) clockDialog)->windowKind =
    dce->dCtlWindow = (Ptr) clockDialog;
    SetPort((GrafPtr) clockDialog);
    hClockMenu = GetMenu(ownedID);

    GetDItem(clockDialog, DISPLAY_ITEM, &dummyType,
      &hDisplay, &displayRect);
    textLeft = displayRect.left;
    textBase = + theFontInfo.ascent;
    clockFormat = WANT_SECONDS;
    clockSelection = TIME_SELECTION;


  return 0;

    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;

  dce->dCtlWindow = NIL;

  return 0;

    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);

    case accMenu:
      doMenu(pb->csp.menuData.menuItem, dce);

  return 0;


  EventRecord *theEvent;

  switch (theEvent->what)
    case updateEvt:

    case activateEvt:
      if (theEvent->modifiers & activeFlag)


    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. */


  clockDirty = TRUE;

    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. */

  (*hClockMenu)->menuID = ownedID;
  InsertMenu(hClockMenu, CLOCK_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 
    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;

    case DATE_ITEM:
      if (clockSelection != DATE_SELECTION)
        clockDirty =TRUE;
      clockFormat = shortDate;
      clockSelection = DATE_SELECTION;

    case ABOUT_ITEM:
      dce->dCtlFlags &= 0xFBFF; /* clear dCtlEnable */
      dce->dCtlFlags ^= 0x4000; /* set dNeedLock */
      aboutDialog = GetNewDialog(ABOUT_DLOG +
        ownedID, NIL, FRONT_WINDOW);

        ModalDialog(doModal, &theItem);
        (theItem > oK);

      dce->dCtlFlags ^= 0x0400; /* set dCtlEnable */
      dce->dCtlFlags &= 0xBFFF; /* clear dNeedLock */

    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 
    Everything works correctly without saving any registers before calling 
drawDisplay(), but saving registers is always a good idea. */


 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

 MOVE.W #1,(A0)  ; first item is hit
 MOVE.W #$0100,4(SP) ; result is TRUE (high byte)
 RTS    ; skip drawDispaly()

 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 "}"

    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:
  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()

 MOVE.L $020C,D0 ; Time
 ; RTS is inserted by the compiler after "}"
  } */

  GrafPtr oldPort;
  long newDateTime;
  Str255 newDTString;
  short newWidth;

  if ((clockDirty) || ((newDateTime = *((long *) TIME)) !=
    SetPort((GrafPtr) clockDialog);

    oldDateTime = newDateTime;
    dTimeToString(clockFormat, &newDTString,
    if ((clockDirty) || ((newWidth =
      StringWidth(&newDTString)) < oldWidth))
      oldWidth = newWidth;
      clockDirty = FALSE;


    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 

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;

 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 "}"


48 209 72 303
Invisible GoAway


103 82 239 430
Visible NoGoAway


StaticText Disabled
4 6 20 88


108 268 126 338

StaticText Disabled
10 10 26 338

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.


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:


 drvrID = 0xC000 - (32 * (1 + dce->dCtlRefNum));


 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:


 dce->dCtlFlags &= 0xFBFF; /* clear dCtlEnable */
 dce->dCtlFlags ^= 0x4000; /* set dNeedLock */
 dce->dCtlFlags ^= 0x4000; /* set dCtlEnable */
 dce->dCtlFlags &= 0xFBFF; /* clear dNeedLock */


 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.


Community Search:
MacTech Search:

Software Updates via MacUpdate

FileMaker Pro 19.4.2 - Quickly build cus...
FileMaker Pro is the tool you use to create a custom app. You also use FileMaker Pro to access your app on a computer. Start by importing data from a spreadsheet or using a built-in Starter app to... Read more
Adobe Illustrator 26.0.3 - Professional...
You can download Adobe Illustrator for Mac as a part of Creative Cloud for only $20.99/month. Adobe Illustrator for Mac is the vector graphics classics in the design industry. It is a digital... Read more
WhatRoute 2.4.9 - Geographically trace o...
WhatRoute is designed to find the names of all the routers an IP packet passes through on its way from your Mac to a destination host. It also measures the round-trip time from your Mac to the router... Read more
Notion 2.0.20 - A unified workspace for...
Notion is the unified workspace for modern teams. Notion Features: Integration with Slack Documents Wikis Tasks Release notes were unavailable when this listing was updated. Download Now]]> Read more
Monterey Cache Cleaner 17.0.2 - Clear ca...
Monterey Cache Cleaner is an award-winning general-purpose tool for macOS X. MCC makes system maintenance simple with an easy point-and-click interface to many macOS X functions. Novice and expert... Read more
Firetask Pro 4.6.8 - Innovative task man...
Firetask Pro represents the next generation of easy-to-use, project-oriented task management apps. By combining David Allen's powerful Getting Things Done (GTD®) approach with classical task... Read more
Smultron 13.0.4 - Easy-to-use, powerful...
Smultron 13 is the text editor for all of us. Smultron is powerful and confident without being complicated. Its elegance and simplicity helps everyone being creative and to write and edit all sorts... Read more
Box Sync 4.0.8057 - Online synchronizati...
Box Sync gives you a hard-drive in the Cloud for online storage. Note: You must first sign up to use Box. What if the files you need are on your laptop -- but you're on the road with your iPhone? No... Read more
Audio Hijack 3.8.10 - Record and enhance...
Audio Hijack (was Audio Hijack Pro) drastically changes the way you use audio on your computer, giving you the freedom to listen to audio when you want and how you want. Record and enhance any audio... Read more
Direct Mail 6.0.1 - Create and send grea...
Direct Mail is an easy-to-use, fully-featured email marketing app purpose-built for macOS. Create, send, and track great looking email campaigns that get results. Start your newsletter by selecting... Read more

Latest Forum Discussions

See All

Hopefully Not Jared’s Last Show – The To...
My suspicions from last week were correct, and after my two kids tested positive for Covid last week both my wife and I have now tested positive as well. It seems you just can’t escape this stuff lately. Thankfully the two little ones are pretty... | Read more »
TouchArcade Game of the Week: ‘Micro RPG...
I feel like idle games are one of those perfect fits for the mobile platform. Not that they replace more involved gaming experiences when you’re in the mood for that, but they do fit in alongside other types of games just fine as a “go to" when you... | Read more »
‘Phantom Blade: Executioners’ Closed Bet...
Phantom Blade: Executioners is holding a small-scale technical test that lets players get first dibs on the KungFuPunk action RPG. Offered to selected players only, S-Game’s first Closed Beta Test will provide players with limited edition in-game... | Read more »
New ‘Warhammer 40,000: Tacticus’ Video S...
Back in September Snowprint Studios, who you may know from their previous Legend of Solgard or Rivengard, announced that they’d partnered up with Games Workshop to put out a new tactical game in the Warhammer 40,000 universe titled Warhammer 40,000... | Read more »
SwitchArcade Round-Up: ‘Pokemon Legends:...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for January 28th, 2022. We’ve got a bunch of new releases to look at today, with a few big hitters, a few mid-level diversions, and a healthy supply of compost. Since it’s Friday, we... | Read more »
Phantom Blade: Executioners, S-Game...
S-Game has kicked off its first Closed Beta Test for Phantom Blade: Executioners, inviting a selected few to get first dibs on the upcoming KungFuPunk action RPG on mobile. The CBT officially begins this January 28th, and beta testers will receive... | Read more »
‘Infinite Galaxy’ First Anniversary: Cel...
Cultivating a new generation of valiant commanders across 240 countries worldwide, Infinite Galaxy has quenched players’ thirst to explore the vastness of space – and there are only more intergalactic adventures to embark on from here on out. Camel... | Read more »
War and Order: How to brave the cold in...
War and Order's 6th-anniversary celebrations are underway, and all in good time too - this season not only brings about fabulous festivities, but it also lets players experience the harsh winter in an entirely new way. [Read more] | Read more »
‘Hidden Folks+’ Is This Week’s New Apple...
The original Hidden Folks from Adriaan de Jongh is an excellent hidden objects game featuring hand drawn visuals. It is an absolute joy to play, and it has now released on Apple Arcade in the form of Hidden Folks+ () as an App Store great. If you’... | Read more »
Mini Metro’s First Big Update of 2022 Ad...
Last year saw great updates for Dinosaur Polo Club’s Mini Metro ($3.99) which is also available on Apple Arcade as an App Store Great. | Read more »

Price Scanner via

Apple has clearance 2020 13″ MacBook Airs ava...
Apple has clearance, Certified Refurbished, 2020 13″ Intel-based MacBook Airs in stock today starting at only $719 and up to $370 off original MSRP. Each MacBook features a new outer case, comes with... Read more
The cheapest iPhones for sale today at Apple...
Apple has restocked Apple Certified Refurbished iPhone 8 models starting at only $359. Each refurbished iPhone comes with a fresh external case, standard Apple 1-year warranty, and free shipping.... Read more
14″ MacBook Pro with Apple M1 Max CPU now in...
Looking for a new 14″ MacBook Pro with an Apple M1 Max CPU? Stock is finally trickling into Apple resellers. B&H has Silver 14″ M1 Max MacBook Pros in stock today for $2899 including free 1-2 day... Read more
14″ MacBook Pros with Apple M1 Pro CPUs are i...
Amazon is reporting stock of 14″ MacBook Pros with M1 Pro CPUs today with a $50 discount. Shipping is free, and delivery is available by February 1st for most configurations. Be sure to make your... Read more
Apple has restocked 13″ M1 MacBook Pros for $...
Apple has restocked a full line of 13″ M1 MacBook Pros available Certified Refurbished, starting at only $1099 and up to $230 off original MSRP. These are the cheapest M1 MacBook Pros for sale today... Read more
Apple’s AirPods Max headphones are on sale fo...
Amazon has Silver, Blue, and Space Gray Apple AirPods Max headphones on sale today for $100 off MSRP. Shipping is free, and all models are in stock today. Their price is the lowest currently... Read more
Open a new line of service at Verizon and get...
Verizon is giving away 64GB Apple iPhone 12 minis or your choice of an iPhone 11 to customers who choose one of these phones and open a new line of service. Offer is available online only, and no... Read more
Open-box 13″ M1 MacBook Airs now available st...
QuickShip Electronics has open-box return 13″ M1 MacBook Airs in stock and on sale for $200-$400 off MSRP on their eBay store right now with free express delivery. According to QuickShip, “The item... Read more
Verizon’s 2022 iPad promo: $100-$310 off any...
Verizon has cellular-capable iPads on sale for $100-$310 off MSRP when purchased with an Unlimited service plan. Sale price is applied to your account monthly over a 24 or 30 month period, depending... Read more
Sunday Sale: Apple AirPods are on sale for up...
Amazon has Apple AirPods on sale for $10-$100 off MSRP today, depending on the model. All are in stock today with free delivery: – AirPods Max headphones (Blue): $449 $100 off MSRP – AirPods Max... Read more

Jobs Board

Registered Nurse (RN) Employee Health PSJH -...
…is calling for a Registered Nurse (RN) Employee Health PSJH to our location in Apple Valley, CA.** We are seeking a Registered Nurse (RN) Employee Health PSJH to be Read more
Systems Administrator - Pearson (United State...
…and troubleshoot Windows operating systems (workstation and server), laptop computers, Apple iPads, Chromebooks and printers** + **Administer and troubleshoot all Read more
IT Assistant Level 1- IT Desktop Support Anal...
…providing tier-1 or better IT help desk support in a large Windows and Apple environment * Experience using IT Service Desk Management Software * Knowledge of IT Read more
Human Resources Business Partner PSJH - Provi...
…**is calling a** **Human Resources Business Partner, PSJH** **to our location in Apple Valley, CA.** **Applicants that meet qualifications will receive a text with Read more
Manager Community Health Investment Programs...
…is calling a Manager Community Health Investment Programs PSJH to our location in Apple Valley, CA.** **Qualified candidates will be invited to do a self-paced video Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.