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

Dash 4.0.0 - Instant search and offline...
Dash is an API documentation browser and code snippet manager. Dash helps you store snippets of code, as well as instantly search and browse documentation for almost any API you might use (for a full... Read more
Bookends 12.7.9 - Reference management a...
Bookends is a full-featured bibliography/reference and information-management system for students and professionals. Bookends uses the cloud to sync reference libraries on all the Macs you use.... Read more
TrailRunner 3.8.834 - Route planning for...
TrailRunner is the perfect companion for runners, bikers, hikers, and all people wandering under the sky. Plan routes on a geographical map. Import GPS or workout recordings and journalize your... Read more
iFFmpeg 6.2.5 - Convert multimedia files...
iFFmpeg is a comprehensive media tool to convert movie, audio and media files between formats. The FFmpeg command line instructions can be very hard to master/understand, so iFFmpeg does all the hard... Read more
DaisyDisk 4.4 - $9.99
DaisyDisk allows you to visualize your disk usage and free up disk space by quickly finding and deleting big unused files. The program scans your disk and displays its content as a sector diagram... Read more
iFFmpeg 6.2.5 - Convert multimedia files...
iFFmpeg is a comprehensive media tool to convert movie, audio and media files between formats. The FFmpeg command line instructions can be very hard to master/understand, so iFFmpeg does all the hard... Read more
DaisyDisk 4.4 - $9.99
DaisyDisk allows you to visualize your disk usage and free up disk space by quickly finding and deleting big unused files. The program scans your disk and displays its content as a sector diagram... Read more
BetterTouchTool 2.07 - Customize Multi-T...
BetterTouchTool adds many new, fully customizable gestures to the Magic Mouse, Multi-Touch MacBook trackpad, and Magic Trackpad. These gestures are customizable: Magic Mouse: Pinch in / out (zoom... Read more
BetterTouchTool 2.071 - Customize Multi-...
BetterTouchTool adds many new, fully customizable gestures to the Magic Mouse, Multi-Touch MacBook trackpad, and Magic Trackpad. These gestures are customizable: Magic Mouse: Pinch in / out (zoom... Read more
BetterTouchTool 2.07 - Customize Multi-T...
BetterTouchTool adds many new, fully customizable gestures to the Magic Mouse, Multi-Touch MacBook trackpad, and Magic Trackpad. These gestures are customizable: Magic Mouse: Pinch in / out (zoom... Read more

RPG Djinn Caster (Games)
RPG Djinn Caster 1.0.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0.0 (iTunes) Description: SPECIAL PRICE 38% OFF(USD 7.99 -> USD 4.99)!!!A Fantasy Action RPG of far foreign lands! Summon the Djinns and rise to... | Read more »
Alto's Odyssey gets its first trail...
There's finally video evidence of Alto's Odyssey, the follow up to the 2015 App Store hit, Alto's Adventure. It looks just as soothing and atmospheric as Alto's last outing, but this time players will be journeying to the desert. Whereas Alto's... | Read more »
Last week on Pocket Gamer
What’s going on in the wider world of portable gaming? Each week we ask that question of our sister website Pocket Gamer. The PG team covers iOS gaming, just like 148Apps, but it also strays into the world of Android games and handheld consoles... | Read more »
Pokémon GO Generation 2 evolution guide
At long last, Niantic Labs finally unleashed the Generation 2 Pokémon into the wild. Pokémon GO trainers are scrambling to grab up this new set of 80 Pokémon. There are some special new tricks required to catch all of these new beasties, though.... | Read more »
The best new games we played this week
It feels as though the New Year got off to a creaking start as far as mobile games go, but that's changed over the past few weeks. The last few days alone have seen the debut of a number of wonderful games, so we thought we'd take the time to... | Read more »
Recruit more scallywags and discover new...
Get ready to show off your sea legs all over again in Oceans & Empires’ new grand update, which aims to make the act of rising to the role of seven seas ruler even more fresh and appealing, thanks to a richness of new content on both iOS and... | Read more »
Mage the Ascension: Refuge (Games)
Mage the Ascension: Refuge 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: The groundbreaking roleplaying game Mage: The Ascension manifests in our turbulent present with Refuge, an... | Read more »
Vampire: Prelude (Games)
Vampire: Prelude 1.0 Device: iOS Universal Category: Games Price: $4.99, Version: 1.0 (iTunes) Description: The classic roleplaying game Vampire: The Masquerade returns to digital games with a Prelude of things to come. Experience a... | Read more »
Digby Forever Guide: How to dig to the d...
Digby Forever is a sparkling homage to arcade classics, and while you may be tiring of the number of arcade games being thrown at you, this endless digger finds many ways to stand out from the rest of the pack. The game manages to be challenging... | Read more »
The best sales on the App Store this wee...
It's been quite the week in mobile games, but if the latest releases(there were some pretty darn good ones, in case you missed out) aren't really doing the trick, perhaps some of these discounted games will. Many of these premium games had their... | Read more »

Price Scanner via MacPrices.net

13-inch 2.0GHz Space Gray MacBook Pro on sale...
Adorama has the non-Touch Bar 13″ 2.0GHz Space Gray MacBook Pro in stock today for $100 off MSRP. Shipping is free, and Adorama charges NY & NJ sales tax only: - 13″ 2.0GHz MacBook Pro Space Gray... Read more
13-inch Touch Bar MacBook Pros on sale for $1...
B&H Photo has select 2016 Apple 13″ Touch Bar MacBook Pros in stock today and up to $100 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 13″ 2.9GHz/512GB Touch Bar MacBook... Read more
KSI-1802R SX Disinfect-able Keyboard With Wav...
KSI has unveiled a new, innovative medical keyboard, the KSI-1802R SX, at HIMSS 2017, running February 19-22 in Orlando, Florida. KSI-1802R SX is the only keyboard that combines dual factor... Read more
15-inch Touch Bar MacBook Pros on sale for up...
Amazon has 15″ Touch Bar MacBook Pros in stock today and on sale for up to $150 off MSRP including free shipping: - 15″ 2.7GHz Touch Bar MacBook Pro Space Gray: $2720.38 $79 off MSRP - 15″ 2.7GHz... Read more
Apple’s Education discount saves up to $300 o...
Purchase a new Mac or iPad using Apple’s Education Store and take up to $300 off MSRP. All teachers, students, and staff of any educational institution qualify for the discount. Shipping is free: -... Read more
13-inch 1.6GHz/256GB MacBook Air on sale for...
Newegg has the 13″ 1.6GHz/256GB MacBook Air (MMGG2LL/A) on sale for $1029.99 including free shipping. Their price is $170 off MSRP, and it’s the lowest price available for this model. Choose Newegg... Read more
Save up to $600 with Apple refurbished Mac Pr...
Apple has Certified Refurbished Mac Pros available for up to $600 off the cost of new models. An Apple one-year warranty is included with each Mac Pro, and shipping is free. The following... Read more
12-inch 1.1GHz Retina MacBooks on sale for $1...
B&H has 12″ 1.1GHz Retina MacBooks on sale for $150 off MSRP. Shipping is free, and B&H charges NY sales tax only: - 12″ 1.1GHz Space Gray Retina MacBook: $1149 $150 off MSRP - 12″ 1.1GHz... Read more
InTouch Health Expands iOS And Windows Produc...
Specialty telehealth enterprise provider InTouch Health has announced an expanded range of FDA Class I listed medical devices and software solutions for ambulatory, non-acute and non-emergent... Read more
iMobie Airs World’s 1st iCloud Manager with M...
iMobie Inc., an Apple-related software company, announced their newly-updated iPhone manager AnyTrans with exclusive feature to sync and manage contents across multiple iCloud accounts. With it,... Read more

Jobs Board

*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Manager *Apple* Systems Administration - Pu...
Req ID 3315BR Position Title Manager, Apple Systems Administration Job Description The Manager of Apple Systems Administration oversees the administration and Read more
*Apple* Technician - nfrastructure (United S...
Let’s Work Together Apple Technician This position is based in Portland, ME Life at nfrastructure At nfrastructure, we understand that our success results from our Read more
*Apple* Retail - Multiple Positions - Apple,...
Job Description: Sales Specialist - Retail Customer Service and Sales Transform Apple Store visitors into loyal Apple customers. When customers enter the store, Read more
Manager *Apple* Systems Administration - Pu...
Req ID 3315BR Position Title Manager, Apple Systems Administration Job Description The Manager of Apple Systems Administration oversees the administration and Read more
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.