TweetFollow Us on Twitter

Cursor Animation
Volume Number:7
Issue Number:3
Column Tag:C Workshop

Related Info: Vert. Retrace Mgr

Animated Color Cursors

By Richard Lesh, Bridgeton, MO

Note: Source code files accompanying article are located on MacTech CD-ROM or source code disks.

Animated Color Cursors

[Richard Lesh is a free-lance programmer who has been programming in C since 1983 and has been doing development on the Macintosh for the past four years. He has done work in a number of fields including numerical methods and analysis, computer graphics, artificial intelligence and neural networks. You can contact him on AppleLink at CALICO or through INTERNET at CALICO@APPLELINK.APPLE.COM]

Introduction

Since the introduction of HyperCard, more and more programs have been sporting animated cursors. Using animated cursors is an attractive way to provide visual feedback to the user that the computer is diligently working on a time consuming task. The standard wrist watch cursor does not provide this type of dynamic feedback to assure the user that things are proceeding normally. This article will present a new method of implementing animated cursors that supports both monochrome and color cursors. Also presented is a simple demo program that exercises the cursor control routines using a number of monochrome and color cursors.

Background

In the past there have been two approaches to implementing animated cursors. The first type is seen in HyperCard, the Finder and in the MPW library cursor control routines. Using this technique, the programmer simply intersperses frequent calls to a routine that sets the cursor to the next cursor in the animation sequence. It typically results in cursor animation that is not smooth, i.e. jerky. The second and less popular method is to set up a VBL task that changes the cursor at a constant rate, thus providing smooth animation. More about these techniques and VBL tasks will be presented later in this article. Both of these techniques have shortcomings that the technique presented here attempts to overcome.

As a general rule of thumb, there are a number of issues that any animated cursor technique should address:

1. It should provide feedback that the computer is executing a task normally.

2. It should provide smooth animation of the cursor.

3. It should support both monochrome (CURS) and color (crsr) cursors.

Each of these issues will now be discussed and the methodology for addressing each will be presented.

Visual Feedback

The primary reason for using animated cursors is to give some reassuring feedback to the user that everything is proceeding normally during a lengthy task that otherwise would not provide visual feedback. In order to set up a sequence of animated cursors, one must first create separate cursor resources for each ‘frame’ of the animation. Each ‘frame’ differs slightly from the next and when displayed in rapid succession they give the illusion of motion. Figure 1 shows an example of the monochrome (CURS) Beach Ball cursor resources commonly seen in HyperCard.

Figure 1. Beach Ball cursors (CURS)

In addition to creating the necessary cursor resources, a resource must be created to store the information about the sequencing of the cursor ‘frames’. This resource is supported by the latest versions of ResEdit and is given the type name ‘acur’ (animated cursor). Figure 2 shows the ‘acur’ resource, as displayed by ResEdit, needed to animate the Beach Ball cursors.

The ‘acur’ resource contains a count of the number of cursor resources in the animation sequence followed by the resource IDs of the cursors in the order that they will appear in the sequence. The counter ‘Number of “Frames”’ is not maintained automatically for you; you must make sure to update it after adding additional cursor ‘frames’. To insert new ‘frames’ in the resource one merely selects the ‘*****’ string and then chooses ‘Insert New Field’ from the Resource menu in ResEdit to insert a space for the new cursor resource ID. The “Frame Counter” field is simply a filler and its value is unimportant. This value will, however, be used to keep track of the current frame after the ‘acur’ resource has been loaded and the cursor animation begins.

Smooth Animation

MPW and HyperCard both use a cursor animation method that requires the execution of specific commands to cause the animation to proceed to the next frame. In the MPW cursor control library these functions are called SpinCursor() and RotateCursor(). Each are similar in function but take different arguments. In HyperCard, the command ‘set cursor to busy’ performs the same function. This technique requires the programmer to sprinkle cursor commands generously throughout the code of a function that may cause a noticeable delay to the user. Not only does this destroy the readability of a code segment but it is difficult in practice to space the calls to SpinCursor() or RotateCursor() at appropriate locations that will generate smooth animation of the cursor. It is more common for the cursor to display somewhat random, jerky motion. This is undesirable and can be overcome by setting up a vertical blanking (VBL) task that will automatically change the cursor to the next cursor in the sequence at a predetermined time interval. The programmer now only has to start the cursor animation before a time consuming task and then remember to stop the animation when the task completes.

Figure 2. ‘acur’ Resource

VBL Tasks

Those who are not familiar with VBL tasks can refer to the “Vertical Retrace Manager” chapter of Inside Macintosh, Vol. II. In short, the Macintosh video circuitry generates an interrupt, known as the vertical blanking interrupt, 60 times a second. This interrupt signals applications that the beam of the display tube is travelling from the bottom right of the screen back to the top left. Since pixels are not being refreshed by the electron beam during this short interval, it is a good time for applications to draw to the screen without fear of the screen being partially updated before the drawing operation is finished. This facilitates smooth, flicker-free animation. Since the VBL interrupt occurs at constant intervals, it is also a good time to do periodic tasks such as incrementing the tick count, handling cursor movement and posting mouse and keyboard events, all of which the system does for you during VBL interrupts. One can take advantage of the VBL interrupt by introducing a user defined task into the VBL queue to be executed periodically.

There are two routines needed to setup VBL tasks, one to insert the task into the VBL queue, InstallVBLTask(), and one to remove the task when it is no longer needed, RemoveVBLTask(). These routines require the definition of the VBLTask structure defined in VRetraceMgr.h. To install a task one needs to pass a pointer to the procedure to be executed at interrupt time and the interval to wait between executions of the procedure. The VBL task procedure must be declared as a Pascal void function with no arguments. These two functions are very simple and are presented below.

/* 1 */

VBLTask *InstallVBLTask(ProcPtr proc, short ticks)
{
 OSErr err;
 VBLTask *taskPtr;
 
 taskPtr=(VBLTask *)NewPtr(sizeof(VBLTask));
 if (taskPtr){
 taskPtr->qType=vType;
 taskPtr->vblAddr=proc;
 taskPtr->vblCount=ticks;
 taskPtr->vblPhase=0;
 err=VInstall((QElemPtr)taskPtr);
 if (err!=noErr){
 DisposPtr(taskPtr);
 taskPtr=NULL;
 }
 }
 return(taskPtr);
}

void RemoveVBLTask(VBLTask *taskPtr)
{
 VRemove((QElemPtr)taskPtr);
 DisposPtr(taskPtr);
}

While the VBL technique is easy to implement and has been implemented in a number of commercial packages, by itself it is not an acceptable approach. This is because the user can no longer have confidence that the program is working properly when the cursor is still spinning. By using a VBL task to separate the cursor animation from the execution of the program, situations can occur where the program has crashed but the cursor keeps spinning away. This problem can be overcome by combining both techniques, thereby creating a function, such as our new SpinCursor(), that instead of setting the cursor to the next cursor in the ‘acur’ sequence, simply stores a duration value in a global variable that is then accessed by a VBL task. The VBL task smoothly animates the cursor but is constantly decrementing the count stored in the global duration variable. If SpinCursor() is not periodically called to reset the duration variable, the VBL task will eventually ‘time out’ and the cursor animation will cease. In this implementation SpinCursor() is called with an integer argument that specifies the number of seconds to spin the cursor before it will ‘time out’. The function CursorSpeed() can be called to set the number of ticks that occur between each frame of the animation.

The VBL task that actually changes the cursor in these cursor routines is called SpinCursorTask(). The ticks parameter passed to InstallVBLTask() specifies the number of ticks (1/60th of a second) to wait between successive calls to the task. This ticks parameter is then stored in the vblCount field of the VBLTask structure. This value is decremented once during each VBL interrupt, and when it reaches zero, the procedure passed in the proc argument is called. The VBL task is then responsible for resetting the value of the vblCount field so that it will be called again after the appropriate interval. The VBL task can access the original value of ticks because it is defined and stored in the global gSpeed by a call to the procedure SetCursorSpeed(). Notice that our VBL task procedure not only resets the vblCount field so that it will be called again, but it also decrements the global variable gSpinCycles and changes the cursor to the next cursor in the animation sequence. The global variable gSpinCycles is set only by the SpinCursor() procedure and when gSpinCycles reaches zero the cursor has ‘timed out’ and will no longer be animated by the VBL task. In order to access global variables like gSpinCycles and gSpeed, the calls to SetCurrentA5() and SetA5() are necessary to ensure the proper contents of the A5 register. The older routines, SetUpA5() and RestoreA5(), are no longer recommended (See Technical Note #208).

/* 2 */

static pascal void SpinCursorTask()
{
 long oldA5;

 oldA5=SetCurrentA5();
 gCursorTask->vblCount=gSpeed;
 if (gSpinCycles){
 gSpinCycles--;
 (*gCurrentHdl)->index++;
 (*gCurrentHdl)->index%=(*gCurrentHdl)->n;
 if (gColorCursor) 
 SetCCursor( );
 else
 SetCursor( );
 }
 SetA5(oldA5);
}

Color Cursors

Unlike HyperCard and the MPW cursor control library, the functions presented here support the use of color cursors (crsr). The initialization function InitCursorCtrl() will first attempt to load the color cursors (crsr) with the IDs specified in the ‘acur’ handle passed to it. If the color cursors are not found or can not be loaded, it then attempts to load the monochrome cursors (CURS) with those same IDs. If these cursors can not be found or otherwise loaded, the static wrist watch cursor will be displayed. InitCursorCtrl() is normally called prior to SpinCursor() to define which cursors should be used. It can be called repeatedly to switch between one set of animated cursors and another. If SpinCursor() is called before a call to InitCursorCtrl(), the ‘acur’ resource with ID 128 will be loaded along with its cursors and set up as the current animated cursor list.

Unfortunately, the toolbox call that sets the cursor to a color cursor, SetCCursor(), can move or purge memory. It does so because the call uses the ‘crsr’ resource as a template and then creates pixmaps to represent the cursor on each monitor connected to the Macintosh. This is unfortunate because VBL tasks are not allowed to call Memory Manager routines. This is a serious potential problem with the VBL approach. Since the SetCCursor() call needs to set up these pixmaps only once, when color cursors are loaded via the InitCursorCtrl() call, the cursor is hidden and each cursor is then passed to SetCCursor() so that these pixmaps can be set up. This seems to eliminate the problem of calling Memory Manager routines from within a VBL task because the subsequent calls to SetCCursor() made by the VBL task no longer need to set up pixmaps since they have already been allocated. In any event, this is not a problem with monochrome cursors because SetCursor() does not move or purge memory.

How to Use These Routines

These routines are very simple to use. The module is set up through a call to InitCursorCtrl(). This routine is passed a handle to an ‘acur’ resource in memory and is typically called like this:

/* 3 */

InitCursorCtrl((acurHandle)GetResource(‘acur’,ID));

It can be passed the value NULL, and it will attempt to load and use the ‘acur’ resource with ID 128. This routine is automatically called by SpinCursor() with a NULL argument if the module has not been previously set up by an explicit call to InitCursorCtrl().

Cursor animation is accomplished through frequent calls to SpinCursor(). It is passed an integer value that specifies the number of seconds that the cursor should spin before it stops. You can make frequent calls to SpinCursor() with small values like one or two seconds throughout your routine, or you can preface your routine with a single call that may spin the cursor for 30 seconds or so. The maximum value allowable is 546 seconds or about nine minutes. A large value like this should never be used because the user would not know that the program has malfunctioned until nine minutes later. In general, values less than 60 seconds should be used. The duration values passed in SpinCursor() do not accumulate but simply set the duration counter, gSpinCycles, to the value passed. A call to SpinCursor() with a value of 10 seconds followed by another call to SpinCursor() with a value of two seconds will cause the cursor to spin for only two seconds after the second call.

The SpinCursor() routine will set up the VBL task if it is not already running. When you need to stop the cursor animation, make a call to StopCursor(). This call must be used even if the cursor has ‘timed out’ because the cursor remains set to the last ‘frame’ in the animated cursor list that it was set to when it ‘timed out’. StopCursor() removes the VBL task but does not reset the cursor to the standard arrow cursor. After the call to StopCursor() you are free to change the cursor or reset it through a call to InitCursor(). One could wait until the cursor has ‘timed out’ and then explicitly set the cursor to a new value, but this would still leave the ‘timed out’ VBL task in the VBL queue. While this is not harmful and since the VBL task will no longer try to change the cursor, this practice is workable but not encouraged. Without an explicit call to StopCursor() you can not be sure that the animation has terminated. StopCursor() should be called before the application is switched out under MultiFinder. Even though the VBL task won’t be called while the application is switched out, update events cause a minor switch to occur which causes the VBL task to run for a short period of time while you are still in the background. Likewise, if your application accepts background NULL events you must call StopCursor() when you receive the suspend event (major switch) because the application is switched back in temporarily (minor switch) during NULL events and the VBL task will cause the foreground application’s cursor to change. InitCursorCtrl() calls StopCursor() before it changes the ‘acur’ list and loads the new cursors. It may or may not reset the cursor to the standard arrow cursor depending on whether color cursors have been loaded.

The speed of the animation can be controlled through calls to SetCursorSpeed(). It takes as an argument the number of ticks that must elapse between ‘frames’ in the animation. A value of 60 will cause the cursor to change every second, while a value of four will cause it to change 15 times a second. Finally, QuitCursorCtrl() closes down the cursor control routines, disposes of cursors, and removes the VBL task if needed.

One note of caution is needed: DO NOT make any calls to SetCCursor() or SetCursor() unless the cursor animation has been stopped via a call to StopCursor(). Since the VBL task can be executed during your call, this would cause a re-entrant condition that neither SetCursor() or SetCCursor() handle very well. You will end up with cursor fragments littered all over your screen and possibly a system crash if this warning is not heeded.

The demo program Cursor Test is provided to illustrate the use of these routines. It has a ‘Cursors’ menu and a ‘Speed’ menu that allow you to control the cursor and rotation speed. The ‘File’ menu contains the the choices ‘Start Cursor’ and ‘Stop Cursor’ which start and stop the cursor animation. When you select ‘Start Cursor’ the cursor will rotate for five seconds by making the call SpinCursor(5). The menu will remain hilighted for six seconds to simulate some time consuming task. Since the cursor was instructed to rotate for five seconds, it will ‘time out’ one second before the simulated task completes. The ‘Stop Cursor’ menu item calls StopCursor() and then re-initializes the cursor to the standard arrow cursor.

Summary

The advantages of this technique are primarily ease of use for the programmer and high quality visual feedback for the end user. The routines presented work on QuickDraw machines and Color QuickDraw machines without any changes to the programming interface. All one needs to do is to define an ‘acur’ resource, and the corresponding monochrome (‘CURS’) and color (‘crsr’) cursor resources. The routines will take care of displaying the correct cursors. Within application code itself one only needs to call SpinCursor() on a casual basis and the rest is automatic. The end user sees high quality animated color cursors with smooth motion that will stop if anything goes wrong with the program. Unfortunately there is still the potential problem with the calls to SetCCursor() from within a VBL task; however, preloading the color cursors should resolve the problem.

Acknowledgements

Thanks to Brian Zuc for his help in getting me started with the VBL manager and Steve Fisher for his helpful comments on this article.

[Due to space limitations, only one animated cursor is listed. Those, and a number of others, are included on the source code disk for this issue.-ed]

Listing: Cursor Test Π

Cursor Test.c
CursorCtrl.c
MacTraps
SANE
Listing: CursorCtrl.c

/**********************************************************
Copyright (c) 1990 Richard Lesh, All Rights Reserved 
**********************************************************/

#include <MacHeaders>
#include <Color.h>
#include <VRetraceMgr.h>

/**********************************************************
Module: Cursor Control

This module provides support for the use of dynamic cursors
which give better feedback to the user that the program is
working properly.  This module has an interface similar to
the MPW version CursorCtrl.c but does not necessarily
function in an identical manner.  (MPW does not support
color cursors).  This version uses a VBL task to smooth out
the cursor animation.  It does not, hoever, allow the cursor
to continue spinning in the event of a system crash.  The
application must execute SpinCursor on a regular basis to
keep the cursor spinning.  RotateCursor is not supported in
this version.  InitCursorCtrl is called to initialize the
cursors or to change them to a new set of cursors.
StopCursor is called to halt the cursor animation so that
it can then be set to the arrow or other application defined
cursors.

Routines:
InitCursorCtrl() -- Initializes the module & loads cursors.
QuitCursorCtrl() -- Cleans up when animated cursors are no
                    longer needed.
SpinCursor()     -- Starts or continues cursor animation.
StopCursor()     -- Stops the cursor animation.
SetCursorSpeed() -- Sets the speed of cursor rotation.
**********************************************************/

/**********************************************************
Definitions
**********************************************************/
#ifndef NULL
#define NULL 0L
#endif

/**********************************************************
TypeDefs and Enums
**********************************************************/
/* Animated cursor control structure 'acur' */
typedef struct {
 short n;
 short index;
 union {
 Handle cursorHdl;
 short resID;
 }frame[1];
}acurRec,*acurPtr,**acurHandle;

/**********************************************************
Private Globals
**********************************************************/
/* Current 'acur' resource handle */
static acurHandle gCurrentHdl=NULL;
/* True if using color cursors */
static Boolean gColorCursor=FALSE;
/* VBL task record for the cursor */
static VBLTask *gCursorTask=NULL;
/* Number of cycles to spin cursor */
static short gSpinCycles=0;
/* Number of ticks between changes */
static short gSpeed=10;
/* TRUE after SpinCursor() and FALSE after StopCursor() */
static Boolean isCursorRunning=FALSE;

/**********************************************************
Prototypes
**********************************************************/
/* Functions to be exported */
void InitCursorCtrl(acurHandle h);
void QuitCursorCtrl(void);
void SpinCursor(short cycles);
void StopCursor(void);
void SetCursorSpeed(short newSpeed);

/* Functions to be used internally */
void DisposCursors(void);
Boolean GetMonoCursors(acurHandle h);
Boolean GetColorCursors(acurHandle h);
pascal void SpinCursorTask(void);

Boolean hasColorQD(void);
VBLTask *InstallVBLTask(ProcPtr proc,short ticks);
void RemoveVBLTask(VBLTask *taskPtr);

/**********************************************************
Routine:InitCursorCtrl(acurHandle resHdl)

This routine initializes the CursorCtrl module using the
'acur' resource indicated via the argument resHdl.  If
resHdl is NULL then the 'acur' resource with ID=128 is
loaded. If the machine has ColorQD, InitCursorCtrl first
attempts to load the color cursor 'crsr' resources with the
IDs indicated in the 'acur' resource.  If this fails or if
the machine does not have ColorQD, InitCursorCtrl attempts
to load the normal cursor 'CURS' resources with the IDs
indicated in the 'acur' resource.  If this action fails,
all subsequent calls to SpinCursor will simply set the
cursor to the watch cursor.

InitCursorCtrl should be called as follows:

InitCursorCtrl((acurHandle)GetResource('acur',200));

If GetResource is unable to find the 'acur' resource it
returns NULL which can be handled by InitCursorCtrl.
InitCursorCtrl will mark the resource as unpurgable and
will be responsible for releasing the all cursor storage
and the 'acur' resource when called again with a new 'acur'
handle.  If the new handle is the same as the current
handle, nothing will be done.  Since 'crsr' resources are
just templates for a color cursor, you should make sure
that all 'crsr' resources are marked purgable in the
resource file since GetCCursor does not release these
resources.
**********************************************************/

void InitCursorCtrl(acurHandle h)
{
 short i,j;
 Boolean useColorCursors;

 useColorCursors=hasColorQD();
 
 if (!h) h=(void *)GetResource('acur', 128);
 if (h && h!=gCurrentHdl){
 HNoPurge(h);
 MoveHHi(h);
 HLock(h);
 
/**********************************************************
Get new cursors.
**********************************************************/
 StopCursor();
 if (useColorCursors)
 useColorCursors=GetColorCursors(h);
 if (!useColorCursors && !GetMonoCursors(h)) return;

 DisposCursors();
 gCurrentHdl=h;
 gColorCursor=useColorCursors;
 (*h)->index=0;
 }
}

/**********************************************************
Routine:QuitCursorCtrl()

Shuts down the cursor control module.
**********************************************************/

void QuitCursorCtrl()
{
 DisposCursors();
 if (gCursorTask) RemoveVBLTask(gCursorTask);
 gCursorTask=NULL;
}

/**********************************************************
Routine:DisposCursors()

Disposes the cursors pointed to in the 'acur' structure.
**********************************************************/

void DisposCursors()
{
 register short i,j;
 
 StopCursor();
 if (gCurrentHdl){
 j=(*gCurrentHdl)->n;
 if (gColorCursor)
 for (i=0;i<j;i++)
 DisposCCursor((*gCurrentHdl)->frame[i].cursorHdl);
 else
 for (i=0;i<j;i++)
 DisposHandle((*gCurrentHdl)->frame[i].cursorHdl);
 ReleaseResource(gCurrentHdl);
 gCurrentHdl=NULL;
 }
}

/**********************************************************
Routine:GetMonoCursors(acurHandle h)

This is an internal routine that loads the normal cursors
('CURS') from the resource file. In the 'acur' resource,
the resource ID of the cursor is stored in the frame union.
When the cursor has been loaded, its handle is then stored
in the frame union.  The function returns TRUE if it was
successful at loading in all the cursors and FALSE if there
was an error.
**********************************************************/

static Boolean GetMonoCursors(acurHandle h)
{
 short i,j;
 CursHandle cursHdl;

 if (h){
 j=(*h)->n;
 for (i=0;i<j;i++){
 cursHdl=GetCursor((*h)->frame[i].resID);
 if (cursHdl==NULL){
 for (j=0;j<i;j++)
 DisposHandle((*h)->frame[j].cursorHdl);
 return(FALSE);
 }else{
 DetachResource(cursHdl);
 (*h)->frame[i].cursorHdl=(Handle)cursHdl;
 }
 }
 }
 return(TRUE);
}

/**********************************************************
Routine:GetColorCursors(acurHandle h)

This is an internal routine that loads the color cursors
('crsr') from the resource file. In the 'acur' resource,
the resource ID of the cursor is stored in the frame union.
When the cursor has been loaded, its handle is then stored
in the frame union.  The function returns TRUE if it was
successful at loading in all the cursors and FALSE if there
was an error.  The 'crsr' resources should be set purgable.
**********************************************************/

static Boolean GetColorCursors(acurHandle h)
{
 short i,j;
 CCrsrHandle cursHdl;
 Boolean result=TRUE;

 if (h){
 j=(*h)->n;
 HideCursor();
 for (i=0;i<j;i++){
 cursHdl=GetCCursor((*h)->frame[i].resID);
 if (cursHdl==NULL){
 for (j=0;j<i;j++)
 DisposCCursor((*h)->frame[j].cursorHdl);
 result=FALSE;
 break;
 }else{
 (*h)->frame[i].cursorHdl=(Handle)cursHdl;
 SetCCursor((*h)->frame[i].cursorHdl);
 }
 }
 InitCursor();
 }
 return(result);
}

/**********************************************************
Routine: SpinCursor(short seconds)

This routine sets gSpinCycles to seconds*60/gSpeed which is
the number of times that the cursor should spin before
stopping. The seconds parameter should therefore be set to
just slightly longer than the time estimated to execute the
code up to the next SpinCursor call.  When the gSpinCycles
value counts down to zero the cursor will no longer spin
and the user will thereby be notified that something is
awry. If there is no current 'acur' resource, this routine
will set the cursor the the watch cursor.
**********************************************************/

void SpinCursor(short seconds)
{
 static long counter=0;
 
 if (gCurrentHdl==0) InitCursorCtrl(NULL);
 if (gCurrentHdl){
 if (!gCursorTask)
 gCursorTask=InstallVBLTask((ProcPtr)SpinCursorTask,
 gSpeed);
 if (gCursorTask){
 if (gSpinCycles==0){
 if (gColorCursor) 
 SetCCursor((*gCurrentHdl)->frame[
 (*gCurrentHdl)->index].cursorHdl);
 else 
 SetCursor(*(*gCurrentHdl)->frame[
 (*gCurrentHdl)->index].cursorHdl);
 }
 gSpinCycles=seconds*60/gSpeed;
 }
 }else
 SetCursor(*GetCursor(watchCursor));
 isCursorRunning=TRUE;
}

/**********************************************************
Routine:SpinCursorTask()

This is the VBL task routine.  If the gSpinCycles global is
not zero it will decrement gSpinCycles and then advance the
cursor to the next one specified by the 'acur' resource in
gCurrentHdl.  This routine does call SetCCursor which can
call the memory manager.  The results could be severe if
the cursors had not be preloaded.  Since application VBL
tasks are only called when the application is in the
foreground, the global CurrentA5 used by SetCurrentA5()
will be the correct value.
**********************************************************/

static pascal void SpinCursorTask()
{
 long oldA5;

 oldA5=SetCurrentA5();
 gCursorTask->vblCount=gSpeed;
 if (gSpinCycles){
 gSpinCycles--;
 (*gCurrentHdl)->index++;
 (*gCurrentHdl)->index%=(*gCurrentHdl)->n;
 if (gColorCursor) 
 SetCCursor((*gCurrentHdl)->frame[(*gCurrentHdl)
 ->index].cursorHdl);
 else
 SetCursor(*(*gCurrentHdl)->frame[(*gCurrentHdl)
 ->index].cursorHdl);
 }
 SetA5(oldA5);
}

/**********************************************************
Routine:StopCursor()

This routine will stop the cursor animation by setting the
gSpinCycles global to zero.  This must be called before the
application changes the cursor to a normal cursor because
the VBL task will continue to set the cursor to the
animated cursors until the gSpinCycles count has run out.
**********************************************************/

void StopCursor()
{
 gSpinCycles=0;
 isCursorRunning=FALSE;
 if (gCursorTask) RemoveVBLTask(gCursorTask);
 gCursorTask=NULL;
}

/**********************************************************
Routine:SetCursorSpeed(short newSpeed)

This routine sets the number of ticks that must occur
before the cursor will change to the next one in the
animation sequence.
**********************************************************/

void SetCursorSpeed(short newSpeed)
{
 if (newSpeed>0 && newSpeed<=60)
 gSpeed=newSpeed;
}

/**********************************************************
Routine: Boolean hasColorQD()

Predicate that returns TRUE if the current machine supports
Color Quickdraw.
**********************************************************/

#define SysEnvironsVersion 2
Boolean hasColorQD()
{
 OSErr theErr;
 SysEnvRec theWorld;
 
 theErr=SysEnvirons(SysEnvironsVersion,&theWorld);

 if (theErr == 0 && theWorld.hasColorQD) return(TRUE);
 else return(FALSE);
}

/**********************************************************
Routine: VBLTask *InstallVBLTask(ProcPtr proc,short ticks)

This routine installs the VLBTask pointed to by the ProcPtr
which will begin execution after the number of ticks
specified.  A pointer to a VBL task record is returned.
NULL is returned if the task could not be set up.
**********************************************************/

VBLTask *InstallVBLTask(ProcPtr proc,short ticks)
{
 OSErr err;
 VBLTask *taskPtr;
 
 taskPtr=(VBLTask *)NewPtr(sizeof(VBLTask));
 if (taskPtr){
 taskPtr->qType=vType;
 taskPtr->vblAddr=proc;
 taskPtr->vblCount=ticks;
 taskPtr->vblPhase=0;
 err=VInstall((QElemPtr)taskPtr);
 if (err!=noErr){
 DisposPtr(taskPtr);
 taskPtr=NULL;
 }
 }
 return(taskPtr);
}

/**********************************************************
Routine: void RemoveVBLTask(VBLTask *taskPtr)

Removes the VBL task specified by taskPtr that was
installed using InstallVBLTask.  It also disposes of the
memory set up in that call.
**********************************************************/

void RemoveVBLTask(VBLTask *taskPtr)
{
 VRemove((QElemPtr)taskPtr);
 DisposPtr(taskPtr);
}
Listing: Cursor Test.c

/**********************************************************
Copyright (c) 1990 Richard Lesh, All Rights Reserved 
**********************************************************/

/**********************************************************
Program: Cursor Test

Demonstrates the use of animated cursors and exercises the
Cursor Control module.
**********************************************************/

#include <MacHeaders>
#include <SANE.h>

/**********************************************************
Definitions
**********************************************************/
#ifndef NULL
#define NULL 0L
#endif

#define ABOUT_DLOG 128

#define APPLE_MENU 128
#define FILE_MENU129
#define EDIT_MENU130
#define CURSOR_MENU131
#define SPEED_MENU 132

/**********************************************************
TypeDefs and Enums
**********************************************************/

/* Animated cursor control structure 'acur' */
typedef struct {
 short n;
 short index;
 union {
 Handle cursorHdl;
 short resID;
 }frame[1];
}acurRec,*acurPtr,**acurHandle;

/* Menu commands */
enum {mAbout=1};
enum {mStart=1,mStop,mQuit=4};
enum {mUndo=1,mCut=3,mCopy,mPaste,mClear};

/**********************************************************
Private Globals
**********************************************************/
static short gCursorNum;
static short gSpeedNum;

/**********************************************************
Prototypes
**********************************************************/
void main(void);
void InitMgrs(void);
void FillMenus(void);
void EventLoop(void);
void DoMenu(long command);
void DoOpen(void);
void DoClose(void);

void InitCursorCtrl(acurHandle h);
void QuitCursorCtrl(void);
void SpinCursor(short cycles);
void StopCursor(void);
void SetCursorSpeed(short newSpeed);

void Wait(short);

/**********************************************************
Routine: main()

Entry point for the application.
**********************************************************/

void main()
{
 short i;

 MaxApplZone();
 for (i=0;i<10;i++)
 MoreMasters();
 InitMgrs();
 FillMenus();
 DoOpen();
 EventLoop();
 DoClose();
}

/**********************************************************
Routine: InitMgrs()

This routine initializes all the appropriate managers.
**********************************************************/

void InitMgrs()
{
 InitGraf(&thePort);
 InitFonts();
 InitWindows();
 InitMenus();
 TEInit();
 InitDialogs(NULL);
 InitCursor();
 FlushEvents(everyEvent,0);
}

/**********************************************************
Routine: FillMenus()

Reads in the menu bar and menu resources, then builds the
dynamic menus.
**********************************************************/

void FillMenus()
{
 short i;
 Handle menuBarHdl;
 MenuHandle menuHdl;
 
 menuBarHdl=GetNewMBar(128);     /*Load the Menu Bar */
 SetMenuBar(menuBarHdl);         /*Load the MENUs */
 menuHdl=GetMHandle(APPLE_MENU); /*Build the Apple menu*/
 AddResMenu(menuHdl,'DRVR');
 menuHdl=GetMHandle(CURSOR_MENU);/*Build the Cursor menu*/
 AddResMenu(menuHdl,'acur');
 DrawMenuBar();
}

/**********************************************************
Routine: EventLoop()

Main event loop processing.
**********************************************************/

Boolean gDone=FALSE;
void EventLoop()
{
 EventRecord theEvent;
 WindowPtr theWindow;
 short partCode;
 
 while (!gDone){
 SystemTask();
 GetNextEvent(everyEvent,&theEvent);
 switch(theEvent.what){
 case nullEvent:
 break;
 case keyDown:
 case autoKey:
 if (theEvent.modifiers & cmdKey){
 DoMenu(MenuKey((short)(theEvent.message & charCodeMask)));
 HiliteMenu(0);
 }
 break;
 case updateEvt:
 break;
 case activateEvt:
 break;
 case mouseDown:
 switch (partCode=FindWindow(theEvent.where,&theWindow)){
 case inDesk: break;
 case inMenuBar:
 DoMenu(MenuSelect(theEvent.where));break;
 case inSysWindow:
 SystemClick(&theEvent,theWindow);break;
 default:
 break;
 }
 break;
 case app4Evt:
 if (!(theEvent.message&suspendResumeMessage)) StopCursor();
 break;
 }
 }
}

/**********************************************************
Routine: DoMenu(long command)

Handles menu selections.
**********************************************************/

void DoMenu(long command)
{
 short menu_id=HiWord(command);
 short item=LoWord(command);
 Str255 item_name;
 MenuHandle mHdl;
 DialogPtr dPtr;
 
 switch(menu_id){
 case APPLE_MENU:
 if (item==1){
 dPtr=GetNewDialog(ABOUT_DLOG, NULL, -1);
 while (ModalDialog(NULL, &item),item!=1);
 DisposDialog(dPtr);
 }else if (item>2){
 GetItem(GetMHandle(menu_id),item,item_name);
 OpenDeskAcc(item_name);
 }
 break;
 case EDIT_MENU:
 SystemEdit(item-1);break;
 case FILE_MENU:
 switch(item){
 case mStart:
 SpinCursor(5);
 Wait(6*60);
 break;
 case mStop:
 StopCursor();
 InitCursor();
 break;
 case mQuit:
 gDone=TRUE;
 break;
 }
 break;
 case CURSOR_MENU:
 mHdl=GetMHandle(menu_id);
 GetItem(mHdl,item,item_name);
 StopCursor();
 InitCursorCtrl((acurHandle)GetNamedResource('acur',
 item_name));
 CheckItem(mHdl, gCursorNum, FALSE);
 CheckItem(mHdl, item, TRUE);
 gCursorNum=item;
 break;
 case SPEED_MENU:
 mHdl=GetMHandle(menu_id);
 GetItem(mHdl,item,item_name);
 SetCursorSpeed((short)str2num((char *)item_name));
 CheckItem(mHdl, gSpeedNum, FALSE);
 CheckItem(mHdl, item, TRUE);
 gSpeedNum=item;
 break; 
 }
 HiliteMenu(0);
}

/**********************************************************
Routine: DoOpen()

Performs tasks that need to be done at startup.
**********************************************************/

void DoOpen()
{
 MenuHandle mHdl;
 Str255 s;
 
 gCursorNum=1; /* Select cursor #1 and update menu */
 mHdl=GetMenu(CURSOR_MENU);
 CheckItem(mHdl, gCursorNum, TRUE);
 GetItem(mHdl,gCursorNum,s);
 InitCursorCtrl((acurHandle)GetNamedResource('acur',s));

 gSpeedNum=5;  /* Select speed #5 and update menu  */
 mHdl=GetMenu(SPEED_MENU);
 CheckItem(mHdl, gSpeedNum, TRUE);
 GetItem(mHdl,gSpeedNum,s);
 SetCursorSpeed((short)str2num((char *)s));
}

/**********************************************************
Routine: DoClose()

Cleans up before shutting down the application.
**********************************************************/

void DoClose()
{
 QuitCursorCtrl();
}

/**********************************************************
Routine:Wait(short t)

Pauses execution for t sixtieths of a second.  Does not
allow background tasks to execute.
**********************************************************/

void Wait(short t)
{
 long s;
 
 s=TickCount()+t;
 while (TickCount()<s);
}
Listing: Cursor Test Π.r

resource 'MENU' (128) {
 128,
 textMenuProc,
 0x7FFFFFFD,
 enabled,
 apple,
 { /* array: 2 elements */
 /* [1] */
 "About Cursor Test", noIcon, noKey, noMark, plain,
 /* [2] */
 "-", noIcon, noKey, noMark, plain
 }
};

resource 'MENU' (129) {
 129,
 textMenuProc,
 0x7FFFFFFB,
 enabled,
 "File",
 { /* array: 4 elements */
 /* [1] */
 "Start Cursor", noIcon, "G", noMark, plain,
 /* [2] */
 "Stop Cursor", noIcon, ".", noMark, plain,
 /* [3] */
 "-", noIcon, noKey, noMark, plain,
 /* [4] */
 "Quit", noIcon, "Q", noMark, plain
 }
};

resource 'MENU' (130) {
 130,
 textMenuProc,
 0x7FFFFFFD,
 enabled,
 "Edit",
 { /* array: 6 elements */
 /* [1] */
 "Undo", noIcon, "Z", noMark, plain,
 /* [2] */
 "-", noIcon, noKey, noMark, plain,
 /* [3] */
 "Cut", noIcon, "X", noMark, plain,
 /* [4] */
 "Copy", noIcon, "C", noMark, plain,
 /* [5] */
 "Paste", noIcon, "V", noMark, plain,
 /* [6] */
 "Clear", noIcon, "B", noMark, plain
 }
};

resource 'MENU' (131) {
 131,
 textMenuProc,
 allEnabled,
 enabled,
 "Cursors",
 { /* array: 0 elements */
 }
};

resource 'MENU' (132) {
 132,
 textMenuProc,
 allEnabled,
 enabled,
 "Speed",
 { /* array: 10 elements */
 /* [1] */
 "1", noIcon, noKey, noMark, plain,
 /* [2] */
 "2", noIcon, noKey, noMark, plain,
 /* [3] */
 "4", noIcon, noKey, noMark, plain,
 /* [4] */
 "6", noIcon, noKey, noMark, plain,
 /* [5] */
 "8", noIcon, noKey, noMark, plain,
 /* [6] */
 "10", noIcon, noKey, noMark, plain,
 /* [7] */
 "15", noIcon, noKey, noMark, plain,
 /* [8] */
 "30", noIcon, noKey, noMark, plain,
 /* [9] */
 "45", noIcon, noKey, noMark, plain,
 /* [10] */
 "60", noIcon, noKey, noMark, plain
 }
};

resource 'MBAR' (128) {
 { /* array MenuArray: 5 elements */
 /* [1] */
 128,
 /* [2] */
 129,
 /* [3] */
 130,
 /* [4] */
 131,
 /* [5] */
 132
 }
};

data 'acur' (128, "Beach Ball") {
 $"00 04 00 00 00 80 00 00 00 81 00 00 00 82 00 00"
 $"00 83 00 00"
};

resource 'CURS' (128) {
 $"07 C0 1F F0 3F F8 5F F4 4F E4 87 C2 83 82 81 02"
 $"83 82 87 C2 4F E4 5F F4 3F F8 1F F0 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7}
};

resource 'CURS' (129) {
 $"07 C0 19 F0 21 F8 41 FC 41 FC 81 FE 81 FE FF FE"
 $"FF 02 FF 02 7F 04 7F 04 3F 08 1F 30 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7}
};

resource 'CURS' (130) {
 $"07 C0 18 30 20 08 70 1C 78 3C FC 7E FE FE FF FE"
 $"FE FE FC 7E 78 3C 70 1C 20 08 18 30 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7}
};

resource 'CURS' (131) {
 $"07 C0 1F 30 3F 08 7F 04 7F 04 FF 02 FF 02 FF FE"
 $"81 FE 81 FE 41 FC 41 FC 21 F8 19 F0 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7}
};

resource 'crsr' (128, purgeable) {
 colorCursor,
 $"07 C0 1F F0 3F F8 5F F4 4F E4 87 C2 83 82 81 02"
 $"83 82 87 C2 4F E4 5F F4 3F F8 1F F0 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7},
 4,
 {0, 0, 16, 16},
 0,
 unpacked,
 0,
 0x480000,
 0x480000,
 chunky,
 2,
 1,
 2,
 0,
 $"00 3F F0 00 03 D5 5F 00 0D 55 55 C0 31 55 55 30"
 $"30 55 54 30 C0 15 50 0C C0 05 40 0C C0 01 00 0C"
 $"C0 05 40 0C C0 15 50 0C 30 55 54 30 31 55 55 30"
 $"0D 55 55 C0 03 D5 5F 00 00 3F F0 00 00 00 00 00",
 0x0,
 0,
 { /* array ColorSpec: 3 elements */
 /* [1] */
 0, 65535, 65535, 65535,
 /* [2] */
 1, 56797, 0, 0,
 /* [3] */
 3, 0, 0, 0
 }
};

resource 'crsr' (129, purgeable) {
 colorCursor,
 $"07 C0 19 F0 21 F8 41 FC 41 FC 81 FE 81 FE FF FE"
 $"FF 02 FF 02 7F 04 7F 04 3F 08 1F 30 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7},
 4,
 {0, 0, 16, 16},
 0,
 unpacked,
 0,
 0x480000,
 0x480000,
 chunky,
 2,
 1,
 2,
 0,
 $"00 3F F0 00 03 C1 5F 00 0C 01 55 C0 30 01 55 70"
 $"30 01 55 70 C0 01 55 5C C0 01 55 5C D5 55 55 5C"
 $"D5 55 00 0C D5 55 00 0C 35 55 00 30 35 55 00 30"
 $"0D 55 00 C0 03 D5 0F 00 00 3F F0 00 00 00 00 00",
 0x0,
 0,
 { /* array ColorSpec: 3 elements */
 /* [1] */
 0, 65535, 65535, 65535,
 /* [2] */
 1, 56797, 0, 0,
 /* [3] */
 3, 0, 0, 0
 }
};

resource 'crsr' (130, purgeable) {
 colorCursor,
 $"07 C0 18 30 20 08 70 1C 78 3C FC 7E FE FE FF FE"
 $"FE FE FC 7E 78 3C 70 1C 20 08 18 30 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7},
 4,
 {0, 0, 16, 16},
 0,
 unpacked,
 0,
 0x480000,
 0x480000,
 chunky,
 2,
 1,
 2,
 0,
 $"00 3F F0 00 03 C0 0F 00 0C 00 00 C0 35 00 01 70"
 $"35 40 05 70 D5 50 15 5C D5 54 55 5C D5 55 55 5C"
 $"D5 54 55 5C D5 50 15 5C 35 40 05 70 35 00 01 70"
 $"0C 00 00 C0 03 C0 0F 00 00 3F F0 00 00 00 00 00",
 0x0,
 0,
 { /* array ColorSpec: 3 elements */
 /* [1] */
 0, 65535, 65535, 65535,
 /* [2] */
 1, 56797, 0, 0,
 /* [3] */
 3, 0, 0, 0
 }
};

resource 'crsr' (131, purgeable) {
 colorCursor,
 $"07 C0 1F 30 3F 08 7F 04 7F 04 FF 02 FF 02 FF FE"
 $"81 FE 81 FE 41 FC 41 FC 21 F8 19 F0 07 C0",
 $"07 C0 1F F0 3F F8 7F FC 7F FC FF FE FF FE FF FE"
 $"FF FE FF FE 7F FC 7F FC 3F F8 1F F0 07 C0",
 {7, 7},
 4,
 {0, 0, 16, 16},
 0,
 unpacked,
 0,
 0x480000,
 0x480000,
 chunky,
 2,
 1,
 2,
 0,
 $"00 3F F0 00 03 D5 0F 00 0D 55 00 C0 35 55 00 30"
 $"35 55 00 30 D5 55 00 0C D5 55 00 0C D5 55 55 5C"
 $"C0 01 55 5C C0 01 55 5C 30 01 55 70 30 01 55 70"
 $"0C 01 55 C0 03 C1 5F 00 00 3F F0 00 00 00 00 00",
 0x0,
 0,
 { /* array ColorSpec: 3 elements */
 /* [1] */
 0, 65535, 65535, 65535,
 /* [2] */
 1, 56797, 0, 0,
 /* [3] */
 3, 0, 0, 0
 }
};

resource 'DLOG' (128) {
 {40, 40, 140, 331},
 dBoxProc,
 visible,
 goAway,
 0x0,
 128,
 ""
};

resource 'DITL' (128) {
 { /* array DITLarray: 4 elements */
 /* [1] */
 {70, 220, 90, 278},
 Button {
 enabled,
 "OK"
 },
 /* [2] */
 {6, 106, 22, 184},
 StaticText {
 disabled,
 "Cursor Test"
 },
 /* [3] */
 {28, 93, 44, 198},
 StaticText {
 disabled,
 "by Richard Lesh"
 },
 /* [4] */
 {50, 52, 66, 238},
 StaticText {
 disabled,
 "©1990, All Rights Reserved"
 }
 }
};

 
AAPL
$116.47
Apple Inc.
+0.16
MSFT
$47.98
Microsoft Corpora
-0.72
GOOG
$537.50
Google Inc.
+2.67

MacTech Search:
Community Search:

Software Updates via MacUpdate

StatsBar 1.9 - Monitor system processes...
StatsBar gives you a comprehensive and detailed analysis of the following areas of your Mac: CPU usage Memory usage Disk usage Network and bandwidth usage Battery power and health (MacBooks only)... Read more
Cyberduck 4.6 - FTP and SFTP browser. (F...
Cyberduck is a robust FTP/FTP-TLS/SFTP browser for the Mac whose lack of visual clutter and cleverly intuitive features make it easy to use. Support for external editors and system technologies such... Read more
Maya 2015 - Professional 3D modeling and...
Maya is an award-winning software and powerful, integrated 3D modeling, animation, visual effects, and rendering solution. Because Maya is based on an open architecture, all your work can be scripted... Read more
Evernote 6.0.1 - Create searchable notes...
Evernote allows you to easily capture information in any environment using whatever device or platform you find most convenient, and makes this information accessible and searchable at anytime, from... Read more
calibre 2.11 - Complete e-library manage...
Calibre is a complete e-book library manager. Organize your collection, convert your books to multiple formats, and sync with all of your devices. Let Calibre be your multi-tasking digital... Read more
Herald 5.0.1 - Notification plugin for M...
Note: Versions 2.1.3 (for OS X 10.7), 3.0.6 (for OS X 10.8), and 4.0.8 (for OS X 10.9) are no longer supported by the developer. Herald is a notification plugin for Mail.app, Apple's Mac OS X email... Read more
Firetask 3.7 - Innovative task managemen...
Firetask uniquely combines the advantages of classical priority-and-due-date-based task management with GTD. Stay focused and on top of your commitments - Firetask's "Today" view shows all relevant... Read more
TechTool Pro 7.0.6 - Hard drive and syst...
TechTool Pro is now 7, and this is the most advanced version of the acclaimed Macintosh troubleshooting utility created in its 20-year history. Micromat has redeveloped TechTool Pro 7 to be fully 64... Read more
PhotoDesk 3.0.1 - Instagram client for p...
PhotoDesk lets you view, like, comment, and download Instagram pictures/videos! (NO Uploads! / Image Posting! Instagram forbids that! AND you *need* an *existing* Instagram account). But you can do... Read more
SuperDuper! 2.7.3 - Advanced disk clonin...
SuperDuper! is an advanced, yet easy to use disk copying program. It can, of course, make a straight copy, or "clone" -- useful when you want to move all your data from one machine to another, or do... Read more

Latest Forum Discussions

See All

Ubisoft Gives Everyone Two New Ways to E...
Ubisoft Gives Everyone Two New Ways to Earn In-Game Stuff for Far Cry 4 Posted by Jessica Fisher on November 21st, 2014 [ permalink ] | Read more »
Golfinity – Tips, Tricks, Strategies, an...
Dig this: Would you like to know what we thought of being an infinite golfer? Check out our Golfinity review! Golfinity offers unlimited ways to test your skills at golf. Here are a few ways to make sure your score doesn’t get too high and your... | Read more »
Dark Hearts, The Sequel to Haunting Meli...
Dark Hearts, The Sequel to Haunting Melissa, is Available Now Posted by Jessica Fisher on November 21st, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
Meowza! Toyze Brings Talking Tom to Life...
Meowza! | Read more »
Square Enix Announces New Tactical RPG f...
Square Enix Announces New Tactical RPG for Mobile, Heavenstrike Rivals. Posted by Jessica Fisher on November 21st, 2014 [ permalink ] With their epic stories and gorgeous graphics, | Read more »
Quest for Revenge (Games)
Quest for Revenge 1.0.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.0 (iTunes) Description: The great Kingdom of the west has fallen. The gods ignore the prayers of the desperate. A dark warlord has extinguished... | Read more »
Threadz is a New Writing Adventure for Y...
Threadz is a New Writing Adventure for You and Your Friends Posted by Jessica Fisher on November 21st, 2014 [ permalink ] In the tradition of round-robin storytelling, | Read more »
SteelSeries Stratus XL Hardware Review
Made by: SteelSeries Price: $59.99 Hardware/iOS Integration Rating: 4 out of 5 stars Usability Rating: 4.5 out of 5 stars Reuse Value Rating: 4.25 out of 5 stars Build Quality Rating: 4.5 out of 5 stars Overall Rating: 4.31 out of 5 stars | Read more »
ACDSee (Photography)
ACDSee 1.0.0 Device: iOS iPhone Category: Photography Price: $1.99, Version: 1.0.0 (iTunes) Description: Capture, perfect, and share your photos with ACDSee. The ACDSee iPhone app combines an innovative camera, a powerful photo... | Read more »
ProTube for YouTube (Entertainment)
ProTube for YouTube 2.0.2 Device: iOS Universal Category: Entertainment Price: $1.99, Version: 2.0.2 (iTunes) Description: ProTube is the ultimate, fully featured YouTube app. With it's highly polished design, ProTube offers ad-free... | Read more »

Price Scanner via MacPrices.net

Save up to $400 with Apple refurbished 2014 1...
The Apple Store has restocked Apple Certified Refurbished 2014 15″ Retina MacBook Pros for up to $400 off the cost of new models. An Apple one-year warranty is included with each model, and shipping... Read more
New 13-inch 1.4GHz MacBook Air on sale for $8...
 Adorama has the 2014 13″ 1.4GHz/128GB MacBook Air on sale for $899.99 including free shipping plus NY & NJ tax only. Their price is $100 off MSRP. B&H Photo has the 13″ 1.4GHz/128GB MacBook... Read more
Apple Expected to Reverse Nine-Month Tablet S...
Apple and Samsung combined accounted for 62 percent of the nearly 36 million branded tablets shipped in 3Q 2014, according to early vendor shipment share estimates from market intelligence firm ABI... Read more
Stratos: 30 Percent of US Smartphone Owners t...
Stratos, Inc., creator of the Bluetooth Connected Card Platform, has announced results from its 2014 Holiday Mobile Payments Survey. The consumer survey found that nearly one out of three (30 percent... Read more
2014 1.4GHz Mac mini on sale for $449, save $...
 B&H Photo has lowered their price on the new 1.4GHz Mac mini to $449.99 including free shipping plus NY tax only. Their price is $50 off MSRP, and it’s the lowest price available for this new... Read more
Check Apple prices on any device with the iTr...
MacPrices is proud to offer readers a free iOS app (iPhones, iPads, & iPod touch) and Android app (Google Play and Amazon App Store) called iTracx, which allows you to glance at today’s lowest... Read more
64GB iPod touch on sale for $249, save $50
Best Buy has the 64GB iPod touch on sale for $249 on their online store for a limited time. Their price is $50 off MSRP. Choose free shipping or free local store pickup (if available). Sale price for... Read more
15″ 2.2GHz Retina MacBook Pro on sale for $17...
 B&H Photo has the 2014 15″ 2.2GHz Retina MacBook Pro on sale for $1799.99 for a limited time. Shipping is free, and B&H charges NY sales tax only. B&H will also include free copies of... Read more
New Logitech AnyAngle Case/Stand Brings Flexi...
Logitec has announced the newest addition to its suite of tablet products — the Logitech AnyAngle. A protective case with an any-angle stand for iPad Air 2 and all iPad mini models, AnyAngle is the... Read more
Notebook PC Shipments Rise Year-Over-Year as...
According to preliminary results from the upcoming DisplaySearch Quarterly Mobile PC Shipment and Forecast Report, the global notebook PC market grew 10 percent year-over-year in Q3’14 to 49.4... Read more

Jobs Board

*Apple* Solutions Consultant (ASC)- Retail S...
**Job Summary** The ASC is an Apple employee who serves as an Apple brand ambassador and influencer in a Reseller's store. The ASC's role is to grow Apple Read more
Project Manager, *Apple* Financial Services...
**Job Summary** Apple Financial Services (AFS) offers consumers, businesses and educational institutions ways to finance Apple purchases. We work with national and Read more
*Apple* Store Leader Program - College Gradu...
Job Description: Job Summary As an Apple Store Leader Program agent, you can continue your education as you major in the art of leadership at the Apple Store. You'll Read more
*Apple* Retail - Multiple Positions (US) - A...
Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, you're also the Read more
Senior Event Manager, *Apple* Retail Market...
…This senior level position is responsible for leading and imagining the Apple Retail Team's global event strategy. Delivering an overarching brand story; in-store, Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.