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
$102.50
Apple Inc.
+0.25
MSFT
$45.43
Microsoft Corpora
+0.55
GOOG
$571.60
Google Inc.
+2.40

MacTech Search:
Community Search:

Software Updates via MacUpdate

VueScan 9.4.41 - Scanner software with a...
VueScan is a scanning program that works with most high-quality flatbed and film scanners to produce scans that have excellent color fidelity and color balance. VueScan is easy to use, and has... Read more
Cloud 3.0.0 - File sharing from your men...
Cloud is simple file sharing for the Mac. Drag a file from your Mac to the CloudApp icon in the menubar and we take care of the rest. A link to the file will automatically be copied to your clipboard... Read more
LibreOffice 4.3.1.2 - Free Open Source o...
LibreOffice is an office suite (word processor, spreadsheet, presentations, drawing tool) compatible with other major office suites. The Document Foundation is coordinating development and... Read more
SlingPlayer Plugin 3.3.20.505 - Browser...
SlingPlayer is the screen interface software that works hand-in-hand with the hardware inside the Slingbox to make your TV viewing experience just like that at home. It features an array of... Read more
Get Lyrical 3.8 - Auto-magically adds ly...
Get Lyrical auto-magically add lyrics to songs in iTunes. You can choose either a selection of tracks, or the current track. Or turn on "Active Tagging" to get lyrics for songs as you play them.... Read more
Viber 4.2.2 - Send messages and make cal...
Viber lets you send free messages and make free calls to other Viber users, on any device and network, in any country! Viber syncs your contacts, messages and call history with your mobile device,... Read more
Cocktail 7.6 - General maintenance and o...
Cocktail is a general purpose utility for OS X that lets you clean, repair and optimize your Mac. It is a powerful digital toolset that helps hundreds of thousands of Mac users around the world get... Read more
LaunchBar 6.1 - Powerful file/URL/email...
LaunchBar is an award-winning productivity utility that offers an amazingly intuitive and efficient way to search and access any kind of information stored on your computer or on the Web. It provides... 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
BBEdit 10.5.12 - Powerful text and HTML...
BBEdit is the leading professional HTML and text editor for the Mac. Specifically crafted in response to the needs of Web authors and software developers, this award-winning product provides a... Read more

Latest Forum Discussions

See All

This Week at 148Apps: August 25-29, 2014
Shiny Happy App Reviews   | Read more »
Qube Kingdom – Tips, Tricks, Strategies,...
Qube Kingdom is a tower defense game from DeNA. You rally your troops – magicians, archers, knights, barbarians, and others – and fight against an evil menace looking to dominate your kingdom of tiny squares. Planning a war isn’t easy, so here are a... | Read more »
Qube Kingdom Review
Qube Kingdom Review By Nadia Oxford on August 29th, 2014 Our Rating: :: KIND OF A SQUARE KINGDOMUniversal App - Designed for iPhone and iPad Qube Kingdom has cute visuals, but it’s a pretty basic tower defense game at heart.   | Read more »
Fire in the Hole Review
Fire in the Hole Review By Rob Thomas on August 29th, 2014 Our Rating: :: WALK THE PLANKUniversal App - Designed for iPhone and iPad Seafoam’s Fire in the Hole looks like a bright, 8-bit throwback, but there’s not enough booty to... | Read more »
Alien Creeps TD is Now Available Worldwi...
Alien Creeps TD is Now Available Worldwide Posted by Ellis Spice on August 29th, 2014 [ permalink ] Universal App - Designed for iPhone and iPad | Read more »
Dodo Master Review
Dodo Master Review By Jordan Minor on August 29th, 2014 Our Rating: :: NEST EGGiPad Only App - Designed for the iPad Dodo Master is tough but fair, and that’s what makes it a joy to play.   | Read more »
Motorsport Manager Review
Motorsport Manager Review By Lee Hamlet on August 29th, 2014 Our Rating: :: MARVELOUS MANAGEMENTUniversal App - Designed for iPhone and iPad Despite its depth and sense of tactical freedom, Motorsport Manager is one of the most... | Read more »
Motorsport Manager – Beginner Tips, Tric...
The world of Motorsport management can be an unforgiving and merciless one, so to help with some of the stress that comes with running a successful race team, here are a few hints and tips to leave your opponents in the dust. | Read more »
CalPal Update Brings the App to 2.0, Add...
CalPal Update Brings the App to 2.0, Adds Lots of New Stuff Posted by Ellis Spice on August 29th, 2014 [ permalink ] | Read more »
Baseball Battle Review
Baseball Battle Review By Jennifer Allen on August 29th, 2014 Our Rating: :: SIMPLE HITTINGUniversal App - Designed for iPhone and iPad Simple and cute, Baseball Battle is a fairly fun baseball game for those looking for something... | Read more »

Price Scanner via MacPrices.net

Labor Day Weekend MacBook Pro sale; 15-inch m...
B&H Photo has the new 2014 15″ Retina MacBook Pros on sale for up to $125 off MSRP. Shipping is free, and B&H charges NY sales tax only. They’ll also include free copies of Parallels Desktop... Read more
Labor Day Weekend iPad mini sale; $50 to $100...
Best Buy has the iPad mini with Retina Display (WiFi models) on sale for $50 off MSRP on their online store for Labor Day Weekend. Choose free shipping or free local store pick up. Price is for... Read more
13-inch 1.4GHz MacBook Air on sale for $899,...
Adorama has the new 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. Read more
It’s Official: Apple Issues Invitations To Se...
Apple has issued one of its characteristically cryptic press invitations for a special event to be held at the Flint Center for the Performing Arts in hometown Cupertino on Sept. 9, 2014 at 10:00 am... Read more
Tablet Shipments To See First On-year Decline...
TrendForce analyst Caroline Chen notes that when the iPad launched in 2010, it was an instant hit and spurred a tablet PC revolution, with tablets so popular that that notebook PC sales stagnated and... Read more
SOBERLINK Releases Apple iOS Compatible Handh...
Cypress, California based SOBERLINK, Inc., creator of the first handheld Breathalyzer designed to improve recovery outcomes, continues to show prominence in the mobile alcohol monitoring space with... Read more
New 21″ 1.4GHz iMac on sale again for $999, s...
Best Buy has the new 21″ 1.4GHz iMac on sale for $999.99 on their online store. Their price is $100 off MSRP. Choose free shipping or free local store pick up. Price is for online orders only, in-... Read more
Smartphone Outlook Remains Strong for 2014, U...
According to a new mobile phone forecast from the International Data Corporation (IDC) Worldwide Quarterly Mobile Phone Tracker, more than 1.25 billion smartphones will be shipped worldwide in 2014,... Read more
Save up to $60 with Apple refurbished iPod to...
The Apple Store has Apple Certified Refurbished 5th generation iPod touches available starting at $149. Apple’s one-year warranty is included with each model, and shipping is free. Many, but not all... Read more
12-Inch MacBook Air Coming in 4Q14 or 2015 –...
Digitimes’ Aaron Lee and Joseph Tsai report that according to Taiwan-based upstream supply chain insiders, Apple plans to launch a thinner MacBook model either at year end 2014 or in 2015, and that... Read more

Jobs Board

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