TweetFollow Us on Twitter

Animal Crackers

Volume Number: 19 (2003)
Issue Number: 1
Column Tag: QuickTime Toolkit

Animal Crackers

Enhancing Cocoa QuickTime Applications

by Tim Monroe

Introduction

In the previous QuickTime Toolkit article ("The Cocoanuts" in MacTech, December 2002), we took a look at using Cocoa, Apple's object-oriented application framework, to develop QuickTime applications. We saw how to put together a basic multi-window application that can open, play, and edit QuickTime movies, and we also investigated a few ways to extend that application by subclassing Cocoa's built-in QuickTime classes or by adding additional methods to those classes with categories.

In this article, I want to continue investigating Cocoa and QuickTime. Primarily, I want to tie up a few loose ends that we didn't have time to address in the previous article. For instance, we need to do a little bit more work to get all the items in the Edit and File menus working as expected, and we need to fix a few minor cosmetic problems with the application. Consider this article then to be some fine tuning to get our Cocoa application, MooVeez, to provide all the functionality of our sample application QTShell. Along the way, however, I also want to take time to investigate a few new topics, including the wired action introduced in QuickTime 6 that allows us to set a movie's scale. And, believe it or not, I'm going to slip in a few tweaks to QTShell itself. So don't be surprised if you see some Carbon code along the way.

Before we begin, I should note that in this article (and the previous one), we're relying on the features of QuickTime and Cocoa that shipped with the so-called Jaguar release: Mac OS X version 10.2 or later, and QuickTime 6.0 or later. Earlier versions of the QuickTime Cocoa classes, NSMovie and NSMovieView, have some noticeable problems in a few key areas. The Jaguar versions of these classes are much more reliable and efficient, so that's what I'll assume we're working with.

Displayable Paths

Let's begin with an easy but important update to our existing MooVeez code. Recall that our application can open a QuickTime movie in a window, and that this window provides a pop-out drawer listing some basic information about the movie (Figure 1).


Figure 1: A movie information drawer

The "Source" is the full path of the QuickTime movie file. In the previous article, I offered the setSource: method shown in Listing 1, which breaks the pathname into its components, tosses out the string "Volumes", and then rebuilds a path using the colon (":") as the path separator.

Listing 1: Displaying the movie's source

setSource
- (void)setSource: (NSString *)name
{
   NSArray *pathComponents = [name 
            componentsSeparatedByString:@"/"];
   NSEnumerator *pathEnumerator = [pathComponents 
            objectEnumerator];
   NSString *component = [pathEnumerator nextObject];
   NSMutableString *massagedPath = [NSMutableString string];
   while (component != nil) {
      if (([component length] > 0) && 
               (strcmp([component cString], "Volumes") != 0)) {
         [massagedPath appendString:component];
         component = [pathEnumerator nextObject];
         if (component != nil)
                [massagedPath appendString:@":"];
         } else {
            component = [pathEnumerator nextObject];
      }
   }
   [_sourceName setStringValue: massagedPath];
}

This isn't quite right, however, since it will toss out a path component named "Volumes" wherever it occurs in the full pathname (not just at the beginning).

I have subsequently learned that there is a Cocoa method that can assist us here; the NSFileManager class provides the componentsToDisplayForPath: method, which returns an array containing the components of a file's displayable name (that is, the name that should be displayed to the user). So if, as above, name is the full UNIX pathname of a file, we can get the array of displayable components like this:

NSArray *pathComponents = [[NSFileManager defaultManager] 
            componentsToDisplayForPath:name];

For instance, the full UNIX pathname of the movie file shown in Figure 1 is:

/Volumes/Meditations/Applications/QuickTime/Sample Movie

The componentsToDisplayForPath: method returns an array whose 4 elements are these:

Meditations
Applications
QuickTime
Sample Movie

Then we just need to reassemble these components with the appropriate path separator. Listing 2 shows our revised version of the setSource: method.

Listing 2: Displaying the movie's source (revised)

setSource
- (void)setSource:(NSString *)name
{
   NSArray *pathComponents = [[NSFileManager defaultManager] 
            componentsToDisplayForPath:name];
   NSEnumerator *pathEnumerator = [pathComponents 
            objectEnumerator];
   NSString *component = [pathEnumerator nextObject];
   NSMutableString *displayablePath = 
            [NSMutableString string];
   while (component != nil) {
      if ([component length] > 0) {
         [displayablePath appendString:component];
         component = [pathEnumerator nextObject];
         if (component != nil)
            [displayablePath appendString:@":"];
      } else {
         component = [pathEnumerator nextObject];
      }
   }
   [_sourceName setStringValue:displayablePath];
}

It's perhaps worth mentioning that Carbon applications can call the Launch Services functions LSCopyDisplayNameForRef or LSCopyDisplayNameForURL to get displayable path names.

Edit Menu

There is one outright bug in the Jaguar implementation of NSMovieView that rears its head when we try to undo a change to a movie. Do this: open a movie, select some of the movie data using the controller bar, and then cut the selected data. As expected, the Paste menu item is now enabled; the problem is that the Undo menu item is not enabled. (See Figure 2.) We ought to be able to undo the cut, and NSMovieView should be handling this automatically for us, but it's not.


Figure 2: The Edit menu of MooVeez

The Cocoa engineers are aware of this problem, and I believe that a fix has been found and will be incorporated into a future version of NSMovieView. In the meantime, there appears to be a fairly simple workaround to this problem. The workaround involves subclassing NSMovieView so that we can override the validateMenuItem: method. Listing 3 shows the override method.

Listing 3: Enabling and disabling the Undo menu item

validateMenuItem
- (BOOL)validateMenuItem:(NSMenuItem *)item
{
   BOOL isValid = NO;
   SEL action = [item action];
   // handle the Undo menu item
   if (action == @selector(undo:)) {
      MovieController mc = [self movieController];
      if (mc) {
         long flags = 0L;
         MCGetControllerInfo(mc, &flags);
         isValid = flags & mcInfoUndoAvailable;
      }
   } else {
      isValid = [super validateMenuItem:item];
   }
   return isValid;
}

Here we're just calling MCGetControllerInfo and checking the returned set of flags to see whether the mcInfoUndoAvailable flag is set. If it is, we enable the Undo menu item; otherwise we disable the menu item. Notice that we pass all other menu items to the superclass (that is, to NSMovieView).

Once we've added this code to our subclass of NSMovieView, the Undo and Redo menu items are enabled and disabled as expected, as shown in Figure 3. In this case, the user has already cut some of the movie data and then undone the cut (that's why the Redo menu item is enabled).


Figure 3: The Edit menu of MooVeez (revised)

In addition, the Undo and Redo menu items appear to function correctly now. Take this new code for a spin: undo some cuts and pastes and see if everything works as expected. My preliminary tests do not reveal any problems, but I'm a bit worried by the following console message that appears the first time the user performs an undo operation:

Warning: -[NSMovieView undo:] is obsolete.

I recommend employing this workaround only after thoroughly testing it yourself.

File Menu

Now that we've got the Edit menu working pretty much as expected, let's take a quick look back at the File menu. In the previous article, we explicitly postponed adding support for creating new, empty movie windows -- that is, supporting the New menu item. To disable the New menu item, we added a validateMenuItem: method to our application controller class (defined in the file AppController.m). And we overrode the newDocument: method to act as a no-op. Since we want to add support for creating new empty movies, we need to remove these methods from our application controller class entirely. Once we do that, the default behaviors of the NSDocument class will kick in and we'll be able to create new empty documents.

For this to work correctly, however, we need to make a few simple fixes to the code that is called by the windowControllerDidLoadNib: method. Hitherto we have assumed that the user had opened a movie file by selecting the Open or "Open Recent" menu item and choosing a movie file, or by dropping a movie file onto the application's icon. In either case, we know that the movie is associated with an existing movie file. But once we allow the user to create a new movie, we have to make sure not to rely on NSDocument's fileName method returning a non-nil value. We'll revise the code in our initializeMovieWindowFromFile: method, as shown in Listing 4.

Listing 4: Creating a new movie

initializeMovieWindowFromFile
if ([self fileName]) {
   // open the movie file with read/write permission and load the movie from it
   err = FSPathMakeRef([[self fileName] 
            fileSystemRepresentation], &fileRef, NULL);
   if (err == noErr)
      err = FSGetCatalogInfo(&fileRef, kFSCatInfoNone, NULL, 
            NULL, &fileSpec, NULL);
   if (err == noErr)
      err = OpenMovieFile(&fileSpec, &fileRefNum, 
            fsRdWrPerm);
   if (err == noErr)
      err = NewMovieFromFile(&qtMovie, fileRefNum, 
            &fileResNum, NULL, 0, NULL);
} else {
   qtMovie = NewMovie(newMovieActive);
}

As you can see, we've conditionalized the existing code so that it is called only if the document already has a file associated with it; otherwise, we simply call NewMovie to create a new empty movie.

We also need to make sure to set the window title to the last component of the file name only if the file actually has a name:

if ([self fileName])
   [[_movieView window] setTitle:[self 
            lastComponentOfFileName]];

If we do not explicitly set the window title, NSDocument will generate one automatically. Figure 4 shows a new document window with an empty movie.


Figure 4: A new, empty document window

We can cut or copy data in other movie windows and then paste it into this new movie. And we can save the new movie into a file, just as we would expect.

Listing 4 is interesting also because it illustrates how to convert a string of type NSString into a file system specification (of type FSSpec): get a C string representation of the NSString, create an FSRef, and then call FSGetCatalogInfo to get an FSSpec record from the FSRef. Listing 5 shows this technique packaged into a nice utility method.

Listing 5: Converting a pathname string into a file system specification

NSStringToFSSpec
(void)NSStringToFSSpec:(NSString *)theFilePath 
            fsSpec:(FSSpec *)theFSSpecPtr
{
   FSRef fsRef;
   Boolean isDirectory = false;
   OSStatus err = noErr;
   // create an FSRef for the specified target file
   if (theFilePath) {
      err = FSPathMakeRef([theFilePath 
            fileSystemRepresentation], &fsRef, &isDirectory);
      // create an FSSpec record from the FSRef
      if (!err && !isDirectory) {
         err = FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, 
            NULL, theFSSpecPtr, NULL);
      }
   }
}

This is useful because Cocoa likes to work with full pathnames, while the QuickTime APIs tend to prefer FSSpec records. Note, however, that the NSStringToFSSpec function works only with pathnames that describe existing files; if we pass in a path to a nonexistent file, FSPathMakeRef will return fnfErr (file not found) and fail to create an FSRef.

If we want to create an FSSpec record for a file that does not exist, we need to be a bit more creative. Listing 6 defines the writeToFile: method, which might be called (for instance) when the user selects the "Save As" menu item. As you can see, if FSPathMakeRef returns fnfErr, we call the standard UNIX file system functions open, write, and close to create a new file. Then we call FSPathMakeRef and FSGetCatalogInfo (as above) to get an FSSpec record, which we pass to FlattenMovie.

Listing 6: Writing a movie into a file

writeToFile
(BOOL)writeToFile:(NSString *)path 
            ofType:(NSString *)type
{
   FSRef fsRef;
   FSSpec fsSpec;
   NSString *newPath;
   OSStatus err = noErr;
   newPath = [NSString stringWithFormat:@"%@~", path];
   // create an FSRef for the specified target file
   err = FSPathMakeRef([newPath fileSystemRepresentation], 
            &fsRef, NULL);
   if (err == fnfErr) {
      // if the file does not yet exist, then let's create the file
      int fd;
      fd = open([newPath fileSystemRepresentation], 
            O_CREAT | O_RDWR, 0600);
      if (fd < 0)
         return NO;
      write(fd, " ", 1);
      close(fd);
      err = FSPathMakeRef([newPath fileSystemRepresentation], 
            &fsRef, NULL);
   }
   if (err == noErr) {
      // create an FSSpec record from the FSRef
      err = FSGetCatalogInfo(&fsRef, kFSCatInfoNone, NULL, 
            NULL, &fsSpec, NULL);
      if (err == noErr) {
         short resId;
         // flatten the movie data into the specified target file
         FlattenMovie([[_movieView movie] QTMovie],
            flattenAddMovieToDataFork | 
            flattenForceMovieResourceBeforeMovieData,
            &fsSpec,
            'TVOD',
            smSystemScript,
            createMovieFileDeleteCurFile | 
            createMovieFileDontCreateResFile,
            &resId,
            nil);
         CloseMovieFile(resId);
      }
      rename([newPath fileSystemRepresentation], 
            [path fileSystemRepresentation]);
      return YES;
   }
   // clean up
   unlink([newPath fileSystemRepresentation]);
   return NO;
}

Window Resizing

Let's now take a look at a couple of issues related to resizing our movie windows. Typically our document windows will be resized directly by the user, by dragging the resize box in the lower right corner of the document window. In Interface Builder, we've set the Size attributes of the movie view as shown in Figure 5.


Figure 5: The size attributes of the movie view

The autosizing springs within the movie view and the rigid connections between the movie view and its superview (the document window) indicate that the movie view should grow or shrink to maintain a constant distance from all its edges to the edges of the document window. That is, when the user resizes the document window, the movie view will automatically be resized to maintain its border.

Occasionally, however, we need to adjust the size of the document window based on the desired size of the movie view. We did this previously when setting the size of the window for a newly-opened QuickTime movie. We'll also want to adjust the size of the document window when we receive the mcActionControllerSizeChanged movie controller action. So the first thing I want to do is rework our existing code a bit, to factor out the code that sets the movie size. Listing 7 shows our new method windowContentSizeForMovie:.

Listing 7: Getting a window size from a movie size

windowContentSizeForMovie
- (NSSize)windowContentSizeForMovie:(Movie)qtMovie
{
   NSSize size;
   Rect rect;
   GetMovieBox(qtMovie, &rect);
   size.width = (float)(rect.right - rect.left);
   size.height = (float)(rect.bottom - rect.top);
   // enforce a minimum width (important for sound-only movies)
   if (size.width == 0)
      size.width = (float)240;
   size.width += 2 * kMoviePaneOffset;
   size.height += 2 * kMoviePaneOffset;
   if ([_movieView isControllerVisible])
      size.height += kMovieControllerBarHeight;
   return size;
}

When the movie window is awaked from the nib file, we set the movie window size like this:

[[_movieView window] setContentSize:
            [self windowContentSizeForMovie:qtMovie]];

And when we receive the mcActionControllerSizeChanged movie controller action, we can set the movie window size as shown in Listing 8.

Listing 8: Setting a window size from a movie size

MyActionFilter
case mcActionControllerSizeChanged: 
   [[[doc movieView] window] setContentSize:
            [doc windowContentSizeForMovie:[[[doc movieView] 
            movie] QTMovie]]];
   break;

Now, when do we receive the mcActionControllerSizeChanged action? We'll receive it whenever the user manually resizes our document window, since NSMovieView informs the movie controller -- probably by calling MCSetControllerBoundsRect -- whenever the movie view is resized. (In this case, we really don't need to call setContentSize:, but it doesn't hurt to do so.) We'll also receive the mcActionControllerSizeChanged action when certain wired actions change the movie size. For instance, QuickTime 6 introduced the kActionMovieSetScale action, which sets the target movie's scale (or magnification). And earlier versions of QuickTime included the kActionTrackSetMatrix wired action, which sets a track's matrix. When the movie controller processes either of these actions, it will eventually inform our application of the size change by sending the mcActionControllerSizeChanged action to our filter procedure.

It turns out that the code in Listing 8 can lead to some drawing glitches when triggered by these wired actions. When a movie receives the kActionMovieSetScale action, our window can end up looking like the one in Figure 6. As you can see, the controller bar was not correctly erased at its previous position or redrawn fully in the new position.


Figure 6: Drawing problems after a set scale action

I haven't investigated this behavior enough to know whether it's a problem in QuickTime's action-handling code or in our own code. But there is at least one easy workaround: just inform the movie controller that the movie has changed, by calling MCMovieChanged. Listing 9 shows our updated code for handling the mcActionControllerSizeChanged action.

Listing 9: Setting a window size from a movie size (revised)

MyActionFilter
case mcActionControllerSizeChanged:
   [[[doc movieView] window] setContentSize:
            [doc windowContentSizeForMovie:[[[doc movieView] 
            movie] QTMovie]]];
   MCMovieChanged([[doc movieView] movieController], 
            [[[doc movieView] movie] QTMovie]);
   break;

By the way, this is not a Cocoa-specific problem. It will also affect our Carbon-based QTShell application. Accordingly, we should add a call to MCMovieChanged in QTShell's movie controller action filter procedure as well, as shown in Listing 10.

Listing 10: Setting a window size from a movie size (QTShell)

QTApp_MCActionFilterProc
case mcActionControllerSizeChanged:
   QTFrame_SizeWindowToMovie(myWindowObject);
   if (theMC && (**myWindowObject).fMovie)
      MCMovieChanged(theMC, (**myWindowObject).fMovie);
   isHandled = true;
   break;

There is at least one other occasion when the movie controller might send us the mcActionControllerSizeChanged action, namely when a streamed movie changes its size dynamically during playback. We don't need any additional code to handle this, but we do need to tell the Movie Toolbox that we want to be informed of any changes in the size of streamed movies. We do this by setting a movie playback hint when we open the movie, as follows:

SetMoviePlayHints(qtMovie, hintsAllowDynamicResize, 
            hintsAllowDynamicResize);

We are informed of movie size changes triggered by wired actions even if we don't set this play hint, but not those triggered by a dynamic size change of a streamed movie.

Cursor Adjustment

As you know, some media types change the cursor image as it moves over various regions in a track. Flash tracks and wired sprite tracks often do this, and QuickTime VR does this as a matter of course. (And with a vengeance: QuickTime VR defines nearly 80 different cursors that can be displayed as the user performs operations within a panoramic movie, and over 100 within an object movie.) Figure 7 shows a QuickTime VR movie with the mouse lying on top of a link hot spot (that is, a hot spot that, when clicked, moves the user to a new node).


Figure 7: A QuickTime VR node with the mouse over a link hot spot

The trouble is that some media types don't change the cursor back to the default arrow cursor when it moves outside of the movie box. The result is that our application ends up displaying an incorrect cursor, as shown in Figure 8. The mouse is just to the right of the movie box (toward the top of the movie). Note that its image is still one supplied by QuickTime VR, not the standard arrow cursor. We should fix that.


Figure 8: A QuickTime VR cursor displayed outside the movie box

Adjusting the Cursor in Carbon

Before we see how to adjust the cursor in our Cocoa application, let's digress briefly to consider how we already do this in our Carbon application QTShell. Listing 11 shows the definition of the QTApp_Idle function, which we call whenever we receive an idle event. As you can see, we simply check whether the current mouse position (which we get by calling GetMouse) is outside of the front movie window or front movie window's visible region; if the mouse is indeed outside of these regions, we call MacSetCursor to reset the cursor to the arrow cursor.

Listing 11: Adjusting the cursor in an idle procedure

QTApp_Idle
void QTApp_Idle (WindowReference theWindow)
{
   WindowObject         myWindowObject = NULL;
   GrafPtr               mySavedPort;
   GetPort(&mySavedPort);
   MacSetPort(QTFrame_GetPortFromWindowReference(theWindow));
   myWindowObject = QTFrame_GetWindowObjectFromWindow
            (theWindow);
   if (myWindowObject != NULL) {
      MovieController      myMC = NULL;
      myMC = (**myWindowObject).fController;
      if (myMC != NULL) {
#if TARGET_OS_MAC
         // restore the cursor to the arrow
         // if it's outside the front movie window or outside the window's visible region
         if (theWindow == QTFrame_GetFrontMovieWindow()) {
            Rect            myRect;
            Point         myPoint;
            RgnHandle   myVisRegion;
            Cursor         myArrow;
            GetMouse(&myPoint);
            myVisRegion = NewRgn();
            GetPortVisibleRegion
               (QTFrame_GetPortFromWindowReference(theWindow), 
                     myVisRegion);
            GetWindowPortBounds(theWindow, &myRect);
            if (!MacPtInRect(myPoint, &myRect) || 
                     !PtInRgn(myPoint, myVisRegion))
               MacSetCursor(GetQDGlobalsArrow(&myArrow));
            DisposeRgn(myVisRegion);
         }
#endif
      }
   }
   MacSetPort(mySavedPort);
}

Frankly, this is old-style Mac programming. It works well enough, but it's likely to eat up CPU cycles unnecessarily since it's still stuck in that old polling mindset. Let's upgrade QTShell by reimplementing cursor adjusting using Carbon events. (For a general discussion of Carbon events and QuickTime, see "Event Horizon" in MacTech, May 2002.)

My first attempt to use Carbon events here was to have the window event handler register for events of class kEventClassMouse and type kEventMouseMoved. After all, we need to check to see if the cursor needs to change only if it's been moved. The trouble is that a movie in QTShell completely fills the content region of a movie window (except for the space occupied by the controller bar at the bottom of a movie window). Once the cursor moves out of the movie to the left or right, it's no longer over the movie window and hence subsequent mouse movements will not trigger kEventMouseMoved events for that movie window.

So I ended by having the application event handler register for kEventMouseMoved events, as shown in Listing 12. When it receives a mouse-moved event, it retrieves the position of the mouse and then checks to see if the mouse is currently outside the frontmost movie window. If it is, the cursor is reset to the default arrow cursor.

Listing 12: Adjusting the cursor in a Carbon event handler

QTFrame_CarbonEventAppHandler
case kEventClassMouse:
   switch (myKind) {
      case kEventMouseMoved:
         myErr = GetEventParameter(theEvent, 
                  kEventParamMouseLocation, typeQDPoint, NULL, 
                  sizeof(Point), NULL, &myPoint);
         if (myErr == noErr) {
            WindowRef      myWindow = NULL;
            Rect               myRect;
            Cursor            myArrow;
         
            GlobalToLocal(&myPoint);
            
            // get the front movie window
            myWindow = QTFrame_GetFrontMovieWindow();
            if (myWindow != NULL) {
               GetWindowPortBounds(myWindow, &myRect);
               if (!PtInRect(myPoint, &myRect))
                  MacSetCursor(GetQDGlobalsArrow(&myArrow));
            }
         }
         break;
   }
   break;

This strategy appears to work quite nicely, and it avoids the polling behavior of our original code. Strictly speaking, we need to adjust the cursor only when a movie contains interactive tracks (that is, Flash, wired sprite, wired text, or QuickTime VR), since they are the only kinds of tracks that are likely to change the cursor from the default arrow cursor. I doubt, however, that there is much to be gained by checking for interactive track types here. It's probably better just to reset the cursor to the arrow for every kind of QuickTime movie, as we do here.

Adjusting the Cursor in Cocoa

Let's return to the more pressing concern of adjusting the cursor in our Cocoa application. There are two principal ways we can return the cursor to its default arrow shape when it moves outside of the movie rectangle. First, we can add a tracking rectangle to the NSMovieView view that holds our movie, by executing the addTrackingRect: method. For instance, inside of windowControllerDidLoadNib:, we can execute this line of code:

[_movieView addTrackingRect:[_movieView bounds] owner:self 
            userData:nil assumeInside:NO];

The addTrackingRect: method causes the specified owner to receive mouseEntered: and mouseExited: messages whenever the mouse is moved into and out of the specified rectangle inside the target view. The message recipient is often the same view whose rectangle will be tracked, but it need not be. In the present case, indeed, we are setting the document instance as the owner, so that these messages are sent to it and not to the movie view. This allows us to define mouseEntered: and mouseExited: within our custom document class, so that we do not need to subclass NSMovieView.

Actually, since we are only interested in knowing when the mouse leaves the movie view, we shall implement only the mouseExited: method (Listing 13).

Listing 13: Handling mouse-exited messages

mouseExited
- (void) mouseExited:(NSEvent*)event
{
   // set cursor to the default cursor
   [[NSCursor arrowCursor] set];
}

Here we send the factory method arrowCursor to the NSCursor class and then set the resulting cursor.

It turns out that windowControllerDidLoadNib: is not really the best place to call addTrackingRect:. The main reason for this is that the tracking rectangle is not automatically resized when the target view is resized. Rather, we need to explicitly remove any current tracking rectangle and then add a new one each time the movie view changes size. So let's make the call to addTrackingRect: inside of our movie controller action filter procedure, when we handle the mcActionControllerSizeChanged action. Listing 14 shows our revised code for handling this action.

Listing 14: Resetting the tracking rectangle

MyActionFilter
case mcActionControllerSizeChanged:
   [[[doc movieView] window] setContentSize:
            [doc windowContentSizeForMovie:
            [[[doc movieView] movie] QTMovie]]];
   [[doc movieView] removeTrackingRect:
            [doc trackingRectTag]];
   [doc setTrackingRectTag:[[doc movieView] 
            addTrackingRect:[[doc movieView] bounds] owner:doc 
            userData:nil assumeInside:NO]];
   MCMovieChanged([[doc movieView] movieController], 
            [[[doc movieView] movie] QTMovie]);
   break;

First, we send the removeTrackingRect: message to the movie view to remove any existing tracking rectangle. Notice that removeTrackingRect: takes a parameter, which specifies a tracking rectangle tag (of the scalar type NSTrackingRectTag). This tag is returned to us by addTrackingRect:, and we are storing it in the new instance variable _trackingRectTag in our document class. Since we are calling removeTrackingRect: and addTrackingRect: within our movie controller action filter procedure, the document class needs to provide accessor methods for us to get and set the tag. Listings 15 and 16 show our definitions of these two methods. Nothing earthshaking here.

Listing 15: Getting the tracking rectangle tag

trackingRectTag
- (NSTrackingRectTag)trackingRectTag
{
   return _trackingRectTag;
}
Listing 16: Setting the tracking rectangle tag
setTrackingRectTag
- (void)setTrackingRectTag:(NSTrackingRectTag)rectTag
{
   _trackingRectTag = rectTag;
}

The second way of adjusting our cursor is to define a cursor rectangle for the movie view. A cursor rectangle is a special kind of tracking rectangle. What's special about it is that NSView knows that the rectangle is likely to change if the associated view is resized or moved. Accordingly, NSView calls the view's resetCursorRects method whenever that happens, to give the view a chance to resize or move the cursor rectangle. Listing 17 shows how we might define the resetCursorRects method.

Listing 17: Resetting the cursor rectangle

resetCursorRects
- (void)resetCursorRects
{
   NSCursor      *newCursor;
   [super resetCursorRects];
   newCursor = [NSCursor arrowCursor];
   [self addCursorRect:[self bounds] cursor:newCursor];
   [newCursor setOnMouseExited:YES];
}

Here we create a new arrow cursor and call addCursorRect: to attach it to a new cursor rectangle that is as large as the target view (that is, the movie view). Then we call setOnMouseExited: to configure the cursor to set itself as the current cursor when the mouse moves outside of the cursor rectangle.

As you can see, there is much less code required when using cursor rectangles than when using tracking rectangles. We don't need to keep track of the tracking rectangle tag, and we don't need any accessor functions to pass that tag to the movie controller action filter procedure. The only downside to using cursor rectangles is that we must subclass NSMovieView (so that we have a class to define the resetCursorRects method). As we saw above, we did not need to subclass NSMovieView when using tracking rectangles.

Conclusion

In this article, we revisited the basic Cocoa movie playing and editing application that we developed in the previous article, with an eye to tying up a few loose ends. We've now got all the items in the File and Edit menus working as expected, and we've cleaned up a few cosmetic glitches in our original version of MooVeez. With these improvements, MooVeez now matches quite closely the capabilities of QTShell, our benchmark Carbon movie playback and editing application. Moreover, the Cocoa framework upon which MooVeez is built provides a number of additional features not found in QTShell, including the "Open Recent" menu item in the File menu and the Window menu for managing all open document windows.

Credits

Listing 6 is based on some code by Vince DeMarco.


Tim Monroe in a member of the QuickTime engineering team. You can contact him at monroe@apple.com. The views expressed here are not necessarily shared by his employer.

 

Community Search:
MacTech Search:

Software Updates via MacUpdate

NetShade 6.3.1 - Browse privately using...
NetShade is an anonymous proxy and VPN app+service for Mac. Unblock your Internet through NetShade's high-speed proxy and VPN servers spanning seven countries. NetShade masks your IP address as you... Read more
Dragon Dictate 4.0.7 - Premium voice-rec...
With Dragon Dictate speech recognition software, you can use your voice to create and edit text or interact with your favorite Mac applications. Far more than just speech-to-text, Dragon Dictate lets... Read more
Persecond 1.0.2 - Timelapse video made e...
Persecond is the easy, fun way to create a beautiful timelapse video. Import an image sequence from any camera, trim the length of your video, adjust the speed and playback direction, and you’re done... Read more
GIMP 2.8.14p2 - Powerful, free image edi...
GIMP is a multi-platform photo manipulation tool. GIMP is an acronym for GNU Image Manipulation Program. The GIMP is suitable for a variety of image manipulation tasks, including photo retouching,... Read more
Sandvox 2.10.2 - Easily build eye-catchi...
Sandvox is for Mac users who want to create a professional looking website quickly and easily. With Sandvox, you don't need to be a Web genius to build a stylish, feature-rich, standards-compliant... Read more
LibreOffice 5.0.1.2 - Free, open-source...
LibreOffice is an office suite (word processor, spreadsheet, presentations, drawing tool) compatible with other major office suites. The Document Foundation is coordinating development and... Read more
f.lux 36.1 - Adjusts the color of your d...
f.lux makes the color of your computer's display adapt to the time of day, warm at night and like sunlight during the day. Ever notice how people texting at night have that eerie blue glow? Or wake... Read more
VirtualBox 5.0.2 - x86 virtualization so...
VirtualBox is a family of powerful x86 virtualization products for enterprise as well as home use. Not only is VirtualBox an extremely feature rich, high performance product for enterprise customers... Read more
File Juicer 4.43 - Extract images, video...
File Juicer is a drag-and-drop can opener and data archaeologist. Its specialty is to find and extract images, video, audio, or text from files which are hard to open in other ways. In computer... Read more
Apple MainStage 3.2 - Live performance t...
MainStage 3 makes it easy to bring to the stage all the same instruments and effects that you love in your recording. Everything from the Sound Library and Smart Controls you're familiar with from... Read more

ReBoard: Revolutionary Keyboard (Utilit...
ReBoard: Revolutionary Keyboard 1.0 Device: iOS Universal Category: Utilities Price: $1.99, Version: 1.0 (iTunes) Description: Do everything within the keyboard without switching apps! If you are in WhatsApp, how do you schedule a... | Read more »
Tiny Empire (Games)
Tiny Empire 1.1.3 Device: iOS Universal Category: Games Price: $2.99, Version: 1.1.3 (iTunes) Description: Launch cannonballs and blow tiny orcs into thousands of pieces in this intuitive fantasy-themed puzzle shooter! Embark on an... | Read more »
Astropad Mini (Productivity)
Astropad Mini 1.0 Device: iOS iPhone Category: Productivity Price: $4.99, Version: 1.0 (iTunes) Description: *** 50% off introductory price! ​*** Get the high-end experience of a Wacom tablet at a fraction of the price with Astropad... | Read more »
Emo Chorus (Music)
Emo Chorus 1.0.0 Device: iOS Universal Category: Music Price: $1.99, Version: 1.0.0 (iTunes) Description: Realistic Choir simulator ranging from simple Chorus emulation to full ensemble Choir with 128 members. ### introductory offer... | Read more »
Forest Spirit (Games)
Forest Spirit 1.0.5 Device: iOS Universal Category: Games Price: $2.99, Version: 1.0.5 (iTunes) Description: | Read more »
Ski Safari 2 (Games)
Ski Safari 2 1.0 Device: iOS Universal Category: Games Price: $1.99, Version: 1.0 (iTunes) Description: The world's most fantastical, fun, family-friendly skiing game is back and better than ever! Play as Sven's sister Evana, share... | Read more »
Lara Croft GO (Games)
Lara Croft GO 1.0.47768 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.47768 (iTunes) Description: Lara Croft GO is a turn based puzzle-adventure set in a long-forgotten world. Explore the ruins of an ancient... | Read more »
Whispering Willows (Games)
Whispering Willows 1.23 Device: iOS Universal Category: Games Price: $4.99, Version: 1.23 (iTunes) Description: **LAUNCH SALE 50% OFF** - Whispering Willows is on sale for 50% off ($4.99) until September 9th. | Read more »
Calvino Noir (Games)
Calvino Noir 1.1 Device: iOS iPhone Category: Games Price: $3.99, Version: 1.1 (iTunes) Description: The film noir stealth game. Calvino Noir is the exploratory, sneaking adventure through the 1930s European criminal underworld.... | Read more »
Angel Sword (Games)
Angel Sword 1.0 Device: iOS Universal Category: Games Price: $6.99, Version: 1.0 (iTunes) Description: Prepare to adventure in the most epic full scale multiplayer 3D RPG for mobile! Experience amazing detailed graphics in full HD.... | Read more »

Price Scanner via MacPrices.net

Big Grips Lift Handle For iPad Air and iPad A...
KEM Ventures, Inc. which pioneered the extra-large, super-protective iPad case market with the introduction of Big Grips Frame and Stand in 2011, is launching Big Grips Lift featuring a new super-... Read more
Samsung Launches Galaxy Tab S2, Its Most Powe...
Samsung Electronics America, Inc. has announced the U.S. release of the Galaxy Tab S2, its thinnest, lightest, ultra-fast tablet. Blending form and function, elegant design and multitasking power,... Read more
Tablet Screen Sizes Expanding as iPad Pro App...
Larger screen sizes are gaining favor as the tablet transforms into a productivity device, with shipments growing 185 percent year-over-year in 2015. According to a new Strategy Analytics’ Tablet... Read more
Today Only: Save US$50 on Adobe Elements 13;...
Keep the memories. lose the distractions. Summer’s winding down and it’s time to turn almost perfect shots into picture perfect memories with Elements 13. And get the power to edit both photos and... Read more
1.4GHz Mac mini on sale for $449, save $50
Best Buy has the 1.4GHz Mac mini on sale for $50 off MSRP on their online store. Choose free shipping or free local store pickup (if available). Price for online orders only, in-store price may vary... Read more
12-inch 1.1GHz Gold MacBook on sale for $1149...
B&H Photo has the 12″ 1.1GHz Gold Retina MacBook on sale for $1149.99 including free shipping plus NY sales tax only. Their price is $150 off MSRP, and it’s the lowest price available for this... Read more
27-inch 3.3GHz 5K iMac on sale for $1849, sav...
Best Buy has the 27″ 3.3GHz 5K iMac on sale for $1849.99. Their price is $150 off MSRP, and it’s the lowest price available for this model. Choose free shipping or free local store pickup (if... Read more
Worldwide Tablet Shipments Expected to Declin...
Does Apple badly need a touchscreen convertible/hybrid laptop MacBook? Yes, judging from a new market forecast from the International Data Corporation (IDC) Worldwide Quarterly Tablet Tracker, which... Read more
Continued PC Shipment Shrinkage Expected Thro...
Worldwide PC shipments are expected to fall by -8.7 percent in 2015 and not stabilize until 2017, according to the latest International Data Corporation (IDC) Worldwide Quarterly PC Tracker data. The... Read more
Imminent iPhone 6s Announcement Leads To 103%...
NextWorth Solutions, with its online and in-store electronics trade-in programs including http://NextWorth.com, reports that it has experienced a 103 percent surge in quoted trade-in values over the... Read more

Jobs Board

*Apple* Retail Online Store: Customer Insigh...
**Job Summary** Apple Retail (Online Store) is seeking an experienced e-commerce analytics professional to join the Customer Insights Team. The Web e-Commerce Analyst Read more
*Apple* Music, Business Operations - Apple I...
Changing the world is all in a day039s work at Apple . If you love innovation, here039s your chance to make a career of it. You039ll work hard. But the job comes with Read more
WW *Apple* Retail Online Store: Customer In...
**Job Summary** The Apple Retail - Online Store is seeking an experienced web merchandising analytics professional to join the Customer Insights Team. The Web Read more
Senior Payments Security Manager - *Apple*...
**Job Summary** Apple , Inc. is looking for a highly motivated, innovative and hands-on senior payments security manager to join the Apple Pay security team. You will Read more
Software QA Engineer, *Apple* Pay Security...
**Job Summary** Imagine what you could do here. At Apple , great ideas have a way of becoming great products, services, and customer experiences very quickly. Bring Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.