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

f.lux 42.1 - Adjusts the color of your d...
f.lux makes the color of your computer's display adapt to the time of day, warm at night and like sunlight during the day. Ever notice how people texting at night have that eerie blue glow? Or wake... Read more
Spotify - Stream music, creat...
Spotify is a streaming music service that gives you on-demand access to millions of songs. Whether you like driving rock, silky R&B, or grandiose classical music, Spotify's massive catalogue puts... Read more
Vitamin-R 4.15 - Personal productivity t...
Vitamin-R creates the optimal conditions for your brain to work at its best by structuring your work into short bursts of distraction-free, highly focused activity alternating with opportunities for... Read more
OfficeTime 2.0.628 - Easy time and expen...
OfficeTime is time and expense tracking that is easy, elegant and focused. Other time keepers are clumsy or oversimplified. OfficeTime balances features and ease of use, allowing you to easily track... Read more
Slack 4.28.182 - Collaborative communica...
Slack brings team communication and collaboration into one place so you can get more work done, whether you belong to a large enterprise or a small business. Check off your to-do list and move your... Read more
DEVONthink Pro 3.8.6 - Knowledge base, i...
DEVONthink is DEVONtechnologies' document and information management solution. It supports a large variety of file formats and stores them in a database enhanced by artificial intelligence (AI). Many... Read more
FileMaker Pro 19.5.4 - 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
Backblaze - Online backup serv...
Backblaze is an online backup service designed from the ground-up for the Mac. With unlimited storage available for $6 per month, as well as a free 15-day trial, peace of mind is within reach with... Read more
Day One 7.16 - Maintain a daily journal.
Day One is an easy, great-looking way to use a journal / diary / text-logging application. Day One is well designed and extremely focused to encourage you to write more through quick Menu Bar entry,... Read more
Garmin Express - Manage your Ga...
Garmin Express is your essential tool for managing your Garmin devices. Update maps, golf courses and device software. You can even register your device. Update maps Update software Register your... Read more

Latest Forum Discussions

See All

We’re Digging ‘Shovel Knight Dig’ – The...
We spend the bulk of this week’s podcast talking about the new iPhone 14. Specifically, the iPhone 14 Pro Max which both Eli and myself picked up. The consensus seems to be: They’re great! They’re iPhones! We do lay down our hot takes on all the new... | Read more »
TouchArcade Game of the Week: ‘Loose Noz...
There aren’t a lot of stories like that of the development of Loose Nozzles, and of those games that do have an interesting development story, even fewer are actually decent games to play. Loose Nozzles nails both, though. The way it was created is... | Read more »
SwitchArcade Round-Up: ‘Shovel Knight Di...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 23rd, 2022. In today’s article, we’ve got the rest of this week’s releases to look at. There are actually a few big games today, including the hot-hot-hot Shovel Knight Dig... | Read more »
‘Gubbins’ is a Way Too Adorable Word Gam...
There are games whose art style, sounds, and overall vibe just make me smile ear to ear. Games like Hidden Folks, Krispee Street, or Tiny Wings. There’s just something so cool about being able to literally feel the heart that goes into a game. Now... | Read more »
Based on the Baking Reality Show, ‘Naile...
Fans of Netflix’s reality baking show Nailed It! have a new holiday-themed season to look forward to next month when Nailed It! Halloween launches on October 5th, but the fun doesn’t stop there because the show is also arriving as a mobile game the... | Read more »
Cookie Run: Kingdom announces collaborat...
In news sure to excite fans of biscuits or K-Pop music, the Korean sensations BTS have teamed up with Cookie Run: Kingdom for a series of events. After some warm-up episodes, the collaboration will culminate in a BTS in-game concert, so if anyone'... | Read more »
‘Shovel Knight Dig’ From Nitrome and Yac...
Shovel Knight Dig () from Nitrome and Yacht Club Games is this week’s new Apple Arcade release. It is definitely one of my favorite additions to the service ever, and a fantastic game overall. I played it a few hours ago when it started rolling out... | Read more »
SwitchArcade Round-Up: ‘Mario Strikers’...
Hello gentle readers, and welcome to the SwitchArcade Round-Up for September 22nd, 2022. Hunh, lots of twos in the date today. Nifty. As those who read yesterday’s article may remember, I got a vaccine shot about twenty four hours ago and it is... | Read more »
Rogue-Like Platformer ‘Tallowmere 2’ Lau...
The original Tallowmere from developer Chris McFarland launched on mobile way back in 2015, and to be honest it did not leave a good first impression with me. For lack of a better term, it just seemed… janky, and right from the start the game sort... | Read more »
Alchemy Stars newest event launches and...
Alchemy Stars has introduced its latest event, entitled Farewell, My Wonderland, bringing with it new characters and a bevvy of rewards. The event will reportedly focus on the underlying message that even after tragic events there is still light,... | Read more »

Price Scanner via

Use our exclusive Apple Price Trackers to fin...
Our Apple award-winning price trackers are the best place to look for the lowest prices and latest sales on all the latest Apple gear this season. Scan our price trackers for the latest information... Read more
New promo at Verizon: Get Apple Watch Series...
Purchase a new iPhone 14 at Verizon, and get an Apple Watch Series 8 for as low as $5 per month. $120 in promo credits for the Watch are spread over a 36 month term, reducing the price of the Watch... Read more
Visible drops prices on Apple iPhone 13 model...
Verizon’s low-cost wireless cell service, Visible has dropped prices on iPhone 13 models to new low prices starting at $599: – iPhone 13 Pro Max: starting at $980 + free $200 gift card – iPhone 13... Read more
Back in stock! 14″ MacBook Pros with Apple M1...
Amazon has restocked 14″ MacBook Pros M1 Pro CPUs for $400 off MSRP, starting at only $1599. Shipping is free. Be sure to make your purchase from Amazon rather than a third-party seller. Their prices... Read more
This is the final week to take advantage of A...
Apple’s Back to School promotion for 2022 ends on September 26, 2022. As part of this promotion, Apple will include a free $150 Apple Gift Card with the purchase of any MacBook Air, MacBook Pro, or... Read more
Mac Studio with M1 Max CPU back in stock toda...
Apple has the base standard-configuration Mac Studio available again in their Certified Refurbished section for $1799, and it’s in stock today. Each Mac Studio comes with Apple’s one-year warranty,... Read more
Apple MagSafe iPhone battery on sale for $84,...
Amazon has Apple’s MagSafe Battery on sale for $84 today. Shipping is free. That’s $15 off Apple’s MSRP, and it’s the lowest price for one of these MagSafe batteries among the Apple retailers we... Read more
24-inch M1-powered iMacs available today at A...
Apple has a full range of 24-inch M1 iMacs available today in their Certified Refurbished store. Models are available starting at only $1099 and range up to $260 off original MSRP. Each iMac is in... Read more
Verizon offers free Apple iPhone 14 models to...
Verizon is offering a $800-$1000 discounts on Apple’s new iPhone 14 models for new and existing customers with a qualified trade-in. Price of the iPhone 14 will be spread over 36 months of payments,... Read more
Gazelle drops prices on iPhone 13 models to a...
Gazelle has a full line of discounted, refurbished, unlocked Apple iPhone 13 models now available starting at $469. iPhones are offered in Fair, Good, and Excellent conditions, and multiple colors... Read more

Jobs Board

Physician Assistant, Primary Care, *Apple*...
Physician Assistant, Primary Care, Apple Valley (1.07FTE) + Job ID: 65766 + Department: AV Primary Care + City: Apple Valley, MN + Location: HP - Apple Read more
Operations Manager - Mac/ *Apple* Engineerin...
…Responsible for the day-to-day activities relating to the engineering of Apple Macs in a complex, multi-platform environment. Demonstrates strong leadership, Read more
Lead Developer - *Apple* tvOS - Rumble (Uni...
…earnings, and positive sentiment About the role: We are looking for a Lead Apple tvOS Developer to join our application engineering team to expand our video centric Read more
Systems Administrator - *Apple* Devices / J...
…Administration **Duties and Responsibilities** + Configure and maintain the client's Apple Device Management (ADM) solution. The current solution is JAMF supporting Read more
Sr Product Manager, *Apple* TV Platforms -...
…an experienced senior product manager to drive the strategy and requirements for our Apple TV devices, acting as the champion and owner of the holistic experience in Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.