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:

#asm
DeskAccessory 'Name',ID,Flags,Rate,EvtMask,Globals
#endasm

That's all there is to it!

As you can see, the macro has six parameters. The first (always enclosed in single quotes) defines the name of the desk accessory and the second its resource ID (12-31 inclusive). The third, fourth and fifth parameters define constants in the DA header (more on these later). The sixth parameter is a conditional request for global variables to be allocated at runtime, and it takes only two arguments: NeedGlobals or NoGlobals.

If you examine DAHeader.asm in listing 2, you'll notice it has conditional assembly statements. This conditional assembly is dictated by the sixth parameter of the macro. The reason I did this is to save code space. Why mess with handles and pointers if you don't need globals?

So, if you want the code to handle global variables in your DA, specify NeedGlobals as the sixth parameter of the DeskAccessory macro. If you're not going to use any global storage, specify NoGlobals. Simple.

However, there are four very important things to remember if you use this macro:

Always invoke the macro before you declare any global variables or define any Mac C functions! If you do specify NeedGlobals, use the compiler option to set the global address register equal to A4 or a system error will occur at runtime. If NeedGlobals is specified and no global variables are declared in the following C source, a system error is very likely at runtime. Finally, all global variables are initialized to zero.

The flags, delay and event mask parameters can be written in whatever number format you're used to, but remember that C and assembler number formats are not the same.

Although the 16-bit flags contain bits that can be set to enable read, write and status calls, these bits are always cleared in DAHeader.asm. I did this because these three bits are only relevant to device drivers with status and prime routines. The header expects to find three functions in the C source: open, control and close (all in lowercase), and it will not recognize status and prime.

In conclusion

Well, I managed to duplicate most of the functions of DeskMaker exept its ability to test the DA after linking. Shucks. I use Loftus Becker's DA Key for this purpose -- a utility I recommend highly. It's also more stable than DeskMaker's testing function anyway. I also recommend testing any DAs you might write while operating ResEdit, a very harsh environment. ResEdit does many interesting things to the heap, like moving nonrelocatable blocks.

Now take some time and read listing 3. It contains the complete source to a clock DA plus an incredible number of useful comments. The listing is designed to be read from start to finish, and the comments are placed in front of each function to make the source itself more legible. A few of these comments repeat themes discussed by Alan Wooton in previous issues of MacTutor, however they are repeated here for completeness and clarity, and many of them have been expanded.

Using the clock DA as a model, you can write a desk accessory which will survive the most brutal test that I know: running under TMON with heap check, scramble and purge all enabled! Many applications can't even stand that.

Don wins our program of the month award for this outstanding contribution to the Mac C community and $50 from MacTutor!


/* Filename: DeskAccessory.c   compiled with Mac C 4.0 */

/*
-----------------------------------------------------------------------------------
D E S K   A C C E S S O R Y   M A C R O
version 02/26/86

Copyright (C)1986 by Don Melton, all rights reserved.

This file is included in a Mac C source file in order to invoke the DeskAccessory 
macro later in that source file. This macro must be invoked before declaring 
any global variables or defining any Mac C functions. */

#asm
noGlobals SET  0
needGlobals SET  1

MACRO DeskAccessory da1,da2,da3,da4,da5,da6 =

daName  SET {da1}
daID  SET {da2}
daFlags SET {da3}
daServiceRate  SET {da4}
daEventMask SET  {da5}
needGlobals SET  {da6}

INCLUDE DAHeader.asm |
#endasm
Listing 2
; Filename: DAHeader.asm   originally compiled with Mac C 4.0

; ---------------------------------------------------------------------------------
; D E S K   A C C E S S O R Y   H E A D E R
; version 02/26/86

; Copyright (C)1986 by Don Melton, all rights reserved.

; This file is configured and included in a Mac C source file by
; invoking the macro called DeskAccessory defined in
; DeskAccessory.c.

; ---------------------------------------------------------------------------------
; EQUATES

.TRAP _NewHandle $A122  ; defined in SysTraps.txt
.TRAP _DisposHandle$A023
.TRAP _HLock$A029
.TRAP _HUnLock $A02A

clear   SET $200

dCtlStorage SET  $14 ; defined in SysEqu.d
dCtlWindowSET  $1E
ioResultSET $10
jIODone SET $8FC

daFlagMaskSET  $F4FF

; ---------------------------------------------------------------------------------
; GLOBAL VARIABLE DECLARATION

globalSizeDS0
XREF    globalSize

; ---------------------------------------------------------------------------------
; LINKER DIRECTIVE

RESOURCE'DRVR' daID daName 32

; ---------------------------------------------------------------------------------
; DESK ACCESSORY HEADER

 DC.W daFlags & daFlagMask
 ; clear dReadEnable, dWritEnable and dStatEnable
 DC.W daServiceRate
 DC.W daEventMask
 DC.W 0 ; space for menu ID
 DC.W daOpen
 DC.W 0 ; no prime
 DC.W daControl
 DC.W 0 ; no status
 DC.W daClose

; ---------------------------------------------------------------------------------
; GLUE ROUTINES TO MAC C FUNCTIONS
; On entry A0 contains *PB (pointer to parameter block) and
; A1 contains *DCE (pointer to device control entry).

daOpen:
 PEA    open; Mac C function
 BRA.S  main

daClose:
 if needGlobals
 PEA    disposeGlobals  ; intercept routine
 else
 PEA    close    ; Mac C function
 endif
 BRA.S  main

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

main:
 if needGlobals
 MOVEM.LD4-D7/A0/A1/A4-A6,-(SP)  ; save registers
 MOVEA.LdCtlStorage(A1),A0; get handle
 MOVE.L A0,D0    ; empty handle?
 BNE.S  lockGlobals; no, lock globals

; allocateGlobals
 CLR.L  D0; clear high word
 ; since MOVE.W #globalSize,D0 causes an error ...
 DC.W $303C ; kludge instruction
 DC.W globalSize ; resolved by linker
 NEG.W  D0; make it positive
 _NewHandle ,clear
 BEQ.S  initGlobals; if no error, init

 MOVE.W #$-1,D0  ; error result
 BRA.S  exit

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

lockGlobals:
 _HLock
 ; Mac C expects A4 to be pointer to global variables
 MOVEA.L(A0),A4  ; dereference handle
 ; since MOVE.W #globalSize,D0 causes an error ...
 DC.W $303C ; kludge instruction
 DC.W globalSize ; resolved by linker
 SUBA.W D0,A4    ; add to globals ptr
 ; Mac C expects *PB in D0 and *DCE in D1
 MOVE.L 16(SP),D0; get *PB from stack
 MOVE.L 20(SP),D1; get *DCE
 MOVEA.L36(SP),A0; get offset and ...
 JSR    (A0); do Mac C routine

; unlockGlobals
 ; only Mac C open and control routines return here
 BSR.S  findGlobals; get handle
 _HUnlock

restoreResult:
 MOVE.L D7,D0    ; restore result

exit:
 MOVEM.L(SP)+,D4-D7/A0/A1/A4-A6  ; restore registers
 MOVE.W D0,ioResult(A0) ; force result
 ADDQ.L #4,SP    ; burn function ptr
 ; open and close return to the trap dispatcher
 ; control goes to jIODone
 RTS

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

findGlobals:
 ; since HUnlock and DisposHandle trash result in D0 ...
 MOVE.L D0,D7    ; save result
 ; since Mac C routine might trash *DCE in A1 ...
 MOVEA.L24(SP),A1; restore *DCE
 MOVEA.LdCtlStorage(A1),A0; get handle
 RTS

 else
; main routine if no global variables are needed
 MOVEM.LD4-D7/A0/A1/A4-A6,-(SP)  ; save registers
 ; Mac C expects *pb in D0 and *dce in D1
 MOVE.L 16(SP),D0; get *pb from stack
 MOVE.L 20(SP),D1; get *dce
 MOVEA.L36(SP),A0; get offset and ...
 JSR    (A0); do Mac C routine

 MOVEM.L(SP)+,D4-D7/A0/A1/A4-A6  ; restore registers
 MOVE.W D0,ioResult(A0) ; force result
 ADDQ.L #4,SP    ; burn function ptr
 ; open and close return to the trap dispatcher
 ; control goes to jIODone
 RTS
 endif



/* Filename: Clock.c   originally compiled with Mac C 4.0 */

/*
-----------------------------------------------------------------------------------
C L O C K   version 02/26/86

Copyright (C)1986 by Don Melton, all rights reserved.

Clock is a desk accessory which opens a window displaying the current 
time in hours, minutes and seconds. It has a menu allowing the choice 
of displaying the time or date, or an "About " dialog.
    This is an example of how to create a desk accessory with Consulair 
Mac C without relying on the DeskMaker application. The source code is 
provided as a reference for Macintosh software developers. The clock 
desk accessory itself may be freely distributed as long as the copyright 
notice remains intact.
    -- Don Melton, CIS: 74166,1006 */

/*
-----------------------------------------------------------------------------------
MAC C COMPILER OPTIONS
    Setup A4 as the index to global variables and inhibit floating point. 
*/

#Options R=4 Z

/*
-----------------------------------------------------------------------------------
HEADER FILES */

#include <MacDefs.h>
#include <QuickDraw.h>
#include <Font.h>
#include <Window.h>
#include <TextEdit.h>
#include <Dialog.h>
#include <Menu.h>
#include <Events.h>
#include <Device.h>
#include <Packages.h>

#include <DeskAccessory.c>

/*
-----------------------------------------------------------------------------------
MODIFIED DEFINITIONS
    OSIO.h is not included because the OpParamType union structure (as 
defined) does not provide access to the menu item. It is redefined here 
to include menuData and the event pointer.
    Note: Other alternate elements of the OpParamType union structure 
are not defined here!
    The CntrlParam structure also must be defined because OSIO.h is not 
included. However, it remains unaltered. */

union __OP
  { 
  struct
    {
    short menuID;
    short menuItem;
    } menuData;
  Ptr event;
  };

#define OpParamType union __OP

struct __CP
  {
  struct __CP *ioLink;
  short ioType;
  short ioTrap;
  Ptr ioCmdAddr;
  ProcPtr ioCompletion;
  short ioResult;
  char *ioNamePtr;
  short ioVRefNum;
  short ioRefNum;
  short CSCode;
  OpParamType csp;
  };

#define CntrlParam struct __CP

/*
-----------------------------------------------------------------------------------
DEFINITIONS NOT IN MAC C HEADER FILES */

typedef struct
  {
  char typeName[4];
  } ResType;

/*
-----------------------------------------------------------------------------------
CONSTANT DEFINITIONS */

#define NIL 0
#define FALSE 0
#define TRUE 1

#define FREE_BLOCK_SIZE 0x1000
#define FRONT_WINDOW -1

#define ABOUT_DLOG 1
#define DISPLAY_ITEM 1
#define TIME_ITEM 1
#define DATE_ITEM 2
#define ABOUT_ITEM 4
#define CLOCK_MENU 0

#define TIME 0x020C
#define WANT_SECONDS 0x0100
#define TIME_SELECTION 2
#define DATE_SELECTION 0

/*
-----------------------------------------------------------------------------------
SETUP DA HEADER AND GLUE ROUTINES
    The assembly language macro from DeskAccessory.c is invoked here. 
The order of parameters is:

'Name',ID,Flags,ServiceRate,EventMask,GlobalsRequest

    The DA event mask is set here to mouseDown, update and activate, 
but DAs will receive these events even if the mask is set to $0000. Since 
this is yet another undocumented “feature,” it's best to be safe and 
use the correct mask because future versions of the ROMs might behave 
differently. */

#asm
DeskAccessory 'Clock',12,$2400,$0010,$0142,NeedGlobals
#endasm

/*
-----------------------------------------------------------------------------------
FORWARD REFERENCES */

doModal();

/*
-----------------------------------------------------------------------------------
GLOBAL VARIABLES */

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

/*
-----------------------------------------------------------------------------------
OPEN CLOCK
    Since the FontDA/Mover renumbers all the IDs of any DA's resources 
whenever it moves a DA into another file, the new IDs must be calculated 
at runtime. The resource ID of a DRVR can be in the range 12-31, inclusive. 
Its owned resources are numbered differently. In this text, the global 
variable ownedID must contain the owned resources base ID number. The 
resource ID of the clock DRVR  is initially 12, so its ownedID will be 
-16000. The ownedID can easily be derived from the dCtlRefNum field of 
the device control entry. [See the Assembly Lab in this issue for the 
formula for this. -Ed]
    Setting the dCtlMenu equal to the ownedID is done before checking 
whether the DA is already open, because an open() call can come while 
the DA is still active. The Desk Manager resets dCtlMenu to whatever 
is in the DA header (in this case zero) every time a DA is opened, so 
dCtlMenu must be reinitialized.
    It's possible to change the DA header at runtime to reflect the correct 
menu ID, but certain situations could actually cause the DRVR resource 
to be temporarily purged while the DA window was still open, thereby 
invalidating the patch. Besides, it's an ugly business to patch what 
is essentially a “code” resource.
    It's unecessary to recalculate the ownedID every time open() is called, 
but placing it outside the conditional saves code space.
    The MENU resource is made unpurgeable here because other events may 
cause heap compaction, purge the menu, and invalidate the menu handle. 
The menu is released from the heap during the DA close() function.
    All owned resources (except possibly a MENU) including the DRVR itself 
should be preset as purgeable. Please note that this must be done with 
ResEdit, because RMaker does not allow this option at compilation.
    The windowKind field of the primary DA window should always be set 
equal to the dCtlRefNum field of the device control entry. The system 
needs a negative number in this field to recognize the window as belonging 
to a DA, and a specific number identifies a specific DA.
    The clock dialog is defined as invisible in the resource directive, 
Clock.r, because on exit of open(), the Desk Manager will always bring 
the DA window to the front of the desktop and make it visible.  The advantage 
to making the window initially invisible is purely aesthetic.  When GetNewDialog() 
is called, the Dialog Manager draws the window frame, reads the DITL 
resource into memory, makes a copy of it, and then begins drawing the 
items.  If the dialog contains a complex item list, waiting until the 
Desk Manager makes it visible will cause the dialog to be drawn faster.
    Setting the port to the clock dialog is done here because the TextMode 
of the grafPort is set to scrCopy. Getting and restoring the old port 
is also a good idea, however everything works properly if this is not 
done.
    To keep the DA window pointer away from other nonrelocatable blocks 
at the bottom of the heap, a 4K temporary space is allocated before the 
DA window pointer is created. Later it is disposed of during close(), 
or on an error of open(). This prevents possible heap fragmentation. 
An application can allocate more nonrelocatable blocks while a DA is 
active, and therefore can create a hole to small to reallocate when the 
DA is closed and its window pointer is removed from the heap. This DA 
also causes Pack6 to be loaded on to the heap. Pack6 is a locked resource 
which will remain on the heap until the heap is reinitialized.
    It is unnecessary for this function to draw the clock display or 
insert the clock menu because update and activate events are generated 
when a DA is first opened. */


short open(pb, dce)
  CntrlParam *pb;
  DCEntry *dce;
  {
  GrafPtr oldPort;
  Ptr freeBlock;
  ResType dummyType;
  FontInfo theFontInfo;

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

  if (!dce->dCtlWindow)
    {
    GetPort(&oldPort);

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

    if (!(clockDialog =
      (DialogPtr) NewPtr(sizeof(DialogRecord))))
      {
      DisposPtr(freeBlock);
      return -1;
      }
    clockDialog = GetNewDialog(ownedID, clockDialog,
      FRONT_WINDOW);
    ((WindowPeek) clockDialog)->windowKind =
      dce->dCtlRefNum;
    dce->dCtlWindow = (Ptr) clockDialog;
    SetPort((GrafPtr) clockDialog);
    TextMode(srcCopy);
  
    hClockMenu = GetMenu(ownedID);
    HNoPurge(hClockMenu);

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

    DisposPtr(freeBlock);

    SetPort(oldPort);
    }
  return 0;
  }

/*
-----------------------------------------------------------------------------------
CLOSE CLOCK 
    The MENU resource must be released from the heap here because an 
error will always occur during the next call to GetMenu() in the open() 
function (when the DA is selected again from the Apple menu) only during 
the operation of Finder versions 5.0 and above on the older 64K ROMs. 
As of this writing, no other application environments produce the error. 
Normally, the MENU resource would only need to be made purgeable.
    When GetMenu() is invoked it calls several other ROM routines including 
GetResource(),  CountTypes(), CalcMenuSize(), GetItem(), MenuSelect() 
and LoadResource(). During the conditions mentioned above, on exit of 
GetItem(), a JSR (A0) instruction will produce an address error at a 
location above the 64K ROMs if the DA menu is on the heap. This is because 
the newer Finders jump directly in and out the 128K ROMs at absolute 
locations, so they expect ROM code at specific memory addresses.
    If the DA menu is not on the heap when GetMenu() is invoked during 
this environment, no error will occur.
    Apple Computer does not recommend using the newer Finders on the 
older ROMs. However, since many users have HD20s hooked to their older 
Macs, they have to use the newer Finders. It's a good idea to prepare 
for these circumstances since there's no gain in code space, and the 
only operational difference is that the MENU resource must be reloaded 
every time the DA is opened. */

short close(pb, dce)
  CntrlParam *pb;
  DCEntry *dce;
  {

  deActivate(dce);
  ReleaseResource(hClockMenu);
  DisposeDialog(clockDialog);
  dce->dCtlWindow = NIL;

  return 0;
  }

/*
-----------------------------------------------------------------------------------
CONTROL CLOCK
    Contrary to popular practice, it's unnecessary to check the dCtlWindow 
field of the device control entry to determine whether the DA exists 
during control(). If a DA receives a call to control(), the DA had better 
exist! The only reason to check this field during control() is to determine 
whether the DA is the frontmost window. This is unnecessary in this DA.
    Setting the port to the clock dialog is done in drawDisplay() rather 
than here, since drawDisplay() is the only routine drawing anything in 
the grafPort. */

short control(pb, dce)
  CntrlParam *pb;
  DCEntry *dce;
  {

  switch (pb->CSCode)
    {
    case accEvent:
      doEvent((EventRecord *) pb->csp.event);
      break;

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

    default:
      drawDisplay();
    }
  return 0;
  }

/*
-----------------------------------------------------------------------------------
MAIN EVENT LOOP */

doEvent(theEvent)
  EventRecord *theEvent;
  {

  switch (theEvent->what)
    {
    case updateEvt:
      updateClock();
      break;

    case activateEvt:
      if (theEvent->modifiers & activeFlag)
        activate();
      else
        deActivate();
      break;

    default:
      drawDisplay();
    }
  }

/*
-----------------------------------------------------------------------------------
UPDATE CLOCK WINDOW
    Normally, if a dialog contains items such as buttons or text, a call 
to DrawDialog() is used between the calls to BeginUpdate() and EndUpdate(). 
In the case this DA, a call to DrawDialog() is not only unnecessary, 
it will also cause an annoying flicker in the time/date display. This 
is because the empty static text item used to position the display will 
erase the dispaly again during the update. */

updateClock()
  {

  BeginUpdate(clockDialog);
  clockDirty = TRUE;
  drawDisplay();
  EndUpdate(clockDialog);
  }

/*
-----------------------------------------------------------------------------------
ACTIVATE CLOCK MENU
    Because older versions of the Font/DA Mover don't correctly reset 
the menuID of a MENU resource (not always the same as the resource ID!) 
when a DA is moved into another file, it must be patched here at runtime 
before the menu is inserted in the menubar. */

activate()
  {
  
  (*hClockMenu)->menuID = ownedID;
  InsertMenu(hClockMenu, CLOCK_MENU);
  DrawMenuBar();
  }

/*
-----------------------------------------------------------------------------------
DEACTIVATE CLOCK MENU */

deActivate()
  {

  DeleteMenu(ownedID);
  DrawMenuBar();
  }

/*
-----------------------------------------------------------------------------------
DO MENU
    Choosing "About " in the "Clock" menu will invoke ModalDialog(). 
Since one of the first things ModalDialog() does is call SystemTask(), 
the DA control routine can be called again. In the case of this DA, this 
problem of re-entrancy will not cause ModalDialog() to be invoked again 
because it can't be selected from the menu once the modal dialog is active.
    Remember that the code in DAHeader.asm which calls control(), locks 
the global variables on the heap on entrance and then unlocks them on 
exit. The Desk Manager does this same thing to the DRVR code resource 
before and after the DA is called.
    To avoid the possibility of ModalDialog() causing heap compaction 
and moving either the DRVR code resource or the globals while unlocked, 
the device control entry of this DA is modified before ModalDialog() 
is invoked. If the DRVR was allowed to move during a call to ModalDialog(), 
the ROM routine could return to a memory address that no longer contained 
the DRVR code.
    First, to prevent control() from being called by SystemTask(), the 
dCtlEnable bit of the dCtlFlags field in the dce is cleared. This makes 
certain all global variables remain locked on the heap, because control() 
will not exit
until the modal dialog routine is completed and the dCtlEnable bit is 
reset.
    Second, the dNeedLock bit of the  dCtlFlags word is set to make certain 
the DRVR code resource remains locked on the heap. If the dNeedLock bit 
is set in the actual flags of the DA header, this step is unnecessary. 
However, using this technique makes presetting the dNeedLock bit unneccesary 
in most situations.
    Some programmers prefer to allow a DA to receive calls to control() 
while a modal dialog is active so the DA can still be performing certain 
tasks in the background. This is accomplished by using a complex technique 
which checks, clears or sets the dNeedLock bit on entrance to control() 
and then performs similar actions on exit. This techniques works OK if 
globals are handled differently than in DAHeader.asm. However, it is 
much more confusing to constantly check the status of dNeedLock, than 
to use the simple techniques presented in this text, which still allow 
this DA to work while the modal dialog is active. */

doMenu(menuItem, dce)
  short menuItem;
  DCEntry *dce;
  {
  short theItem;
  DialogPtr aboutDialog;

  switch (menuItem)
    {
    case TIME_ITEM:
      if (clockSelection != TIME_SELECTION)
        clockDirty =TRUE;
      clockFormat = WANT_SECONDS;
      clockSelection = TIME_SELECTION;
      drawDisplay();
      break;

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

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

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

      DisposeDialog(aboutDialog);
      dce->dCtlFlags ^= 0x0400; /* set dCtlEnable */
      dce->dCtlFlags &= 0xBFFF; /* clear dNeedLock */
    }
  HiliteMenu(CLOCK_MENU);
  }

/*
-----------------------------------------------------------------------------------
MODAL DIALOG FILTER FUNCTION
    Since the DA control routine is disabled before ModalDialog() is 
invoked, and SystemTask() can no longer call control() which then calls 
drawDisplay(); this function, called by ModalDialog(), draws the clock 
display. It must also check the modal dialog event record for a keypress 
character code equal to Return or Enter, and return a result to ModalDialog().
    Since Mac C passes most parameters in registers, doModal() must be 
written in assembly language.

The Lisa Pascal format for a dialog filter function is:
  PROCEDURE MyFilter(theDialog: DialogPtr; VAR theEvent:
    EventRecord; VAR itemHit: INTEGER) : BOOLEAN;

On entry the stack contains (in descending order):
  space for boolean result (word)
  pointer to modal dialog (long)
  pointer to dialog event record (long)
  pointer to dialog item hit (long)
  return address (long)
On exit:
  boolean result (word)
  return address (long)

    The “correct” method of addressing parameters passed to a subroutine 
on the stack is to define them in a stack frame via a LINK An instruction 
on entrance and UNLK An on exit. However, the “correct” method takes 
more code space and is
not especially any more legible than the implementation here.
    If Return or Enter characters have been generated from the keyboard, 
doModal() must set itemHit equal to 1 and return a result of TRUE. If 
not, it must return a result of FALSE so ModalDialog() will handle the 
event.
    Everything works correctly without saving any registers before calling 
drawDisplay(), but saving registers is always a good idea. */

doModal()
  {

  #asm
 MOVE.L (SP)+,D0 ; save return address
 MOVEA.L(SP)+,A0 ; save item hit ptr
 MOVEA.L(SP)+,A1 ; save event record ptr
 MOVE.L D0,(SP)  ; restore return address
 ; and trash dialog ptr
 MOVE.W (A1),D0  ; get evtNum
 CMPI.W #3,D0  ; keyDwnEvt?
 BNE.S  noKeyEvent

 MOVE.W 4(A1),D0 ; get evtMessage (low word)
 ; check the character code, NOT the key code!
 CMPI.B #3,D0  ; Enter character?
 BEQ.S  setItemHit

 CMPI.B #13,D0 ; Return character?
 BNE.S  noKeyEvent

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

 noKeyEvent:
 CLR.W  4(SP)  ; result is FALSE
 MOVEM.LD3-D7/A3-A4,-(SP) ; save registers
 JSR  drawDisplay
 MOVEM.L(SP)+,D3-D7/A3-A4 ; restore registers
 ; RTS is inserted by the compiler after "}"
  #endasm
  }

/*
-----------------------------------------------------------------------------------
DRAW CLOCK DISPLAY
    Setting the port to the clock dialog is done here rather than in 
control(), because this function is also called by doModal(). However, 
everything works properly if the port is set only in control() and not 
here. Setting the port here is just a good idea. Getting and restoring 
the old port is also a good idea, however everything works properly if 
this is not done either.
    The current time is fetched from the low-memory system global "Time" 
($020C) using C typecasting and indirection. Since the current time is 
not needed in many different places in this source, this technique is 
faster and takes less code than writing an assembly language function. 
Also there's no equivalent in Mac C to the Lisa Pascal routine:
  PROCEDURE GetDateTime(VAR secs: LONGINT);
  However, a function could be defined similar to this procedure but 
returning a long result, rather than having a variable passed to it. 
For example:

long getDateTime()
  {

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

drawDisplay()
  {
  GrafPtr oldPort;
  long newDateTime;
  Str255 newDTString;
  short newWidth;

  if ((clockDirty) || ((newDateTime = *((long *) TIME)) !=
    oldDateTime))
    {
    GetPort(&oldPort);
    SetPort((GrafPtr) clockDialog);

    oldDateTime = newDateTime;
    dTimeToString(clockFormat, &newDTString,
      clockSelection);
    if ((clockDirty) || ((newWidth =
      StringWidth(&newDTString)) < oldWidth))
      {
      oldWidth = newWidth;
      EraseRect(&displayRect);
      clockDirty = FALSE;
      }
    MoveTo(textLeft,textBase);
    DrawString(&newDTString);

    SetPort(oldPort);
    }
  }

/*
-----------------------------------------------------------------------------------
CONVERT DATE OR TIME TO STRING
    This is a variation on two Lisa Pacal procedures contained in the 
international utilities package. There are no equivalents to these procedures 
in Mac C. Here the dateTime parameter is not used. Instead, the current 
time is fetched and placed on the stack, and a new parameter allows selection 
between date and time.
DA for Mac C 2

Continued from - DA for Mac C


    The first parameter determines the format of the output. This is 
either the constants WANT_SECONDS or FALSE for time; or shortDate, longDate 
or medDate, for the date.
    WANT_SECONDS is defined as $0100 because it is a boolean TRUE, and 
therefore bit 1 of the high byte must be set. Actually any bit set in 
the high byte will work but setting bit 1 is the proper method.
    A pointer to the string which will contain the time or date characters 
is the second parameter.
    The third parameter is the selector for the Pack6 trap, either TIME_SELECTION 
or DATE_SELECTION.

The Lisa Pascal format for the original two routines is:
  PROCEDURE IUDateString(dateTime: LONGINT; form:
    DateForm; VAR result: Str255);
  PROCEDURE IUTimeString(dateTime: LONGINT;
    wantSeconds: BOOLEAN; VAR result: Str255); */

dTimeToString(theFormat, theStr, theSelector)
  short theFormat;
  Str255 *theStr;
  short theSelector;
  {

  #asm
 MOVE.L $020C,-(SP); Time
 MOVE.W D0,-(SP) ; wantSeconds or dateForm
 MOVE.L D1,-(SP) ; theStr
 MOVE.W D2,-(SP) ; routine selector
 DC.W $A9ED ; _Pack6
 ; RTS is inserted by the compiler after "}"
  #endasm
  }




DA:Clock.rsrc


TYPE DLOG
  ,-16000
Clock
48 209 72 303
Invisible GoAway
4
0
-16000

  ,-15999

103 82 239 430
Visible NoGoAway
1
0
-15999


TYPE DITL
  ,-16000
1

StaticText Disabled
4 6 20 88


  ,-15999
5

Button
108 268 126 338
OK

StaticText Disabled
10 10 26 338
Clock

StaticText Disabled
34 10 50 338
©1986 by Don Melton, all rights reserved.

StaticText Disabled
66 10 82 338
A demonstration desk accessory designed for

StaticText Disabled
82 10 98 338
MacTutor™ magazine and Consulair Corp.


TYPE MENU
  ,-16000
Clock
Time
Date
(-
About 


From Volume 2 Number 5:

Clock DA source correction

Don Melton

Santa Ana, CA

For those of you typing in the Clock DA I wrote for the April 1986 issue of MacTutor, be warned, there is a typo in the magazine source listing. It's my fault, not David Smith's -- probably my old brain tumor acting up again. Thanks go to MacScotty of the MouseHole BBS for finding this error. Turn to page 46, and in the 'open' function you'll find it also. Here's the problem line and the correction:

INCORRECT SOURCE:

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

CORRECT SOURCE:

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

It seems I accidently used an older variable name from a previous version of my source code. This mistake does NOT appear on the source code disk available from MacTutor.

There is also a non-destructive typo later in the same source. It does not cause errors but I thought you all might want to know about it anyway. Thanks go to Katz of the MouseHole BBS for pointing out this one. Turn to page 48, and in the 'doMenu' function you'll find these four lines:

QUESTIONABLE SOURCE:

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

BETTER SOURCE:

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

BEST SOURCE (but be careful using it):

 dce->dCtlFlags ^= 0x4400;
 /* clear dCtlEnable and set dNeedLock*/
 :
 dce->dCtlFlags ^= 0x4400;
 /* set dCtlEnable and clear dNeedLock*/

I accidently typed an XOR to manipulate the dNeedLock bit instead of an ordinary OR. An XOR works but it is not what I intended. Of course, as the 'BEST' example shows, you can manipulate both bits with an XOR. However, I did not include this example in the original source because I thought it might be confusing.

Please pass this information on to your local BBS. And feel free to ask me any questions about the Clock DA or the DA Header source via MacTutor, Compuserve (74166,1006) or Delphi (DONMELTON). Thanks.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

Make the passage of time your plaything...
While some of us are still waiting for a chance to get our hands on Ash Prime - yes, don’t remind me I could currently buy him this month I’m barely hanging on - Digital Extremes has announced its next anticipated Prime Form for Warframe. Starting... | Read more »
If you can find it and fit through the d...
The holy trinity of amazing company names have come together, to release their equally amazing and adorable mobile game, Hamster Inn. Published by HyperBeard Games, and co-developed by Mum Not Proud and Little Sasquatch Studios, it's time to... | Read more »
Amikin Survival opens for pre-orders on...
Join me on the wonderful trip down the inspiration rabbit hole; much as Palworld seemingly “borrowed” many aspects from the hit Pokemon franchise, it is time for the heavily armed animal survival to also spawn some illegitimate children as Helio... | Read more »
PUBG Mobile teams up with global phenome...
Since launching in 2019, SpyxFamily has exploded to damn near catastrophic popularity, so it was only a matter of time before a mobile game snapped up a collaboration. Enter PUBG Mobile. Until May 12th, players will be able to collect a host of... | Read more »
Embark into the frozen tundra of certain...
Chucklefish, developers of hit action-adventure sandbox game Starbound and owner of one of the cutest logos in gaming, has released their roguelike deck-builder Wildfrost. Created alongside developers Gaziter and Deadpan Games, Wildfrost will... | Read more »
MoreFun Studios has announced Season 4,...
Tension has escalated in the ever-volatile world of Arena Breakout, as your old pal Randall Fisher and bosses Fred and Perrero continue to lob insults and explosives at each other, bringing us to a new phase of warfare. Season 4, Into The Fog of... | Read more »
Top Mobile Game Discounts
Every day, we pick out a curated list of the best mobile discounts on the App Store and post them here. This list won't be comprehensive, but it every game on it is recommended. Feel free to check out the coverage we did on them in the links below... | Read more »
Marvel Future Fight celebrates nine year...
Announced alongside an advertising image I can only assume was aimed squarely at myself with the prominent Deadpool and Odin featured on it, Netmarble has revealed their celebrations for the 9th anniversary of Marvel Future Fight. The Countdown... | Read more »
HoYoFair 2024 prepares to showcase over...
To say Genshin Impact took the world by storm when it was released would be an understatement. However, I think the most surprising part of the launch was just how much further it went than gaming. There have been concerts, art shows, massive... | Read more »
Explore some of BBCs' most iconic s...
Despite your personal opinion on the BBC at a managerial level, it is undeniable that it has overseen some fantastic British shows in the past, and now thanks to a partnership with Roblox, players will be able to interact with some of these... | Read more »

Price Scanner via MacPrices.net

You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more
Sunday Sale: 13-inch M3 MacBook Air for $999,...
Several Apple retailers have the new 13″ MacBook Air with an M3 CPU in stock and on sale today for only $999 in Midnight. These are the lowest prices currently available for new 13″ M3 MacBook Airs... Read more
Multiple Apple retailers are offering 13-inch...
Several Apple retailers have 13″ MacBook Airs with M2 CPUs in stock and on sale this weekend starting at only $849 in Space Gray, Silver, Starlight, and Midnight colors. These are the lowest prices... Read more
Roundup of Verizon’s April Apple iPhone Promo...
Verizon is offering a number of iPhone deals for the month of April. Switch, and open a new of service, and you can qualify for a free iPhone 15 or heavy monthly discounts on other models: – 128GB... Read more

Jobs Board

Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
Liquor Stock Clerk - S. *Apple* St. - Idaho...
Liquor Stock Clerk - S. Apple St. Boise Posting Begin Date: 2023/10/10 Posting End Date: 2024/10/14 Category: Retail Sub Category: Customer Service Work Type: Part Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.