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

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

NetNewsWire 5.0.4 - RSS and Atom news re...
NetNewsWire is the best way to keep up with the sites and authors you read most regularly. Let NetNewsWire pull down the latest articles, and read them in a distraction-free and Mac-like way. Native... Read more
EarthDesk 7.4.5 - $24.99
EarthDesk replaces your static desktop picture with a rendered image of Earth showing correct sun, moon, and city illumination. With an Internet connection, EarthDesk displays near-real-time global... Read more
BetterTouchTool 3.401 - Customize multi-...
BetterTouchTool adds many new, fully customizable gestures to the Magic Mouse, Multi-Touch MacBook trackpad, and Magic Trackpad. These gestures are customizable: Magic Mouse: Pinch in / out (zoom)... Read more
Vienna 3.5.6 :e12c952d: - RSS and Atom n...
Vienna is a freeware and Open-Source RSS/Atom newsreader with article storage and management via a SQLite database, written in Objective-C and Cocoa, for the OS X operating system. It provides... Read more
WhatsApp 2.2031.5 - Desktop client for W...
WhatsApp is the desktop client for WhatsApp Messenger, a cross-platform mobile messaging app which allows you to exchange messages without having to pay for SMS. WhatsApp Messenger is available for... Read more
Day One 4.16 - Maintain a daily journal.
Day One is an easy, great-looking way to use a journal / diary / text-logging application. Day One is well designed and extremely focused to encourage you to write more through quick Menu Bar entry,... Read more
VMware Fusion 11.5.6 - Run Windows apps...
VMware Fusion and Fusion Pro - virtualization software for running Windows, Linux, and other systems on a Mac without rebooting. The latest version includes full support for Windows 10, macOS Mojave... Read more
Alfred 4.1 - Quick launcher for apps and...
Alfred is an award-winning productivity application for OS X. Alfred saves you time when you search for files online or on your Mac. Be more productive with hotkeys, keywords, and file actions at... Read more
Dashlane 6.2032.0 - Password manager and...
Dashlane is an award-winning service that revolutionizes the online experience by replacing the drudgery of everyday transactional processes with convenient, automated simplicity - in other words,... Read more
Skype 8.63.0.76 - Voice-over-internet ph...
Skype is a telecommunications app that provides HD video calls, instant messaging, calling to any phone number or landline, and Skype for Business for productive cooperation on the projects. This... Read more

Latest Forum Discussions

See All

Motorball is a car football game from No...
A few years back Noodlecake Studios announced that they would be dipping in the multiplayer gaming realm with two different games. The first of those, Golf Blitz, released a while back and has proven to be very popular. Now, the second has arrived... | Read more »
SINoALICE's latest update introduce...
SINoALICE's latest update has now arrived, adding several fan-favourite characters from popular RPG series NieR. Young Nier, Kaine, and Young Emil are available in-game as part of a limited-time crossover event set to run until August 20th. [Read... | Read more »
Rocat Jumpurr is an intense roguelite pl...
Rocat Jumpurr is a roguelite platformer from developer Mousetrap Games. You might already be familiar with it if you follow the Big Indie Pitch, where it won first place during this year's Pocket Gamer Connects London competition. Following its... | Read more »
PUBG Mobile's Play As One campaign...
Back in mid-July, we reported that PUGB Mobile had teamed up with Direct Relief to help raise money for the charity's COVID-19 response project. It focused on an in-game running challenge for players, which lead to the PUBG Mobile donating $2... | Read more »
Marvel Contest of Champions' latest...
Marvel Contest of Champions' latest motion comic has arrived, and it shows off new fighters Air-Walker and Dragon Man. Both characters are set to arrive in-game this month. [Read more] | Read more »
Clash Royale: The Road to Legendary Aren...
Supercell recently celebrated its 10th anniversary and their best title, Clash Royale, is as good as it's ever been. Even for lapsed players, returning to the game is as easy as can be. If you want to join us in picking the game back up, we've put... | Read more »
Global Spy is an intriguing 2D spy sim f...
Developer Yuyosoft Innovations' Global Spy launched last month for iOS and Android, though if you missed it at the time, we're here to tell you why it's well worth a go. This one's all about international espionage, tracking down elusive spies,... | Read more »
Distract Yourself With These Great Mobil...
There’s a lot going on right now, and I don’t really feel like trying to write some kind of pithy intro for it. All I’ll say is lots of people have been coming together and helping each other in small ways, and I’m choosing to focus on that as I... | Read more »
Hyena Squad is sci-fi turn-based strateg...
Wave Light Games has just revealed its latest release, Hyena Squad, a turn-based RPG set in a space station infested by gross aliens and the living dead. The announcement was first reported on by Touch Arcade. [Read more] | Read more »
Idle Guardians: Never Die is a pixel art...
SuperPlanet has been fairly prolific with game releases so far this year with both Evil Hunter Tycoon and Lucid Adventure releasing earlier this year. Now, they've released another idle RPG called Idle Guardians: Never Die, which you can download... | Read more »

Price Scanner via MacPrices.net

Woot offers numerous 2018-2020 MacBook Pros a...
Amazon-owned Woot has many open-box return MacBook Airs and MacBook Pros available today at prices starting at $879. Shipping is free for Prime members. Here’s what they have as of this post, and... Read more
Apple restocks refurbished 2020 13″ MacBook A...
Apple has restocked Certified Refurbished 2020 13″ MacBook Airs starting at only $849 and up to $200 off the cost of new Airs. Each MacBook features a new outer case, comes with a standard Apple one-... Read more
Apple restocks clearance 2019 13″ 2.4GHz MacB...
Apple has restocked Certified Refurbished 2019 13″ 2.4GHz 4-Core Touch Bar MacBook Pros starting at $1359 and up to $560 off original MSRP. Apple’s one-year warranty is included, shipping is free,... Read more
Apple restocks refurbished iPhone XR models s...
Apple has restocked Certified Refurbished, unlocked, iPhone XR models in the refurbished section of their online store starting at $539. Each iPhone comes with Apple’s standard one-year warranty,... Read more
Price drops! $100-$200 off clearance 27″ 5K i...
B&H Photo has dropped prices on clearance, previous-generation 27″ 5K iMacs by up to $200 off Apple’s original MSRP: – 27″ 3.0GHz 6-Core 5K iMac: $1699 $100 off original MSRP – 27″ 3.1GHz 6-Core... Read more
Woot offers Apple Watch and iPhone models fro...
Amazon-owned Woot has refurbished Apple Watch and iPhone models available from $99-$749 through August 6th. According to Woot, the items may show some wear, but they have all been fully tested and... Read more
Apple’s Phil Schiller Steps Down As SVP OF Wo...
NEWS: 08.05.20 – Former Apple senior Vice President of worldwide marketing, Phil Schiller, is stepping down from his long time role at the company in order to focus on spending more time with family... Read more
Expercom offers $320 discount on the 6-core 1...
Apple reseller Expercom has the Silver 16″ 6-core MacBook Pro on sale for a limited time for $2079 shipped. Their price is $320 off Apple’s MSRP for this model, and it’s the cheapest price currently... Read more
Apple announces Education pricing for new 202...
Purchase a new 2020 iMac or iMac Pro at Apple using Apple’s Education discount, and take up to $400 off MSRP. All teachers, students, and staff of any educational institution with a .edu email... Read more
Apple reseller Expercom offers $256 discount...
Expercom has Apple’s new 2020 10-core iMac Pro available for order and on sale for $4743 shipped. Their price is $256 off Apple’s MSRP for this new model, and it’s the cheapest price we’ve seen so... Read more

Jobs Board

Cub Foods - *Apple* Valley - Now Hiring Par...
Cub Foods - Apple Valley - Now Hiring Part Time! United States of America, Minnesota, Apple Valley New Retail Post Date 3 days ago Requisition # 122305 Sign Up Read more
Executive Team Leader GM Sales (Assistant Man...
…(Assistant Manager General Merchandise and Operations) - Apple Valley, CaliforniaApply NowJob ID:R0000082364job family:Store Managementschedule:Full Read more
Cub Foods - *Apple* Valley - Now Hiring Par...
Cub Foods - Apple Valley - Now Hiring Part Time! United States of America, Minnesota, Apple Valley New Retail Post Date 2 days ago Requisition # 122305 Sign Up Read more
Part-time Geek Squad *Apple* Consultation P...
**770829BR** **Job Title:** Part-time Geek Squad Apple Consultation Professional-Store 384(Ithaca) **Job Category:** Store Associates **Store Number or Department:** Read more
Product Manager, *Apple* Commercial Sales -...
Product Manager, Apple Commercial Sales Austin, TX, US Requisition Number:77652 As an Apple Product Manager for the Commercial Sales team at Insight, you Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.