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

Latest Forum Discussions

See All

Tokkun Studio unveils alpha trailer for...
We are back on the MMORPG news train, and this time it comes from the sort of international developers Tokkun Studio. They are based in France and Japan, so it counts. Anyway, semantics aside, they have released an alpha trailer for the upcoming... | Read more »
Win a host of exclusive in-game Honor of...
To celebrate its latest Jujutsu Kaisen crossover event, Honor of Kings is offering a bounty of login and achievement rewards kicking off the holiday season early. [Read more] | Read more »
Miraibo GO comes out swinging hard as it...
Having just launched what feels like yesterday, Dreamcube Studio is wasting no time adding events to their open-world survival Miraibo GO. Abyssal Souls arrives relatively in time for the spooky season and brings with it horrifying new partners to... | Read more »
Ditch the heavy binders and high price t...
As fun as the real-world equivalent and the very old Game Boy version are, the Pokemon Trading Card games have historically been received poorly on mobile. It is a very strange and confusing trend, but one that The Pokemon Company is determined to... | Read more »
Peace amongst mobile gamers is now shatt...
Some of the crazy folk tales from gaming have undoubtedly come from the EVE universe. Stories of spying, betrayal, and epic battles have entered history, and now the franchise expands as CCP Games launches EVE Galaxy Conquest, a free-to-play 4x... | Read more »
Lord of Nazarick, the turn-based RPG bas...
Crunchyroll and A PLUS JAPAN have just confirmed that Lord of Nazarick, their turn-based RPG based on the popular OVERLORD anime, is now available for iOS and Android. Starting today at 2PM CET, fans can download the game from Google Play and the... | Read more »
Digital Extremes' recent Devstream...
If you are anything like me you are impatiently waiting for Warframe: 1999 whilst simultaneously cursing the fact Excalibur Prime is permanently Vault locked. To keep us fed during our wait, Digital Extremes hosted a Double Devstream to dish out a... | Read more »
The Frozen Canvas adds a splash of colou...
It is time to grab your gloves and layer up, as Torchlight: Infinite is diving into the frozen tundra in its sixth season. The Frozen Canvas is a colourful new update that brings a stylish flair to the Netherrealm and puts creativity in the... | Read more »
Back When AOL WAS the Internet – The Tou...
In Episode 606 of The TouchArcade Show we kick things off talking about my plans for this weekend, which has resulted in this week’s show being a bit shorter than normal. We also go over some more updates on our Patreon situation, which has been... | Read more »
Creative Assembly's latest mobile p...
The Total War series has been slowly trickling onto mobile, which is a fantastic thing because most, if not all, of them are incredibly great fun. Creative Assembly's latest to get the Feral Interactive treatment into portable form is Total War:... | Read more »

Price Scanner via MacPrices.net

Early Black Friday Deal: Apple’s newly upgrad...
Amazon has Apple 13″ MacBook Airs with M2 CPUs and 16GB of RAM on early Black Friday sale for $200 off MSRP, only $799. Their prices are the lowest currently available for these newly upgraded 13″ M2... Read more
13-inch 8GB M2 MacBook Airs for $749, $250 of...
Best Buy has Apple 13″ MacBook Airs with M2 CPUs and 8GB of RAM in stock and on sale on their online store for $250 off MSRP. Prices start at $749. Their prices are the lowest currently available for... Read more
Amazon is offering an early Black Friday $100...
Amazon is offering early Black Friday discounts on Apple’s new 2024 WiFi iPad minis ranging up to $100 off MSRP, each with free shipping. These are the lowest prices available for new minis anywhere... Read more
Price Drop! Clearance 14-inch M3 MacBook Pros...
Best Buy is offering a $500 discount on clearance 14″ M3 MacBook Pros on their online store this week with prices available starting at only $1099. Prices valid for online orders only, in-store... Read more
Apple AirPods Pro with USB-C on early Black F...
A couple of Apple retailers are offering $70 (28%) discounts on Apple’s AirPods Pro with USB-C (and hearing aid capabilities) this weekend. These are early AirPods Black Friday discounts if you’re... Read more
Price drop! 13-inch M3 MacBook Airs now avail...
With yesterday’s across-the-board MacBook Air upgrade to 16GB of RAM standard, Apple has dropped prices on clearance 13″ 8GB M3 MacBook Airs, Certified Refurbished, to a new low starting at only $829... Read more
Price drop! Apple 15-inch M3 MacBook Airs now...
With yesterday’s release of 15-inch M3 MacBook Airs with 16GB of RAM standard, Apple has dropped prices on clearance Certified Refurbished 15″ 8GB M3 MacBook Airs to a new low starting at only $999.... Read more
Apple has clearance 15-inch M2 MacBook Airs a...
Apple has clearance, Certified Refurbished, 15″ M2 MacBook Airs now available starting at $929 and ranging up to $410 off original MSRP. These are the cheapest 15″ MacBook Airs for sale today at... Read more
Apple drops prices on 13-inch M2 MacBook Airs...
Apple has dropped prices on 13″ M2 MacBook Airs to a new low of only $749 in their Certified Refurbished store. These are the cheapest M2-powered MacBooks for sale at Apple. Apple’s one-year warranty... Read more
Clearance 13-inch M1 MacBook Airs available a...
Apple has clearance 13″ M1 MacBook Airs, Certified Refurbished, now available for $679 for 8-Core CPU/7-Core GPU/256GB models. Apple’s one-year warranty is included, shipping is free, and each... Read more

Jobs Board

Seasonal Cashier - *Apple* Blossom Mall - J...
Seasonal Cashier - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Read more
Seasonal Fine Jewelry Commission Associate -...
…Fine Jewelry Commission Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) Read more
Seasonal Operations Associate - *Apple* Blo...
Seasonal Operations Associate - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Read more
Hair Stylist - *Apple* Blossom Mall - JCPen...
Hair Stylist - Apple Blossom Mall Location:Winchester, VA, United States (https://jobs.jcp.com/jobs/location/191170/winchester-va-united-states) - Apple Blossom 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
All contents are Copyright 1984-2011 by Xplain Corporation. All rights reserved. Theme designed by Icreon.