TweetFollow Us on Twitter

Aug 01 QT Toolkit

Volume Number: 17 (2001)
Issue Number: 08
Column Tag: QuickTime Toolkit

The Skin Game

by Tim Monroe

Working with QuickTime Skins

Introduction

QuickTime 5 introduced support for displaying movies inside of arbitrarily shaped windows. These windows are called skinned movie windows, and the custom shape of one of those windows is called its skin. Up to now, our sample applications have always displayed QuickTime movies inside a standard document window, which occupies a rectangular area on the screen. Even QuickTime Player, which uses a snazzy brushed-metal window frame with rounded edges, always shows a movie inside a rectangular pane inside the frame. Skins give us a way to break out of this rectangular mold. For instance, Figure 1 shows a QuickTime movie with a skin that's shaped like the QuickTime logo.


Figure 1: A QuickTime movie with a skin

This movie contains two video tracks, one for the grainy, grayscale video showing in the center of the logo, and one for the logo image itself. (The second video track contains a single sample that extends for the entire duration of the first track.) The user can start and stop the movie by pressing the spacebar or by clicking in the visible portion of the grayscale video. And the user can move the window around on the screen by clicking anywhere on the blue logo and dragging.

Figure 2 shows another possibility. Here is our penguin sprite movie once again, but this time as a skinned movie. It's still got a tween track that changes the sprite's graphics mode from total transparency to total opacity. But now I've set the looping mode to palindrome looping so that the penguin fades in and out as the movie plays.


Figure 2: Another QuickTime movie with a skin

Figure 3 shows yet another skinned movie window. Most of what you see here, including all the buttons and draggable handles, is provided by a Flash track. The grayscale image is once again a frame of a video track, which we can start, stop, pause, and play in slow motion using the tools palette on the right side of the movie window.


Figure 3: Yet another QuickTime movie with a skin

In this article, we're going to learn how to create skinned movies. More importantly, we're going to learn how to open a skinned movie file and display the movie to the user in a window of the appropriate shape. That is to say, we're going to learn how to make our applications skin savvy. Currently there are very few applications that can open skinned movies. The only widely-available application that can do this, to my knowledge, is QuickTime Player.

Our sample application in this article, which can both create skinned movies and play them back, is called QTSkins. The Test menu of QTSkins is shown in Figure 4; as you can see, it has only one menu item, which allows us to add a skin track to a movie.


Figure 4: The Test menu of QTSkins

Skins

Perhaps the best way to think of QuickTime skins is like this: a skinned movie is just a QuickTime movie with a custom window shape. A skin provides a way of selecting some portion of an existing movie and having that portion be all that's displayed to the user when the movie is opened. Skinned movies don't have title bars or window frames, and they don't display a controller bar. As a result, if we want the user to be able to interact with the movie, we'll need to supply our own controls. We can use wired sprite tracks or Flash tracks for this, or perhaps even wired text tracks (which we encountered in the previous article).

The data that defines the custom window shape is contained in the skinned movie file itself. This fact has some very important consequences. For one thing, it means that we can select on a per-movie basis whether a movie is displayed in a normal document window or in a custom-shaped skinned window. We're not modifying the general appearance of the playback application (which is perhaps the typical use of the term ‘skins'). Rather, we're modifying the specific appearance of what's being played back. In a nutshell, we're changing the movie, not the movie player. Previously, the movie data represented some content that plays back inside a document window or pane, usually under the supervision of a movie controller and controller bar. Now the movie data can represent the content and the window and the controller. For the first time, really, the movie author has complete control over the user's playback experience.

So what kind of data do we use to construct a skinned movie? The first thing we need is some way of specifying which portion of the movie rectangle we want to appear as the content region of the skinned movie window. The content region of a window is the portion of the window in which an application displays the contents of a document; in our case, it's where the movie data and any movie controls are displayed. We specify the skinned movie's content region by providing a 1-bit (that is, black and white) mask that's the same size as the movie rectangle. If a pixel in the mask is black, then the corresponding pixel in the movie rectangle is displayed; otherwise, the corresponding pixel is not displayed. Let's call this mask the content region mask. Figure 5 shows the content region mask for the skinned movie shown in Figure 1.


Figure 5: A content region mask

We also need some way to move the skinned movie window around on the screen. Typically, of course, we move a window by grabbing its title bar or window frame and then dragging. Because skinned movie windows don't have title bars or frames, however, we need to explicitly indicate the portion of the skinned movie window that the user can grab and drag. We do this by specifying a second mask, the drag region mask. (This is also a 1-bit mask.) A user can click anywhere in this region and drag the window around. Figure 6 shows the drag region mask for the skinned movie shown in Figure 1.


Figure 6: A drag region mask

You'll notice that the drag region mask is entirely contained within the content region mask, so that the user can grab only in some visible portion of the movie window. In addition, the drag region mask should exclude any areas of the movie rectangle that you want to be interactive. It won't do any good, for instance, to have a skinned movie's drag region overlap any wired sprites, since a click in that area will be interpreted as the beginning of a drag operation.

So we need three ingredients to create a skinned movie. We need the movie data itself. We need a content region mask, to indicate the portion of the movie rectangle that is displayed to the user. And we need a drag region mask to indicate the portion of the movie rectangle that can be grabbed.

Creating Skinned Movies

The typical way to create a skinned movie is to add a skin track to an existing movie. The skin track contains data that specifies the content region and the drag region of the movie window. In this section, we'll investigate two different ways to add a skin track to a movie. First, though, we'll take a brief moment to learn about media characteristics. This will help us see that skin data can in fact be contained in other kinds of tracks as well.

Searching Media Characteristics

Let's begin by considering a utility function we'll call several times in our application, QTSkin_IsSkinnedMovie (defined in Listing 1). This function returns a Boolean value that indicates whether the specified movie contains skin data.

Listing 1: Determining whether a movie is a skinned movie

Boolean QTSkin_IsSkinnedMovie (Movie theMovie) 
{
   return(GetMovieIndTrackType(theMovie, 1, 
               FOUR_CHAR_CODE(‘skin'), movieTrackCharacteristic) 
               != NULL);
}

We've worked with GetMovieIndTrackType a handful of times previously, but only using the movieTrackMediaType flag as the last parameter, to search for a track of a given index that has a specific type. Here, you'll notice, we use the movieTrackCharacteristic flag instead, which tells GetMovieIndTrackType to search for a track of a given index that has a specific media characteristic. A media characteristic is a feature that can be shared by two or more track types, such as the ability to draw data. Originally, in QuickTime version 2.0, there were two supported media characteristics, indicating whether the track has video or audio data in it:

enum {
   VisualMediaCharacteristic            = FOUR_CHAR_CODE(‘eyes'),
   AudioMediaCharacteristic             = FOUR_CHAR_CODE(‘ears')
};

Any track that displays visible data to the user has the VisualMediaCharacteristic media characteristic; some examples are video tracks, sprite tracks, text tracks, MPEG tracks, and timecode tracks. Similarly, any track that plays audible data to the user has the AudioMediaCharacteristic media characteristic; some examples are sound tracks and music tracks. QuickTime has subsequently added a few other searchable media characteristics, including kCharacteristicProvidesActions for tracks that contain wired actions.

In Listing 1, we're looking to see whether any track in the movie contains skin data. (There is as yet no publicly-defined constant for the skin media characteristic, so we've hard-coded the value FOUR_CHAR_CODE(‘skin').) Skin tracks certainly contain skin data, so they have this characteristic. But other kinds of tracks may very well contain skin data, and so they too would have this characteristic. (If we are interested in knowing whether a specific track has a given characteristic, we can call the MediaHasCharacteristic function.) By searching for the skin media characteristic instead of the skin media type, we allow our applications to work with any movie tracks that contain skin data. Right now, to be sure, there are no track types with that characteristic aside from skin tracks; but we are equipped to deal with them when they come along.

Using the QuickTime XML Importer

By far the easiest way to create a movie with a skin track is to use a QuickTime XML importer, introduced in QuickTime 5. XML (for Extensible Markup Language) is a textual description of a document that contains structured information. It's similar in flavor to HTML, but differs significantly in that XML does not have a predefined set of markup tags. Rather, XML is more of a metalanguage for describing structured information. A QuickTime XML importer is a movie importer that knows how to parse certain kinds of XML files. QuickTime provides an importer that knows how to parse XML files that contain tags describing a skinned movie. Listing 2 shows the file used to construct the skinned movie shown in Figure 1. As you can see, this XML file specifies three other files, which contain the original movie data, a mask for the content region of the window, and a mask for the drag region of the window.

Listing 2: An XML file that specifies a skinned movie

<?xml version=”1.0”?>
<?quicktime type=”application/x-qtskin”?>
<skin>
   <movie src=”QTLogo.mov”/>
   <contentregion src=”contentmask.pct”/>
   <dragregion src=”dragmask.pct”/>
</skin>

If we open this file using QuickTime Player or any other skin-savvy application, we'll see the skinned movie shown in Figure 1. The application probably calls NewMovieFromFile or NewMovieFromDataRef to open the XML file. QuickTime will see that the file doesn't contain a movie atom and then go looking for a suitable movie importer. (See "In and Out" in MacTech, May 2000, for a more in-depth discussion of how this works.) In the present case, QuickTime will invoke the XML importer to import the movie data and return a movie to the calling application. Note that some importers, including the QuickTime XML importer, seem to ignore the newMovieActive flag passed to NewMovieFromFile. So we'll add the following line of code to the QTFrame_OpenMovieInWindow function, after we call NewMovieFromFile:

SetMovieActive(myMovie, true);

We can create a self-contained skinned movie file by calling FlattenMovieData on the open skinned movie. Our sample applications make this call when the user selects the "Save As..." menu item. The self-contained movie file is easier to move around and to transport from machine to machine. It's also preferable for web-based movie delivery.

Creating Skin Tracks Programmatically

Using the XML importer is fine and dandy, but we'd also like to be able to create skinned movies directly, using the QuickTime APIs. Once again, we'll do this by adding a skin track to an existing movie. We've created many kinds of tracks in QuickTime movies, so we've got the drill down. You'll recall that it goes basically like this:

  • Create a new track and media (NewMovieTrack and NewTrackMedia).
  • Create a new sample description (NewHandle).
  • Start a media-editing session (BeginMediaEdits).
  • Add media data to the new media (AddMediaSample).
  • End the media-editing session (EndMediaEdits).
  • Insert the new media data into the track (InsertMediaIntoTrack).

It turns out, however, that we need to use a slightly different method for constructing a skin track. When we build (for instance) a video track or a sprite track, we need to know the exact structure of the media sample data, and we need to fill out a sample description that describes that data (its size, its compression type, and so forth). Moreover, when we call AddMediaSample, we need to specify the duration of the media sample. But with skin media data, the notion of duration doesn't really apply. After all, we're just specifying a couple a masks for a window shape, not any time-based data.

To simplify our handling of media data that isn't time based, QuickTime 5 introduced public media information, which can be any data associated with a media that does not need to be pegged to a specific time in a track. Currently, to my knowledge, only the skin media handler supports public media information, to maintain the content and drag region masks.

QuickTime 5 includes two new functions for working with public media information, MediaSetPublicInfo and MediaGetPublicInfo. MediaSetPublicInfo is declared essentially like this:

ComponentResult MediaSetPublicInfo(MediaHandler mh,
         OSType infoSelector, void *infoDataPtr, Size dataSize);

The mh parameter specifies the media handler we're giving the information to; in the present case, 
it's the skin media handler. The infoSelector parameter specifies the kind of public information 
we're setting. The skin media handler currently understands two selectors, ‘skcr' 
(for the content region mask) and ‘skdr' (for the drag region mask). The parameters infoDataPtr 
and dataSize specify the memory location and size of the public media information data. With the skin 
media handler, however, dataSize should be 0 and infoDataPtr should be a picture handle (of type 
PicHandle). For instance, here's how we'll set the content region mask:

myErr = MediaSetPublicInfo(myHandler, FOUR_CHAR_CODE(‘skcr'), 
                        (void *)myContentPic, 0);

Our work really boils down to this: have the user select two pictures, one for the content region mask and another for the drag region mask; then create a new track and media (of type ‘skin'), call MediaSetPublicInfo for each of the pictures selected by the user, and finish up by calling InsertMediaIntoTrack. Once we've got the two picture handles, the 6-step sequence listed above reduces to this:

  • Create a new track and media (NewMovieTrack and NewTrackMedia).
  • Add media data to the new media (MediaSetPublicInfo).
  • Insert the new media data into the track (InsertMediaIntoTrack).

Let's consider, then, how to get the two picture handles. Ideally, we'd like to allow the user to work with any kind of image file that QuickTime can open (just like the XML importer does). MediaSetPublicInfo expects the data we pass it to be a PicHandle, so we need some way to convert the image data in a file selected by the user into a PicHandle. Happily, there is a graphics importer function, GraphicsImportGetAsPicture, that does precisely this. Listing 3 defines the QTSkin_GetPicHandleFromFile function, which we use (in Listing 4) to prompt the user for the two images we need. (For more information about graphics importers, see "Quick on the Draw" in MacTech, April 2000.)

Listing 3: Getting a picture handle from an image file

PicHandle QTSkin_GetPicHandleFromFile (void)
{
   OSType                            myTypeList = 
                                                  kQTFileTypeQuickTimeImage;
   short                             myNumTypes = 1;
   FSSpec                            myPictSpec;
   QTFrameFileFilterUPP             myFilterUPP = NULL;
   GraphicsImportComponent           myImporter = NULL;
   PicHandle                          myPicture = NULL;
   OSErr                                  myErr = noErr;

#if TARGET_OS_MAC
   myNumTypes = 0;
#endif

   // have the user select an image file
   myFilterUPP = QTFrame_GetFileFilterUPP(
                  (ProcPtr)QTSkin_FileFilterFunction);

   myErr = QTFrame_GetOneFileWithPreview(myNumTypes, 
                  (QTFrameTypeListPtr)&myTypeList, &myPictSpec, 
                  myFilterUPP);
   if (myErr != noErr)
      goto bail;

   // get a graphics importer for the image file
   myErr = GetGraphicsImporterForFile(&myPictSpec, 
                           &myImporter);
   if (myErr != noErr)
      goto bail;

   // convert the image into a PicHandle
   myErr = GraphicsImportGetAsPicture(myImporter, &myPicture);
   
bail:
   if (myFilterUPP != NULL)
      DisposeNavObjectFilterUPP(myFilterUPP);

   if (myImporter != NULL)
      CloseComponent(myImporter);

   return(myPicture);
}

We are finally ready to put this all together. When the user selects the "Add Skin Track..." menu item, we execute the QTSkin_AddSkinTrack function defined in Listing 4.

Listing 4: Adding a skin track to a movie

OSErr QTSkin_AddSkinTrack (Movie theMovie)
{
   Track               myTrack = NULL;         // the movie track
   Media               myMedia = NULL;         // the movie track's media
   Rect                myRect;
   MediaHandler        myHandler = NULL;
   PicHandle           myContentPic = NULL;
   PicHandle           myDragPic = NULL;
   OSErr               myErr = paramErr;

   if (theMovie == NULL)
      goto bail;

   // elicit the two pictures we need from the user
   myContentPic = QTSkin_GetPicHandleFromFile();
   if (myContentPic == NULL)
      goto bail;

   myDragPic = QTSkin_GetPicHandleFromFile();
   if (myDragPic == NULL)
      goto bail;

   // get the movie's dimensions
   GetMovieBox(theMovie, &myRect);
   MacOffsetRect(&myRect, -myRect.left, -myRect.top);

   // create the skin track and media
   myTrack = NewMovieTrack(theMovie, 
                        FixRatio(myRect.right, 1), 
                        FixRatio(myRect.bottom, 1), kNoVolume);
   if (myTrack == NULL)
      goto bail;

   myMedia = NewTrackMedia(myTrack, FOUR_CHAR_CODE(‘skin'), 
                        GetMovieTimeScale(theMovie), NULL, 0);
   if (myMedia == NULL)
      goto bail;

   myHandler = GetMediaHandler(myMedia);
   if (myHandler == NULL)
      goto bail;

   // add the skin content picture as public media information
   myErr = MediaSetPublicInfo(myHandler, 
                        FOUR_CHAR_CODE(‘skcr'), 
                        (void *)myContentPic, 0);
   if (myErr != noErr)
      goto bail;

   // add the skin drag picture as public media information
   myErr = MediaSetPublicInfo(myHandler, 
                        FOUR_CHAR_CODE(‘skdr'), 
                        (void *)myDragPic, 0);
   if (myErr != noErr)
      goto bail;

   // add the media to the track
   myErr = InsertMediaIntoTrack(myTrack, 0, 0, 
                        GetMediaDuration(myMedia), fixed1);

bail:
   if (myContentPic != NULL)
      KillPicture(myContentPic);

   if (myDragPic != NULL)
      KillPicture(myDragPic);

   return(myErr);
}

As you can see, using MediaSetPublicInfo greatly simplifies creating a skin track. We don't have to create a sample description, and we don't need to call BeginMediaEdits or AddMediaSample or EndMediaEdits. The skin media handler takes care of all the details of storing the content and drag region masks in the skin media.

Skinned Movie Playback

So now we know how to build a skinned movie, using either the QuickTime XML importer or our own application code. As mentioned earlier, we also want our application to be able to open and play back skinned movies. This turns out to be significantly more complicated, however, since we need to be able to assign a custom window shape to a movie window and window shapes are handled by the application, not by QuickTime. So, we're going to have to get acquainted with some of the low-level window-handling capabilities of our host operating systems if we want to be able to open and manipulate skinned movies.

On Macintosh operating systems, we assign a custom shape to a movie window by writing a custom window definition procedure. Under Carbon, the code for a custom window definition procedure is contained in the application itself, not in a code resource of type ‘WDEF' (as in the pre-Carbon Mac world). Once we've defined our custom procedure, we can call the CreateCustomWindow function to create a skinned movie window. Whenever the Window Manager needs to draw our custom window or handle clicks on it, it calls our custom window definition procedure.

On Windows operating systems, it's even easier to assign a custom shape to a window: we can call the SetWindowRgn function when opening the movie window to assign an arbitrary region as the window shape. We'll also add a little code to our basic movie window procedure QTFrame_MovieWndProc to handle skinned window dragging.

Before we can do any of this, however, we need to get ahold of the skin data that determines the window's appearance and drag behavior. That is, we need to read the content and drag region masks out of the skinned movie file. As you might guess, we'll use GetMediaPublicInfo to get the picture data stored in the skin track. Then we'll need to convert that picture into a region, which we'll pass to the operating system window handlers.

Setting Up the Application Data

For each skinned movie file we open, we need to maintain some application-specific data. Basically, we need a place to keep track of the various regions describing the geometry of the skinned movie window. In the file ComApplication.h, we'll declare the ApplicationDataRecord data structure like this:

typedef struct ApplicationDataRecord {
   RgnHandle      fContentRegion;         // content region of window
   RgnHandle      fDragRegion;            // drag region of window
   RgnHandle      fStructRegion;          // structure region of window
#if TARGET_OS_WIN32
   HRGN               fWinHRGN;           // window region
#endif
} ApplicationDataRecord, *ApplicationDataPtr, 
                                       **ApplicationDataHdl;

The fContentRegion and fDragRegion fields hold handles to the content region and the drag region of the window, which we retrieve from the skin track as we're opening a skinned movie. The fStructRegion field holds the structure region of the skinned window. A window's structure region is the entire screen area occupied by the window, including the window's content region and its window frame. For skinned movies, the structure region is usually identical to its content region. The fWinHRGN field holds the window content region as an object of type HRGN. This is the object we'll to pass to SetWindowRgn when we set the window's shape on Windows.

Recall that the data stored in a skin track is of type PicHandle. We can retrieve that data by calling GetMediaPublicInfo, passing it a selector for the type of information we want. For instance, to retrieve the content region mask from a skin track, we can execute this code:

myPicture = (PicHandle)NewHandle(0);
if (myPicture == NULL)
   goto bail;

myErr = MediaGetPublicInfo(myHandler, FOUR_CHAR_CODE(‘skcr'), 
                  myPicture, NULL);

If GetMediaPublicInfo completes successfully, myPicture will contain a handle to the picture data. We 
then need to convert this picture data into a region, since that's the kind of data we'll need to have
available in our custom window definition procedure. We make this conversion by calling the application 
function QTSkin_ConvertPictureToRegion:

myErr = QTSkin_ConvertPictureToRegion(myPicture, 
                  &(**myAppData).fContentRegion);

QTSkin_ConvertPictureToRegion creates a region that contains every non-white pixel in the specified picture. The key step is using QuickDraw's BitmapToRegion function to convert a bitmap or a pixel map into a region. So we need to create a pixel map from our picture data. But this is very easy: we simply create a new offscreen graphics world and draw the picture data into it. We can then use the GetGWorldPixMap function to get the pixel map associated with that graphics world. Listing 5 shows our definition of QTSkin_ConvertPictureToRegion.

Listing 5: Converting a picture into a region

OSErr QTSkin_ConvertPictureToRegion (PicHandle thePicture, 
                  RgnHandle *theRegionPtr)
{
   Rect                     myRect;
   GWorldPtr            myGWorld = NULL;
   PixMapHandle         myPixMap = NULL;
   CGrafPtr               mySavedPort = NULL;
   GDHandle               mySavedDevice = NULL;
   RgnHandle            myRegion = NULL;
   OSErr                  myErr = noErr;

   if ((thePicture == NULL) || (theRegionPtr == NULL))
      return(paramErr);

   // get the current graphics port and device
   GetGWorld(&mySavedPort, &mySavedDevice);
   
   // get the bounding box of the picture
   myRect = (**thePicture).picFrame;
   myRect.bottom = EndianS16_BtoN(myRect.bottom);
   myRect.right = EndianS16_BtoN(myRect.right);

   // create a new GWorld and draw the picture into it
   myErr = QTNewGWorld(&myGWorld, k1MonochromePixelFormat, 
                     &myRect, NULL, NULL, kICMTempThenAppMemory);
   if (myGWorld == NULL)
      goto bail;

   SetGWorld(myGWorld, NULL);

   myPixMap = GetGWorldPixMap(myGWorld);
   if (myPixMap == NULL)
      goto bail;

   LockPixels(myPixMap);
   HLock((Handle)myPixMap);

   EraseRect(&myRect);
   DrawPicture(thePicture, &myRect);

   // create a new region and convert the pixmap into a region
   myRegion = NewRgn();
   myErr = MemError();
   if (myErr != noErr)
      goto bail;

   myErr = BitMapToRegion(myRegion, (BitMap *)*myPixMap);

bail:
   if (myErr != noErr) {
      if (myRegion != NULL) {
         DisposeRgn(myRegion);
         myRegion = NULL;
      }
   }

   if (myGWorld != NULL)
      DisposeGWorld(myGWorld);

   // restore the original graphics port and device
   SetGWorld(mySavedPort, mySavedDevice);

   *theRegionPtr = myRegion;

   return(myErr);
}
For our Windows applications, we need to take one further step and convert the Macintosh region (of type RgnHandle) into a Windows region (of type HRGN). The QuickTime Media Layer provides a function that will do this for us:
(**myAppData).fWinHRGN = MacRegionToNativeRegion
                                          ((**myAppData).fContentRegion);

All of this start-up code will go into the function QTSkin_InitWindowData, which is called by QTApp_SetupWindowObject to perform any application-specific initialization of the movie window and its associated data. QTApp_SetupWindowObject contains this code to handle skinned movies:

if (QTSkin_IsSkinnedMovie(myMovie)) {
   // hide the controller bar
   MCSetVisible(myMC, false);

   // detach the controller
   MCSetControllerAttached(myMC, false);

   // initialize the window data for a skins movie
   (**theWindowObject).fAppData = 
                  (Handle)QTSkin_InitWindowData(theWindowObject);
}

When QTApp_SetupWindowObject is called, the skinned movie window has already been created but it has not yet been displayed to the user. On Windows, we used our standard function QTFrame_CreateMovieWindow to create the movie window. So at this point, on Windows, we can already call SetWindowRgn to set the shape of the skinned movie window:

if ((**myAppData).fWinHRGN != NULL) {
   RECT         myRect;
   int         myResult;

   GetRgnBox((**myAppData).fWinHRGN, &myRect);

   OffsetRgn((**myAppData).fWinHRGN, 
      -myRect.left + GetSystemMetrics(SM_CXFRAME), 
      -myRect.top + GetSystemMetrics(SM_CYCAPTION) + 
                           GetSystemMetrics(SM_CYFRAME));
   myResult = SetWindowRgn((**theWindowObject).fWindow, 
                  (**myAppData).fWinHRGN, true);
}

SetWindowRgn sets the visible region of a window; it expects the origin of the window region we pass it to be relative to the upper-left corner of the window, not relative to the client area of the window. So we need to offset the stored window region (**myAppData).fWinHRGN horizontally by the width of the window frame and vertically by the height of the window frame and the height of the title bar (or caption). Figure 7 shows the penguin window with these offsets.


Figure 7: The client region offsets

As far as Windows is concerned, the window frame and window controls still exist — they are just not visible on the screen. The window is a full-fledged MDI child window, just like any of our other (non-skinned) movie windows. The only difference is that the skinned movie window has a special visible region.

Listing 6 shows the full version of our skinned movie window initialization code.

Listing 6: Initializing the application data for a skinned movie

ApplicationDataHdl QTSkin_InitWindowData 
                                       (WindowObject theWindowObject)
{
   ApplicationDataHdl         myAppData = NULL;
   Track                        myTrack = NULL;
   MediaHandler               myHandler = NULL;
   PicHandle                  myPicture = NULL;
   MatrixRecord               myMatrix;
   OSErr                          myErr = noErr;

   // if we already have some window data, dump it
   myAppData = 
      (ApplicationDataHdl)QTFrame_GetAppDataFromWindowObject
               (theWindowObject);
   if (myAppData != NULL)
      QTSkin_DumpWindowData(theWindowObject);     // see Listing 14

   myAppData = 
      (ApplicationDataHdl)NewHandleClear
               (sizeof(ApplicationDataRecord));
   if (myAppData != NULL) {

      myTrack = GetMovieIndTrackType
         ((**theWindowObject).fMovie, 1, FOUR_CHAR_CODE(‘skin'), 
         movieTrackCharacteristic);
      if (myTrack != NULL) {
         myHandler = GetMediaHandler(GetTrackMedia(myTrack));
         if (myHandler != NULL) {

            // get the current movie matrix
            GetMovieMatrix((**theWindowObject).fMovie, 
                                                &myMatrix);

            myPicture = (PicHandle)NewHandle(0);
            if (myPicture == NULL)
               goto bail;

            // get the content region picture
            myErr = MediaGetPublicInfo(myHandler, 
                           FOUR_CHAR_CODE(‘skcr'), myPicture, NULL);
            if (myErr != noErr)
               goto bail;

            // convert it to a region
            myErr = QTSkin_ConvertPictureToRegion(myPicture, 
                           &(**myAppData).fContentRegion);
            if (myErr != noErr)
               goto bail;

            // scale that region so the window scales with the movie
            myErr = TransformRgn(&myMatrix, 
                           (**myAppData).fContentRegion);
            if (myErr != noErr)
               goto bail;

#if TARGET_OS_WIN32
            (**myAppData).fWinHRGN = MacRegionToNativeRegion(
                           (**myAppData).fContentRegion);
            if ((**myAppData).fWinHRGN != NULL) {
               RECT         myRect;
               int         myResult;

               GetRgnBox((**myAppData).fWinHRGN, &myRect);
               // the coordinates of a window region are relative to the upper-left corner 
               // of the window (not to the client area of the window)
               OffsetRgn((**myAppData).fWinHRGN, 
                  -myRect.left + GetSystemMetrics(SM_CXFRAME), 
                  -myRect.top + GetSystemMetrics(SM_CYCAPTION) + 
                  GetSystemMetrics(SM_CYFRAME));
               myResult = SetWindowRgn(
                  (**theWindowObject).fWindow, 
                  (**myAppData).fWinHRGN, true);
               if (myResult == 0) {
                  // SetWindowRgn failed
                  DeleteObject((**myAppData).fWinHRGN);
                  (**myAppData).fWinHRGN = NULL;
                  goto bail;
               }
            }
#endif

            // repeat with drag region picture
            myErr = MediaGetPublicInfo(myHandler, 
                           FOUR_CHAR_CODE(‘skdr'), myPicture, NULL);
            if (myErr != noErr)
               goto bail;

            // convert it to a region
            myErr = QTSkin_ConvertPictureToRegion(myPicture, 
                           &(**myAppData).fDragRegion);
            if (myErr != noErr)
               goto bail;

            // scale that region so the window scales with the movie
            myErr = TransformRgn(&myMatrix, 
                           (**myAppData).fDragRegion);
            if (myErr != noErr)
               goto bail;

            // copy the content region into the structure region
            (**myAppData).fStructRegion = NewRgn();
            MacCopyRgn((**myAppData).fContentRegion, 
                           (**myAppData).fStructRegion);
         }
      }
   }

bail:
   if (myPicture != NULL)
      DisposeHandle((Handle)myPicture);

   return(myAppData);
}

Specifying a Custom Window Shape

As we've seen, it's child's play on Windows operating systems to specify a custom window shape: just pass the shape (as an HRGN) to SetWindowRgn. On the Mac, it's quite a bit more complicated. We need to write a custom window definition procedure and attach it to any skinned movies that the user opens. In our framework function QTFrame_OpenMovieInWindow, we'll add a few Mac-specific lines before the existing call to QTFrame_CreateMovieWindow:

#if TARGET_OS_MAC
   // create a new window to display the movie in
   if (QTSkin_IsSkinnedMovie(myMovie))
      myWindow = QTSkin_CreateSkinsWindow();
   else
#endif
      myWindow = QTFrame_CreateMovieWindow();

On Macintosh computers, QTFrame_CreateMovieWindow calls the Window Manager function NewCWindow to create a standard document window. For skinned windows, we need to call CreateCustomWindow, as shown in Listing 7.

Listing 7: Opening a window with a custom shape

WindowReference QTSkin_CreateSkinsWindow (void)
{
   WindowPtr                  myWindow = NULL;
   WindowReference         myWindowRef = NULL;
   Rect                         myRect = {10, 60, 200, 200};

   // call CreateCustomWindow to create a window using our custom window defproc
   CreateCustomWindow(&gDefSpec, kDocumentWindowClass, 
                        kWindowNoAttributes, &myRect, &myWindow);
   if (myWindow != NULL) {
      // get the "window reference" for this window
      myWindowRef = 
         QTFrame_GetWindowReferenceFromWindow(myWindow);

      // create a new window object associated with the new window
      QTFrame_CreateWindowObject(myWindowRef);
   }

   return(myWindowRef);
}

This call to CreateCustomWindow asks for a document window with no special attributes. (The rectangle parameter is arbitrary, since we'll change the window size later.) The window definition procedure to be used to handle the custom window is specified by the &gDefSpec parameter, which is a pointer to a window definition specification, declared like this:

struct WindowDefSpec {
   WindowDefType               defType;
   union {
      WindowDefUPP             defProc;
      Void                     *classRef;
      Short                    procID;
   } u;
};

The defType field specifies which member of the union u we want to use. In the present case, we want to use the defProc member, so we set defType to kWindowDefProcPtr. And we'll set the defProc member to a universal procedure pointer to our custom window definition procedure. We initialize the gDefSpec global variable in the application start-up code for QTSkins, by calling the QTSkin_Init function defined in Listing 8.

Listing 8: Setting up a window definition specification

void QTSkin_Init (void)
{
   // set up the window definition specification structure
   gDefSpec.defType = kWindowDefProcPtr;
   gDefSpec.u.defProc = NewWindowDefUPP(QTSkin_SkinWindowDef);
}

Writing a Custom Window Definition Procedure

On Macintosh operating systems, the appearance and behavior of our skinned movie windows are determined by QTSkin_SkinWindowDef, our custom window definition procedure. QTSkin_SkinWindowDef is declared like this:

static PASCAL_RTN long QTSkin_SkinWindowDef 
                     (short theVarCode, WindowRef theWindow, 
                        short theMessage, long theParam);

Here, theMessage is a window definition message that indicates which task the window definition procedure is to perform. These are the common window definition messages:

enum {
   kWindowMsgDraw                        = 0,
   kWindowMsgHitTest                     = 1,
   kWindowMsgCalculateShape              = 2,
   kWindowMsgInitialize                  = 3,
   kWindowMsgCleanUp                     = 4,
   kWindowMsgDrawGrowOutline             = 5,
   kWindowMsgDrawGrowBox                 = 6,
   kWindowMsgGetFeatures                 = 7,
   kWindowMsgGetRegion                   = 8,
   kWindowMsgDragHilite                  = 9,
   kWindowMsgModified                    = 10,
   kWindowMsgDrawInCurrentPort           = 11,
   kWindowMsgSetupProxyDragImage         = 12,
   kWindowMsgStateChanged                = 13,
   kWindowMsgMeasureTitle                = 14,
   kWindowMsgGetGrowImageRegion          = 19
};

We can ignore most of these messages in our procedure. For instance, our skinned movie windows don't have grow boxes, so we can ignore the kWindowMsgDrawGrowOutline and kWindowMsgDrawGrowBox messages. In fact, we'll need to handle only three of these messages: kWindowMsgHitTest, kWindowMsgGetFeatures, and kWindowMsgGetRegion.

When we receive the kWindowMsgGetFeatures message, we need to return (through theParam) a value that indicates the capabilities of our custom window definition procedure. Really all our custom procedure can do is return information about various window regions. So we'll set the features information like this:

case kWindowMsgGetFeatures:
   if (theParam != 0L)
      *(OptionBits *)theParam = kWindowCanGetWindowRegion;
   return(1);

The meaning of the return value of our custom window definition procedure varies, depending on the message the procedure is handling. In this case, the documentation tells us to return 1.

When we receive the kWindowMsgHitTest message, we need to return one of these values, indicating which region of the movie (if any) was clicked in:

enum {
   wNoHit                                  = 0,
   wInContent                              = 1,
   wInDrag                                 = 2,
   wInGrow                                 = 3,
   wInGoAway                               = 4,
   wInZoomIn                               = 5,
   wInZoomOut                              = 6,
   wInCollapseBox                          = 9,
   wInProxyIcon                            = 10
};

With this message, theParam contains the coordinates of the mouse click, which we can extract like this:

myPoint.v = HiWord(theParam);
myPoint.h = LoWord(theParam);

This point is in global screen coordinates. Our regions, however, are stored with the upper-left corner 
set to (0,Ę0). So we need to map myPoint into the window's local coordinate system, as follows:

GetPort(&myPort);
SetPortWindowPort(theWindow);

myLocal = myPoint;
GlobalToLocal(&myLocal);

MacSetPort(myPort);

The GlobalToLocal function maps the specified point into the coordinate system of the current graphics port, so we need to make sure that our custom window is the current graphics port (taking care to save and restore the previous current port).

Now that we've got a point local to the skinned movie window, we can use the PtInRgn function to do the required hit-testing:

if (PtInRgn(myLocal, (**myAppData).fDragRegion))
   return(wInDrag);

if (PtInRgn(myLocal, (**myAppData).fContentRegion))
   return(wInContent);

return(wNoHit);

We first look to see whether the specified point is in the drag region. If not, we look to see whether it's in the content region. If the point is in neither region, we indicate that no hit occurred.

When we receive the kWindowMsgGetRegion message, theParam is a pointer to a structure of type GetWindowRegionRec:

struct GetWindowRegionRec {
   RgnHandle                winRgn;
   WindowRegionCode         regionCode;
};

The regionCode field indicates which region we are supposed to return (through the winRgn field). Our skinned movie windows have only three interesting regions, the content region, the drag region, and the structure region (which is typically identical to the content region). So we'll respond to only three values for the regionCode field: kWindowContentRgn, kWindowDragRgn, and kWindowStructureRgn.

The region whose handle we return in the winRgn field is supposed to be specified in global screen coordinates. Our stored regions, however, are specified in coordinates local to the client region of the movie window. So we need to offset those regions before we return them from our window definition procedure. First, then, we need to figure out the global coordinates of the top-left corner of the window, like this:

GetPort(&myPort);   
SetPortWindowPort(theWindow);

GetPortBounds(GetWindowPort(theWindow), &myPortBounds);

myTopLeft.h = myPortBounds.left;
myTopLeft.v = myPortBounds.top;
LocalToGlobal(&myTopLeft);

MacSetPort(myPort);

Then we need to offset any of the regions we pass back. For instance, we'll pass back the window's drag region like this:

MacCopyRgn((**myAppData).fDragRegion, myRgnRec->winRgn);
MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h, myTopLeft.v);

Listing 9 shows our complete window definition procedure for skinned movie windows.

Listing 9: Handling skinned movie window messages

static PASCAL_RTN long QTSkin_SkinWindowDef 
                     (short theVarCode, WindowRef theWindow, 
                        short theMessage, long theParam)
{
#pragma unused(theVarCode)

   switch (theMessage) {

      case kWindowMsgInitialize:
      case kWindowMsgCleanUp:
      case kWindowMsgDrawGrowOutline:
      case kWindowMsgDrawGrowBox:
      case kWindowMsgDraw:
         // nothing here
         break;

      case kWindowMsgHitTest: {
         ApplicationDataHdl      myAppData = NULL;
         Point                        myPoint;
         Point                        myLocal;
         GrafPtr                     myPort;

         myAppData = 
               (ApplicationDataHdl)QTFrame_GetAppDataFromWindow
               (QTFrame_GetWindowReferenceFromWindow(theWindow));
         if (myAppData == NULL)
            return(wNoHit);

         // on entry, theParam contains the mouse location in global screen coordinates
         myPoint.v = HiWord(theParam);
         myPoint.h = LoWord(theParam);

         // the content and drag regions are offset relative to the window origin
         GetPort(&myPort);
         SetPortWindowPort(theWindow);

         myLocal = myPoint;
         GlobalToLocal(&myLocal);

         MacSetPort(myPort);

         // look first to see if the mouse event is in the drag region;
         // it takes precedence over the content region
         if (PtInRgn(myLocal, (**myAppData).fDragRegion))
            return(wInDrag);

         if (PtInRgn(myLocal, (**myAppData).fContentRegion))
            return(wInContent);

         return(wNoHit);
      }

      case kWindowMsgGetFeatures:
         if (theParam != 0L)
            *(OptionBits *)theParam = kWindowCanGetWindowRegion;
         return(1);

      case kWindowMsgGetRegion: {
         GetWindowRegionRec      *myRgnRec = 
                                       (GetWindowRegionRec *)theParam;
         ApplicationDataHdl      myAppData = NULL; 
         GrafPtr                     myPort;
         Rect                           myPortBounds;
         Point                        myTopLeft;

         myAppData = 
               (ApplicationDataHdl)QTFrame_GetAppDataFromWindow
               (QTFrame_GetWindowReferenceFromWindow(theWindow));
         if (myAppData == NULL)
            break;

         // get the top-left corner of the window, in global coordinates
         GetPort(&myPort);   
         SetPortWindowPort(theWindow);

#if TARGET_API_MAC_CARBON
         GetPortBounds(GetWindowPort(theWindow), &myPortBounds);
#else
         myPortBounds = theWindow->portRect;
#endif
         myTopLeft.h = myPortBounds.left;
         myTopLeft.v = myPortBounds.top;
         LocalToGlobal(&myTopLeft);

         MacSetPort(myPort);

         switch (myRgnRec->regionCode) {
            case kWindowTitleBarRgn:
            case kWindowCloseBoxRgn:
               break;

            case kWindowDragRgn:
               MacCopyRgn((**myAppData).fDragRegion, 
                                                   myRgnRec->winRgn);
               MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h, 
                                                   myTopLeft.v);
               break;

            case kWindowContentRgn:
               MacCopyRgn((**myAppData).fContentRegion, 
                                                   myRgnRec->winRgn);
               MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h, 
                                                   myTopLeft.v);
               break;

            case kWindowStructureRgn:
               MacCopyRgn((**myAppData).fStructRegion, 
                                                   myRgnRec->winRgn);
               MacOffsetRgn(myRgnRec->winRgn, myTopLeft.h, 
                                                   myTopLeft.v);
               break;

            default:
               break;
         }

         return(noErr);
      }

      default:
         break;
   }

   return(0L);
}

Handling Dragging on Windows Computers

Earlier we saw how to assign a custom window shape to a movie on Windows operating systems, by calling SetWindowRgn. We still need to see how to handle window dragging on Windows. Let's begin by reviewing briefly how our window procedure for movie windows processes the messages it receives. Listing 10 shows a snippet from QTFrame_MovieWndProc. First of all, it fills out an MSG structure and translates the Windows message into a Macintosh event by calling WinEventToMacEvent. Then it passes the Mac event to the application function QTApp_HandleEvent. Then, if QTApp_HandleEvent did not handle the event, QTFrame_MovieWndProc passes the Mac event to MCIsPlayerEvent.

Listing 10: Sending Windows messages to the movie controller

MSG          myMsg = {0};
LONG         myPoints = GetMessagePos();

myMsg.hwnd = theWnd;
myMsg.message = theMessage;
myMsg.wParam = wParam;
myMsg.lParam = lParam;
myMsg.time = GetMessageTime();
myMsg.pt.x = LOWORD(myPoints);
myMsg.pt.y = HIWORD(myPoints);

// translate a Windows event to a Mac event
WinEventToMacEvent(&myMsg, &myMacEvent);

// let the application-specific code have a chance to intercept the event
myIsHandled = QTApp_HandleEvent(&myMacEvent);

// pass the Mac event to the movie controller
if (!myIsHandled)
   if (myMC != NULL)
      if (!IsIconic(theWnd))
         myIsHandled = MCIsPlayerEvent(myMC, 
                                             (EventRecord *)&myMacEvent);

With skinned windows, the drag regions and the content regions virtually always overlap, so we need to prevent the movie controller from getting any mouse clicks that are in the drag region (since it would likely interpret them as clicks in the content region). We can do this quite easily by having QTApp_HandleEvent look to see whether the event it's passed is a mouse click in the drag region and, if it is, return true. Listing 11 shows the QTSkins version of QTApp_HandleEvent. Note that this code is conditionalized for Windows applications only, since on Macintosh the window definition procedure is responsible for finding clicks in the drag region.

Listing 11: Looking for drag region clicks (Windows)

Boolean QTApp_HandleEvent (EventRecord *theEvent)
{
#if TARGET_OS_MAC
#pragma unused(theEvent)
#endif

   Boolean            myIsHandled = false;

#if TARGET_OS_WIN32
   ApplicationDataHdl      myAppData = (ApplicationDataHdl)
                              QTFrame_GetAppDataFromFrontWindow();
   Point                        myPoint;

   if (theEvent == NULL)
      goto bail;

   if (theEvent->what == mouseDown) {
      myPoint = theEvent->where;
      GlobalToLocal(&myPoint);

      if (myAppData != NULL)
         if (PtInRgn(myPoint, (**myAppData).fDragRegion))
            myIsHandled = true;
   }
#endif

bail:
   return(myIsHandled);
}

So far, then, we've managed to prevent the movie controller associated with a movie window from getting clicks in the window's drag region. Now we need to actually handle those clicks. On Windows, we can look for messages of type WM_LBUTTONDOWN and see if they are in the drag region. If they are, we want to trick the default window procedure into thinking that the clicks are on the title bar, so that the default window procedure will handle the dragging for us. We can do this by sending a message of type WM_NCLBUTTONDOWN to the default window procedure, like this:

SendMessage(theWnd, WM_NCLBUTTONDOWN, (WPARAM)HTCAPTION, 
                                          MAKELPARAM(5, 5));

The WM_NCLBUTTONDOWN message reports a button-down event in a non-client area of a window. The first parameter indicates which part of the window is directly under the cursor hot spot at the time of the click. In our case, we want to say that the click occurred in the title bar (indicated by the HTCAPTION constant). The second parameter indicates the location of the cursor hot spot, in coordinates that are relative to the upper-left corner of the screen. As best I can tell, the default window procedure ignores that parameter when the first parameter is set to HTCAPTION. So we'll pass an arbitrary value of (5,Ę5). Our complete left-button click handling is shown in Listing 12.

Listing 12: Handling drag region clicks (Windows)

case WM_LBUTTONDOWN:
   // handle potential clicks in window drag region; 
   // if we get one, map it into a click on the title bar
   if (QTSkin_IsSkinnedMovie(myMovie))
      if (QTSkin_IsDragClick(myWindowObject, lParam)) {
         SendMessage(theWnd, WM_NCLBUTTONDOWN, 
                     (WPARAM)HTCAPTION, MAKELPARAM(5, 5));
         myIsHandled = true;
      }

   // do any application-specific mouse-button handling, 
   // but only if the message hasn't already been handled
   if (!myIsHandled)
      QTApp_HandleContentClick(theWnd, &myMacEvent);

   break;

The only thing left is to consider the definition of QTSkin_IsDragClick, which we call in Listing 12 to determine whether the specified point is in the drag region of the skinned movie window. Here we have several possibilities. We saw above that our version of QTApp_HandleEvent returns true if the specified event is a mouse-down event in the window's drag region. So we could just use that function. Alternatively, we can convert the Mac-style drag region (saved in our application data record) to a Windows region (of type HRGN) and call the Windows function PtInRegion to see whether the specified point is in that region. That's the strategy we'll use here; Listing 13 shows our definition of QTSkin_IsDragClick.

Listing 13: Finding drag region clicks (Windows)

#if TARGET_OS_WIN32
Boolean QTSkin_IsDragClick 
                  (WindowObject theWindowObject, LONG lParam)
{
   WindowObject                   myWindowObject = NULL;
   ApplicationDataHdl             myAppData = NULL;
   HRGN                           myRegion = NULL;
   POINT                          myPoint;
   Boolean                        isDragClick = false;

   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData != NULL) {
      myPoint.x = LOWORD(lParam);
      myPoint.y = HIWORD(lParam);

      myRegion = MacRegionToNativeRegion
            ((**myAppData).fDragRegion);

      if (PtInRegion(myRegion, myPoint.x, myPoint.y))
         isDragClick = true;

      DeleteObject(myRegion);
   }

   return(isDragClick);
}
#endif

The lParam parameter that was passed to WM_LBUTTONDOWN (which we also pass to QTSkin_IsDragClick) specifies a point in coordinates that are local to the client area of the window. As a result, we don't need to offset the drag region in Listing 13.

So now we've completely handled a click in the drag region of a skinned movie window on Windows.

Shutting Down

When the user closes a skinned movie window, we need to deallocate any memory used for displaying the movie in a skin. In particular, we need to dispose of the window regions that we're storing in the application data record. Listing 14 shows the definition of QTSkin_DumpWindowData, which is called by QTApp_RemoveWindowObject.

Listing 14: Cleaning up when a skinned window is closed

void QTSkin_DumpWindowData (WindowObject theWindowObject)
{
   ApplicationDataHdl      myAppData = NULL;

   myAppData = (ApplicationDataHdl)
            QTFrame_GetAppDataFromWindowObject(theWindowObject);
   if (myAppData != NULL) {
      if ((**myAppData).fContentRegion != NULL)
         DisposeRgn((**myAppData).fContentRegion);

      if ((**myAppData).fDragRegion != NULL)
         DisposeRgn((**myAppData).fDragRegion);

      if ((**myAppData).fStructRegion != NULL)
         DisposeRgn((**myAppData).fStructRegion);

      DisposeHandle((Handle)myAppData);
      (**theWindowObject).fAppData = NULL;
   }
}

You'll notice that we didn't do anything to free up the memory addressed by (**myAppData).fWinHRGN. The documentation for the SetWindowRgn function indicates that the operating system owns the region we pass it; this means that we don't need to call DeleteObject on that region.

When our application shuts down, we need to deallocate the universal procedure pointer contained inside of the gDefSpec structure. Listing 15 shows how we do this.

Listing 15: Cleaning up at application shut-down

void QTSkin_Stop (void)
{
   // dispose of the window procedure UPP
   if (gDefSpec.u.defProc != NULL)
      DisposeWindowDefUPP(gDefSpec.u.defProc);
}

Conclusion

If you've made it this far, you deserve a pat on the back. We've had our usual dose of new QuickTime APIs, but we've also had a big gulp of low-level window management. On the Macintosh, we had to write a custom window definition procedure in order for our application to handle skinned movie windows. And on Windows, we had to tinker with our application's event-handling to support skinned movie window dragging. But the payoff for all this work is tremendous, precisely because skinned movies are such great stuff. As we've noted, the movie author now has virtually complete control over the appearance and behavior of movie windows. The movie interface has become part of the movie content. The medium is now part of the message.

Credits

Special thanks are due to Jim Batson for reviewing this article and providing some helpful comments. Thanks are also due to ici Media, Inc. (http://www.icimediainc.net) for permission to use the picture of the movie in Figure 3.


Tim Monroe is intrigued to discover that his lizards often eat their own skin after they molt. You can explain this to him at monroe@apple.com.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

Latest Forum Discussions

See All

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

Price Scanner via MacPrices.net

Apple Watch Ultra 2 now available at Apple fo...
Apple has, for the first time, begun offering Certified Refurbished Apple Watch Ultra 2 models in their online store for $679, or $120 off MSRP. Each Watch includes Apple’s standard one-year warranty... Read more
AT&T has the iPhone 14 on sale for only $...
AT&T has the 128GB Apple iPhone 14 available for only $5.99 per month for new and existing customers when you activate unlimited service and use AT&T’s 36 month installment plan. The fine... Read more
Amazon is offering a $100 discount on every M...
Amazon is offering a $100 instant discount on each configuration of Apple’s new 13″ M3 MacBook Air, in Midnight, this weekend. These are the lowest prices currently available for new 13″ M3 MacBook... Read more
You can save $300-$480 on a 14-inch M3 Pro/Ma...
Apple has 14″ M3 Pro and M3 Max MacBook Pros in stock today and available, Certified Refurbished, starting at $1699 and ranging up to $480 off MSRP. Each model features a new outer case, shipping is... Read more
24-inch M1 iMacs available at Apple starting...
Apple has clearance M1 iMacs available in their Certified Refurbished store starting at $1049 and ranging up to $300 off original MSRP. Each iMac is in like-new condition and comes with Apple’s... Read more
Walmart continues to offer $699 13-inch M1 Ma...
Walmart continues to offer new Apple 13″ M1 MacBook Airs (8GB RAM, 256GB SSD) online for $699, $300 off original MSRP, in Space Gray, Silver, and Gold colors. These are new MacBook for sale by... Read more
B&H has 13-inch M2 MacBook Airs with 16GB...
B&H Photo has 13″ MacBook Airs with M2 CPUs, 16GB of memory, and 256GB of storage in stock and on sale for $1099, $100 off Apple’s MSRP for this configuration. Free 1-2 day delivery is available... Read more
14-inch M3 MacBook Pro with 16GB of RAM avail...
Apple has the 14″ M3 MacBook Pro with 16GB of RAM and 1TB of storage, Certified Refurbished, available for $300 off MSRP. Each MacBook Pro features a new outer case, shipping is free, and an Apple 1-... Read more
Apple M2 Mac minis on sale for up to $150 off...
Amazon has Apple’s M2-powered Mac minis in stock and on sale for $100-$150 off MSRP, each including free delivery: – Mac mini M2/256GB SSD: $499, save $100 – Mac mini M2/512GB SSD: $699, save $100 –... Read more
Amazon is offering a $200 discount on 14-inch...
Amazon has 14-inch M3 MacBook Pros in stock and on sale for $200 off MSRP. Shipping is free. Note that Amazon’s stock tends to come and go: – 14″ M3 MacBook Pro (8GB RAM/512GB SSD): $1399.99, $200... Read more

Jobs Board

*Apple* Systems Administrator - JAMF - Syste...
Title: Apple Systems Administrator - JAMF ALTA is supporting a direct hire opportunity. This position is 100% Onsite for initial 3-6 months and then remote 1-2 Read more
Relationship Banker - *Apple* Valley Financ...
Relationship Banker - Apple Valley Financial Center APPLE VALLEY, Minnesota **Job Description:** At Bank of America, we are guided by a common purpose to help Read more
IN6728 Optometrist- *Apple* Valley, CA- Tar...
Date: Apr 9, 2024 Brand: Target Optical Location: Apple Valley, CA, US, 92308 **Requisition ID:** 824398 At Target Optical, we help people see and look great - and Read more
Medical Assistant - Orthopedics *Apple* Hil...
Medical Assistant - Orthopedics Apple Hill York Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Now Read more
*Apple* Systems Administrator - JAMF - Activ...
…**Public Trust/Other Required:** None **Job Family:** Systems Administration **Skills:** Apple Platforms,Computer Servers,Jamf Pro **Experience:** 3 + years of Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.