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

Whitethorn Games combines two completely...
If you have ever gone fishing then you know that it is a lesson in patience, sitting around waiting for a bite that may never come. Well, that's because you have been doing it wrong, since as Whitehorn Games now demonstrates in new release Skate... | Read more »
Call of Duty Warzone is a Waiting Simula...
It's always fun when a splashy multiplayer game comes to mobile because they are few and far between, so I was excited to see the notification about Call of Duty: Warzone Mobile (finally) launching last week and wanted to try it out. As someone who... | Read more »
Albion Online introduces some massive ne...
Sandbox Interactive has announced an upcoming update to its flagship MMORPG Albion Online, containing massive updates to its existing guild Vs guild systems. Someone clearly rewatched the Helms Deep battle in Lord of the Rings and spent the next... | Read more »
Chucklefish announces launch date of the...
Chucklefish, the indie London-based team we probably all know from developing Terraria or their stint publishing Stardew Valley, has revealed the mobile release date for roguelike deck-builder Wildfrost. Developed by Gaziter and Deadpan Games, the... | Read more »
Netmarble opens pre-registration for act...
It has been close to three years since Netmarble announced they would be adapting the smash series Solo Leveling into a video game, and at last, they have announced the opening of pre-orders for Solo Leveling: Arise. [Read more] | Read more »
PUBG Mobile celebrates sixth anniversary...
For the past six years, PUBG Mobile has been one of the most popular shooters you can play in the palm of your hand, and Krafton is celebrating this milestone and many years of ups by teaming up with hit music man JVKE to create a special song for... | Read more »
ASTRA: Knights of Veda refuse to pump th...
In perhaps the most recent example of being incredibly eager, ASTRA: Knights of Veda has dropped its second collaboration with South Korean boyband Seventeen, named so as it consists of exactly thirteen members and a video collaboration with Lee... | Read more »
Collect all your cats and caterpillars a...
If you are growing tired of trying to build a town with your phone by using it as a tiny, ineffectual shover then fear no longer, as Independent Arts Software has announced the upcoming release of Construction Simulator 4, from the critically... | Read more »
Backbone complete its lineup of 2nd Gene...
With all the ports of big AAA games that have been coming to mobile, it is becoming more convenient than ever to own a good controller, and to help with this Backbone has announced the completion of their 2nd generation product lineup with their... | Read more »
Zenless Zone Zero opens entries for its...
miHoYo, aka HoYoverse, has become such a big name in mobile gaming that it's hard to believe that arguably their flagship title, Genshin Impact, is only three and a half years old. Now, they continue the road to the next title in their world, with... | Read more »

Price Scanner via MacPrices.net

B&H has Apple’s 13-inch M2 MacBook Airs o...
B&H Photo has 13″ MacBook Airs with M2 CPUs and 256GB of storage in stock and on sale for up to $150 off Apple’s new MSRP, starting at only $849. Free 1-2 day delivery is available to most US... Read more
M2 Mac minis on sale for $100-$200 off MSRP,...
B&H Photo has Apple’s M2-powered Mac minis back in stock and on sale today for $100-$200 off MSRP. Free 1-2 day shipping is available for most US addresses: – Mac mini M2/256GB SSD: $499, save $... Read more
Mac Studios with M2 Max and M2 Ultra CPUs on...
B&H Photo has standard-configuration Mac Studios with Apple’s M2 Max & Ultra CPUs in stock today and on Easter sale for $200 off MSRP. Their prices are the lowest available for these models... Read more
Deal Alert! B&H Photo has Apple’s 14-inch...
B&H Photo has new Gray and Black 14″ M3, M3 Pro, and M3 Max MacBook Pros on sale for $200-$300 off MSRP, starting at only $1399. B&H offers free 1-2 day delivery to most US addresses: – 14″ 8... Read more
Department Of Justice Sets Sights On Apple In...
NEWS – The ball has finally dropped on the big Apple. The ball (metaphorically speaking) — an antitrust lawsuit filed in the U.S. on March 21 by the Department of Justice (DOJ) — came down following... Read more
New 13-inch M3 MacBook Air on sale for $999,...
Amazon has Apple’s new 13″ M3 MacBook Air on sale for $100 off MSRP for the first time, now just $999 shipped. Shipping is free: – 13″ MacBook Air (8GB RAM/256GB SSD/Space Gray): $999 $100 off MSRP... Read more
Amazon has Apple’s 9th-generation WiFi iPads...
Amazon has Apple’s 9th generation 10.2″ WiFi iPads on sale for $80-$100 off MSRP, starting only $249. Their prices are the lowest available for new iPads anywhere: – 10″ 64GB WiFi iPad (Space Gray or... Read more
Discounted 14-inch M3 MacBook Pros with 16GB...
Apple retailer Expercom has 14″ MacBook Pros with M3 CPUs and 16GB of standard memory discounted by up to $120 off Apple’s MSRP: – 14″ M3 MacBook Pro (16GB RAM/256GB SSD): $1691.06 $108 off MSRP – 14... Read more
Clearance 15-inch M2 MacBook Airs on sale for...
B&H Photo has Apple’s 15″ MacBook Airs with M2 CPUs (8GB RAM/256GB SSD) in stock today and on clearance sale for $999 in all four colors. Free 1-2 delivery is available to most US addresses.... Read more
Clearance 13-inch M1 MacBook Airs drop to onl...
B&H has Apple’s base 13″ M1 MacBook Air (Space Gray, Silver, & Gold) in stock and on clearance sale today for $300 off MSRP, only $699. Free 1-2 day shipping is available to most addresses in... Read more

Jobs Board

Medical Assistant - Surgical Oncology- *Apple...
Medical Assistant - Surgical Oncology- Apple Hill Location: WellSpan Medical Group, York, PA Schedule: Full Time Sign-On Bonus Eligible Remote/Hybrid Regular Apply Read more
Omnichannel Associate - *Apple* Blossom Mal...
Omnichannel Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Cashier - *Apple* Blossom Mall - JCPenney (...
Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom Mall Read more
Operations Associate - *Apple* Blossom Mall...
Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Business Analyst | *Apple* Pay - Banco Popu...
Business Analyst | Apple PayApply now " Apply now + Apply Now + Start applying with LinkedIn Start + Please wait Date:Mar 19, 2024 Location: San Juan-Cupey, PR Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.