The Cocoanuts
Volume Number: 18 (2002)
Issue Number: 12
Column Tag: QuickTime Toolkit
The Cocoanuts
Developing QuickTime Applications with Cocoa
by Tim Monroe
Introduction
Up to now in this series of articles, we've focused on using the QuickTime APIs to accomplish various tasks involving movies and images. We've seen how to play movies back in windows on the screen, edit movies, create movies, and apply video effects to movies and images. We've worked with a large variety of media types, including video, text, timecodes, sprites, skins, QuickTime VR, and Flash. And we've seen how to wire movies, capture movies from video and sound sources, broadcast movies to the Internet, and play movies fullscreen. Maybe it's time to take a small break from this feature blitz and turn our attention elsewhere for a little while.
As you know, we've done all our work in the C programming language, using Metrowerks CodeWarrior (on Macintosh) and Microsoft Developer Studio (on Windows). In this article and the next several articles, I want to take a look at some of the other languages and programming environments that we can use to develop applications that use QuickTime. To that end, I've selected a handful of alternatives to the CodeWarrior and Developer Studio environments, including PowerPlant, REALbasic, and a few others. All of these alternative development environments offer some support for QuickTime, usually by allowing us to attach some sort of movie object to a window or pane. (Yes, thankfully, almost all of these alternatives are object-oriented.)
My primary goal is to see how to develop a basic multi-window movie playback and editing application using these other languages or IDEs. In other words, I want to see how to duplicate, as much as possible, the capabilities of our C-based application QTShell. I also want to see how easy it is, using each of these languages or development environments, to move beyond the basics. Movie playback and cut-and-paste editing might be easy to implement, but it might also be considerably harder to extend our applications to include more advanced capabilities. Time, as they say, will tell.
On the first leg of this little detour, we'll take a look at the QuickTime support offered by Cocoa, Apple's object-oriented framework for creating applications that run on Mac OS X. Cocoa supports development using either Objective-C or Java, but here we'll work exclusively with Objective-C. We'll develop an application -- called MooVeez -- that can open QuickTime movie files and display their movies in windows on the screen. MooVeez will also allow the user to interact with the movie in the standard ways and to perform basic movie editing on any open movies. Figure 1 shows a movie window displayed by MooVeez.
Figure 1: A movie window displayed by MooVeez
You'll notice that the movie does not completely fill the movie window. Part of the reason for this is to prevent the window's size box, which is provided automatically by the Cocoa framework, from overlapping the right side of the movie controller bar. More importantly, I wanted to have room on the right side of the movie window for a button (the small triangle in Figure 1); when the user clicks that button, a drawer slides out to the right displaying some basic information about the movie, as shown in Figure 2.
Figure 2: A movie information drawer
We'll begin by taking a quick look at Cocoa and Objective-C, and then we'll modify Cocoa's standard multi-document application template to support QuickTime movies. Cocoa includes two classes, NSMovie and NSMovieView, that provide an object-oriented representation of QuickTime movies and a means of displaying movies in Cocoa windows. Getting MooVeez to display windows like the one in Figure 1 is largely a matter of adding an instance of the NSMovieView class to a standard document window. We'll need some code to set the size of the document window appropriately and to make sure the Edit and File menu items work correctly. And we'll need a bit more code when we add the movie info drawer to the basic movie window. But all in all, the amount of code we have to write for all this will be fairly small.
Toward the end of this article, we'll take a look at several ways of modifying the default capabilities of NSMovie and NSMovieView. We'll see how to load a movie asynchronously into an NSMovieView instance. Then we'll see how to subclass the NSMovieView class to override some of its behaviors. Finally, we'll see how to use an Objective-C feature called a category to add instance methods to the NSMovieView class.
Cocoa and Objective-C Overview
Cocoa is an application development environment and runtime system introduced in 1987 by NeXT Inc. and subsequently moved to Mac OS X when NeXT was absorbed by Apple. Cocoa was originally called "NeXTSTEP" (whence the "NS" in class names like "NSMovie") and was later renamed "OpenStep". Fundamentally, Cocoa is a large set of software components, or classes, that developers can use to build applications. These components are organized into several frameworks:
- The Foundation framework provides classes for basic application execution and data manipulation. It provides classes for managing operating system services like run loops (which retrieve events from the user and dispatch them to the appropriate handler), interprocess communication, file manipulation, memory allocation and deallocation, timers, asset locking, and application threading. It also provides classes for working with strings, dates, URLs, and several kinds of data collections like arrays and dictionaries (which are essentially associative arrays).
- The Application framework (also called the Application Kit) provides classes for implementing the graphical user interface of an application. It includes classes for displaying and communicating with windows, panels, controls (buttons, sliders, check boxes, and so forth), and other graphical objects (such as images, colors, and fonts). The Application framework also provides services for managing document-based applications, such as opening and saving document data stored in files, copying and pasting data among documents, undoing and redoing document changes, and printing.
- The Application Scripting framework provides support for managing AppleEvent-based scripting for an application.
Virtually all Cocoa classes inherit ultimately from a single root class called NSObject, which provides its inheriting classes methods for allocating, deallocating, copying, comparing, and archiving objects. NSObject also provides methods for querying an object about its capabilities and position in the class hierarchy.
As mentioned above, we're going to access Cocoa's capabilities using Objective-C. Objective-C is a simple extension to the C programming language that allows for arranging objects into a class hierarchy. It specifies a syntax for creating new classes by subclassing existing classes. It also provides a distinctive way to execute an object's methods by sending it messages. For example, if aMovieObject is an instance of a movie object, then we could instruct it to set its volume to the current system volume with this line of code:
[aMovieObject setVolume: 1.0];
The value 1.0 is a parameter to the setVolume method. Parameters are always introduced by the colon (:), and there may be multiple parameters for some messages. As you can see, message expressions are enclosed in brackets.
Objects can return values to the message sender. For example, we can get a movie object's current volume like this:
curVolume = [aMovieObject volume];
And we can nest message calls; here's a typical example that halves the current volume:
[aMovieObject setVolume: [aMovieObject volume] / 2];
Here's another example:
[[aMovieObject window] setTitle: [self
lastComponentOfFileName]];
This line of code sets the title of the window containing the movie object to the last component of the filename of the current document (here self refers to the current document -- that is, the object issuing the message). The lastComponentOfFileName message returns an object of type NSString, which is what setTitle: expects.
It's easy to subclass an existing class. We need to provide an interface for the class, which declares the instance variables and methods for the class. We also need to provide an implementation of the class, which provides the code for the methods. Listing 1 shows the declaration of the class MyDocument, which is a subclass of the Cocoa class NSDocument.
Listing 1: Declaring a document subclass
MyDocument.h
@interface MyDocument : NSDocument
{
IBOutlet id _movieView;
IBOutlet id _drawer;
IBOutlet id _timeDisplay;
IBOutlet id _duration;
IBOutlet id _normalSize;
IBOutlet id _currentSize;
IBOutlet id _sourceName;
short _fileRefNum;
short _fileResNum;
}
- (void)initializeMovieWindowFromFile:(BOOL)isReverting;
- (IBAction)selectNone:(id)sender;
- (IBAction)toggleDrawer:(id)sender;
- (id)drawer;
- (NSString *)timeStringFromLong:(long)value;
- (void)setTimeDisplay;
- (void)setDuration;
- (void)setNormalSize;
- (void)setCurrentSize;
- (void)setSource: (NSString *)name;
@end
The first group of items (all of type IBOutlet id) specifies the instance variables of our custom document class; in this case, these are mostly references to the user-interface elements in the movie document window and its associated info drawer. (See Figure 2 again.) The second group lists our instance methods; each of these methods needs to be implemented, as illustrated in Listing 2 for the drawer method.
Listing 2: Getting the drawer identifier
drawer
- (id)drawer
{
return _drawer;
}
The drawer method simply returns the current value of the _drawer instance variable. This method is useful mainly to objects other than the document object (since it could simply reference the _drawer instance variable directly). Later we'll see why we need this method at all.
In Objective-C, classes themselves are objects too, and we can send messages to class objects. Indeed, that's one way we can create instances of Cocoa classes, by sending the alloc class method to the class object we want to instantiate. Here's how we might create a new empty string:
NSMutableString *string = [NSMutableString alloc];
(NSMutableString is the type of a string that we can alter at runtime; for unchanging strings, we would use the NSString class.)
Objects have a reference count (or retain count) that indicates how many connections exist to that object. When an object is first allocated, its reference count is 1. Certain operations on the object cause the reference count to be incremented, such as copying the object or adding it to an array. We can also explicitly increment the reference count by sending the retain message to the object. We can decrement an object's reference count by sending it the release message, like this:
[string release];
When an object's reference count falls to 0, the object will be destroyed. Prior to destruction, the object receives a dealloc message, in response to which it can free up any memory it may have allocated or release any objects it may have created.
The Project File
We could spend lots more time investigating Cocoa and Objective-C, but our focus here is to build a movie-playing application. So let's get cracking. The IDE we'll use to weave Cocoa classes into our application consists of two applications, called Project Builder and Interface Builder. Project Builder is the basic development environment; it keeps track of the source code, resources, and frameworks used by our application. Project Builder also provides capabilities for source code editing and navigation, compiling and linking, and source-level debugging.
Interface Builder is a tool for creating and editing nib files, which are packages of user interface objects and other resources that define the basic look-and-feel of an application. Our MooVeez project will contain three nib files: one that defines the application menu, one that defines the appearance of a document window, and one that defines the appearance of the application's About box. The first two nib files were already present in the project when it was created; I added the third nib file to support a custom About box, shown in Figure 3.
Figure 3: MooVeez' About box
Creating a New Project
Let's jump right in and create a new project for our application. Launch Project Builder; select "New Project..." from the File menu and then select "Cocoa Document-based Application", as shown in Figure 4.
Figure 4: Creating a new multi-document project
The Assistant then prompts us for a project name and location; let's call our project "MooVeez" and save it in our home directory. Project Builder displays the project window shown in Figure 5. (I've opened all the disclosure triangles, to reveal the contents of each group.)
Figure 5: The MooVeez project window
The "Product" -- that is, our application -- is shown in red because it hasn't been built yet. We can build the application by selecting Build (Command-B) from the Build menu. And we can run the application by selecting Run (Command-R). If we do that, we'll see the document window shown in Figure 6.
Figure 6: The default document window
That's certainly not what we want; remember, we want to display and manage QuickTime movies in this window, not a short text string. But before we undertake to fix this, let's poke around the MooVeez project file a moment. As you can see, there are two files that declare and implement a single class (MyDocument.h and MyDocument.m) and another file main.m. (Objective-C source code files use the .m file type suffix.) The contents of main.m are quite simple (Listing 3).
Listing 3: Running the application
main
#import <Cocoa/Cocoa.h>
int main(int argc, const char *argv[])
{
return NSApplicationMain(argc, argv);
}
The NSApplication class provides essential application execution services; its primary service is to retrieve events from the Window Server (the part of the Cocoa runtime environment that handles compositing windows on the screen and dispatching events in those windows) and send them to the appropriate target. The function NSApplicationMain is a convenience function that creates a new instance of the NSApplication class, loads the main nib file, and then sets the application to run. For present purposes, we won't need to alter main.m.
The files MyDocument.h and MyDocument.m implement our custom document class. As initially created by Project Builder, they are pretty bare-bones. Listing 4 shows the original version of MyDocument.h.
Listing 4: Declaring the default document class
MyDocument.h
#import <Cocoa/Cocoa.h>
@interface MyDocument : NSDocument
{
}
@end
This just says that the MyDocument class is a subclass of the NSDocument class. As you've already seen (in Listing 1), we're soon going to add some instance variables and methods to this class.
Listing 5 shows the original version of MyDocument.m (with comments removed to save space).
Listing 5: Implementing the default document class
MyDocument.m
#import "MyDocument.h"
@implementation MyDocument
- (id)init
{
[super init];
if (self) {
}
return self;
}
- (NSString *)windowNibName
{
return @"MyDocument";
}
- (void)windowControllerDidLoadNib:
(NSWindowController *) aController
{
[super windowControllerDidLoadNib:aController];
}
- (NSData *)dataRepresentationOfType:(NSString *)aType
{
return nil;
}
- (BOOL)loadDataRepresentation:(NSData *)data
ofType:(NSString *)aType
{
return YES;
}
@end
So MyDocument.m simply provides placeholders for us to override five NSDocument methods. Only one of these, windowNibName, currently does any real work; it returns the name of the nib file that defines the appearance of a document window.
Modifying the Document Window Appearance
As we saw above, the default document simply displays a text message when it's opened. We want the document windows in MooVeez to display QuickTime movies. So let's open MyDocument.nib with Interface Builder. We can do this by double-clicking the entry in the project window. Figure 7 shows the main window for this nib file.
Figure 7: The document nib file
If we select the icon labeled "Window" and then choose the "Show Info" menu item in the Tools menu, we'll see the window shown in Figure 8.
Figure 8: Specifying the document attributes
For reasons that will become clear later, we want to uncheck the "Deferred" attribute. Now let's select the Size item in the pop-up menu; we'll see the window in Figure 9. Change the sizes to the specified values.
Figure 9: Specifying the document size
Now let's remove the text from the main document window and replace it with a QuickTime view. Select the GraphicsViews icon in the toolbar, as in Figure 10.
Figure 10: The Graphics Views palette
Drag a QuickTime icon from the palette into the document window and resize it appropriately, as shown in Figure 11.
Figure 11: The revised document window
Now, we need a way for our custom document class (MyDocument) to keep track of this new object that we've added to the document window. We do this by adding an outlet to the document class. An outlet is essentially just an instance variable that holds a reference to some object. We'll add the instance variable to our class declaration in a moment, but we also need to add it to our nib file. Click on the Classes tab in the MyDocument.nib window and navigate to the MyDocument class, as shown in Figure 12.
Figure 12: The Class hierarchy window
Then select the "Add Outlet to MyDocument" menu item in the Classes menu. Interface Builder will display the Class Info window shown in Figure 13.
Figure 13: The Class Info window
Change the default outlet name "myOutlet" to "_movieView".
So far, we've just created a new outlet for the document class. Now we need to link the outlet with the movie view we added to the document window. Click on the Instances tab in the MyDocument.nib window; then hold down the control key, click on the File's Owner icon, and drag until the cursor is over the movie view in the document window. When we release the mouse button, Interface Builder displays the window shown in Figure 14.
Figure 14: Connections of the File's Owner
Click the Connect button to make the desired connection. For the moment, we are done modifying the nib file. Let's save our work and quit Interface Builder.
Cocoa's Movie Classes
The nib file contains archived (or serialized) versions of actual Cocoa objects. Our nib file currently contains an archived window object, of type NSWindow. This window object has a content view, which is of type NSView. A view object provides methods for drawing items on the screen, handling events related to those items, and printing those items. When we dragged the movie view into the window, it was automatically made a subview of the window's content view. This movie view is of type NSMovieView (as you probably guessed from Figure 11).
NSMovieView is a subclass of NSView. It is responsible for drawing QuickTime movies and their associated movie controller bars. NSMovieView represents the QuickTime movie as an object of type NSMovie, which is a very thin wrapper for a QuickTime Movie. NSMovie is a subclass of the root class, NSObject.
It's important to note that our document nib file contains an instance of NSMovieView, but that movie view does not yet have an NSMovie associated with it. We need to explicitly assign an NSMovie instance to the NSMovieView instance at run time, when a document window is opened by the application. Let's see how to do that, and how to work with these two Cocoa classes.
Setting Openable Document Types
First of all, we need to tell our application what kinds of files it can open and hence what kinds of files should be selectable in the file-opening dialog box (displayed in response to the Open menu item). To do this, let's select "Edit Active Target" in the Project menu in Project Builder. Click the "Document Types" item on the left-hand side and add the desired document types. Figure 15 shows our document types settings. We want MooVeez to be able to open QuickTime movie files and Flash files.
Figure 15: Specifying openable document types
Opening Movie Files
When the user selects a file in the file-opening dialog box, our multi-document application creates a new document window, based on the information in the nib file. When it has loaded the nib and initialized all the objects specified therein, it calls our document's windowControllerDidLoadNib method. This is where we want to assign the movie to the movie view.
By the time windowControllerDidLoadNib is called, an instance of the MyDocument class has already been created. All the instance variables, and in particular _movieView, are already set up for us. It remains only for us to do any final configuration of the movie or view before the new document window is displayed to the user.
As you know, we need to create an NSMovie object and assign it to the movie view. NSMovie provides the initWithURL:byReference: method, which initializes a movie object with a QuickTime movie specified by a URL. We can create a URL for the file the user selected by invoking NSURL's initFileURLWithPath: method, like this:
NSURL *fileUrl;
NSMovie *movie;
fileUrl = [[NSURL alloc] initFileURLWithPath: [self
fileName]];
movie = [[NSMovie alloc] initWithURL: fileUrl byReference:
NO];
There is an important drawback to opening our movies like this, however. NSMovie opens a movie file with read-only access; worse yet, even if NSMovie did open movie files with read-write access, it doesn't provide any way for us to get the file reference number of the opened movie file or the resource ID of the opened movie resource. But we need these pieces of data if we want to call QuickTime functions like UpdateMovieResource.
So it looks like we need to open the movie file ourselves and then initialize our NSMovie instance using its initWithMovie: method. Listing 6 shows the code we can use to do that.
Listing 6: Opening a movie file
initializeMovieWindowFromFile
FSRef fileRef;
FSSpec fileSpec;
short fileRefNum = 0;
short fileResNum = 0;
OSErr err = noErr;
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);
movie = [[NSMovie alloc] initWithMovie: qtMovie];
_fileRefNum = fileRefNum;
_fileResNum = fileResNum;
Notice that we're saving the file reference number and the resource ID in the instance variables _fileRefNum and _fileResNum.
Eventually, we'll set movie as the NSMovie associated with the NSMovieView, like this:
[_movieView setMovie: movie];
Before we do that, however, we want to resize the document window so that it fits the movie and movie controller, with the small border around the movie view (as in Figure 1). To do this, we need to find the movie's natural size. Since the movie has just been loaded from its file, we can use the QuickTime function GetMovieBox. To make this call, however, we need an identifier of type Movie; happily, NSMovie has an instance method that returns this identifier:
Movie qtMovie = [movie QTMovie];
Now we can calculate the appropriate size for the document window and set it, as shown in Listing 7.
Listing 7: Setting a document's size
initializeMovieWindowFromFile
GetMovieBox(qtMovie, &movieBox);
size.width = (float)(movieBox.right - movieBox.left);
size.height = (float)(movieBox.bottom - movieBox.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;
[[_movieView window] setContentSize: size];
Before we display the movie window to the user, we need to take care of two additional tasks. First, we want to set the window title, using this line of code:
[[_movieView window] setTitle: [self
lastComponentOfFileName]];
Second, we want to install a movie controller action filter procedure. We'll do that like so:
mc = [_movieView movieController];
if (mc) {
MCActionFilterWithRefConUPP upp =
NewMCActionFilterWithRefConUPP(MyActionFilter);
MCSetActionFilterWithRefCon(mc, upp, (long)self);
DisposeMCActionFilterWithRefConUPP(upp);
}
Notice that NSMovieView supports the movieController method, which returns the identifier of the movie controller associated with the specified movie. The document window must be marked as non-deferred (in the Interface Builder info panel) or else the movieController method will always return nil inside of windowControllerDidLoadNib. (There are other ways around this while keeping the window deferred, but the current strategy is the simplest, I think.)
Notice also that we are passing the value self as the reference constant when we call MCSetActionFilterWithRefCon; this gives us access to the document instance inside of the movie controller filter procedure. This filter procedure can do any required processing; for the present, we'll use it principally to update the "Current Time" display in the movie info drawer. More on that later.
Listing 8 shows the complete definition of initializeMovieWindowFromFile.
Listing 8: Loading a movie from a file
initializeMovieWindowFromFile
- (void)initializeMovieWindowFromFile:(BOOL)isReverting
{
NSMovie *movie;
NSSize size;
Movie qtMovie = nil;
MovieController mc = nil;
Rect movieBox;
FSRef fileRef;
FSSpec fileSpec;
short fileRefNum = 0;
short fileResNum = 0;
OSErr err = noErr;
if ([_movieView movie] != nil)
[_movieView setMovie: nil];
if (_fileRefNum != 0)
CloseMovieFile(_fileRefNum);
// 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);
movie = [[NSMovie alloc] initWithMovie: qtMovie];
_fileRefNum = fileRefNum;
_fileResNum = fileResNum;
// set size of the document window
GetMovieBox(qtMovie, &movieBox);
size.width = (float)(movieBox.right - movieBox.left);
size.height = (float)(movieBox.bottom - movieBox.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;
[[_movieView window] setContentSize: size];
// set the movie view's movie
[_movieView setMovie: movie];
// set the document title
[[_movieView window] setTitle: [self
lastComponentOfFileName]];
// install a movie controller action filter procedure
mc = [_movieView movieController];
if (mc) {
MCActionFilterWithRefConUPP upp =
NewMCActionFilterWithRefConUPP(MyActionFilter);
MCSetActionFilterWithRefCon(mc, upp, (long)self);
DisposeMCActionFilterWithRefConUPP(upp);
}
}
We call initializeMovieWindowFromFile in our windowControllerDidLoadNib method, as shown in Listing 9.
Listing 9: Configuring a document's movie
windowControllerDidLoadNib
- (void)windowControllerDidLoadNib:
(NSWindowController *) aController
{
[super windowControllerDidLoadNib: aController];
[self initializeMovieWindowFromFile: NO];
}
NSMovie calls EnterMovies internally, when it's handling the initWithURL:byReference: and initWithMovie: methods. However, MooVeez makes direct calls to QuickTime APIs (namely, OpenMovieFile and NewMovieFromFile) before it calls initWithMovie:. So we need to call EnterMovies ourselves. Accordingly, I've reworked the applicationShouldOpenUntitledFile method in the AppController.c file, as shown in Listing 10.
Listing 10: Handling the applicationShouldOpenUntitledFile method
applicationShouldOpenUntitledFile
- (BOOL)applicationShouldOpenUntitledFile:
(NSApplication *)sender
{
static BOOL initedQT = NO;
if (!initedQT) {
EnterMovies();
initedQT = YES;
}
return NO;
}
The applicationShouldOpenUntitledFile method is sent to our application immediately before an untitled document is opened. The multi-document framework will want to do that immediately upon application launch, so that's a reasonable time to initialize QuickTime. (Note, by the way, that we return NO in order to prevent Cocoa from opening a new empty document; this violates the Aqua human interface guidelines, but for the moment MooVeez does not support empty documents.)
Calling EnterMovies ourselves also helps work around a bug in NSMovie. It turns out that NSMovie calls EnterMovies in all cases except one, namely when we call initWithURL:byReference: with a remote URL. In that case, if we haven't already called EnterMovies, the attempt to load the remote movie will fail (at least on the first such attempt). As we see, however, it's easy to work around this issue.
One final point before we can build and run our application: we need to add the QuickTime framework to our project. To do this, we select "Add Frameworks..." in Project Builder's Project menu and then choose the QuickTime framework (it's in System/Library/Frameworks). Figure 16 shows our updated project file.
Figure 16: The project file with the QuickTime framework
Movie Playback
So far, we've created a project based on the multi-document Cocoa application template and reworked the document nib file so that a document window contains a movie view (of type NSMovieView). And we've adapted the windowControllerDidLoadNib method of our custom document subclass to set the NSMovie associated with that movie view and to perform other necessary movie, movie controller, and window configuration. At this point, we've got a working Cocoa application that opens and displays QuickTime movies. The movie view handles all user interaction with the movie, such a clicks on buttons in the controller bar, dragging the thumb in the controller bar, and any standard keyboard equivalents. In addition, NSMovieView takes care of tasking the QuickTime movie at appropriate intervals. We don't need to install any timers or event callbacks to make the movies play correctly.
Since we used the multi-document template, MooVeez provides a number of document-related capabilities without any extra programming. For instance, our application's menu bar includes a Window menu that allows us to select from among open movies and to minimize the current movie window. Also, the File menu contains an "Open Recent" menu item (Figure 17) from which the user can select a recently-opened movie file. And of course we get the standard window-related behaviors (minimizing, maximizing, moving, resizing, and so forth) all absolutely "for free". NSDocument handles all of that for us.
Figure 17: The Open Recent menu item
Still, there are a few refinements we might want to make. For one thing, we certainly want to add our own menus and menu items. As you can see in Figure 17, we've added a menu called "Movie" to the menu bar. Currently this menu contains just one item, which shows or hides the movie controller bar. In Interface Builder, I've configured this menu item to send the toggleController: message to the first responder (that is, to the object that has focus). Listing 11 shows the method in our custom document class that handles that message.
Listing 11: Toggling the controller bar state
toggleController
- (IBAction)toggleController:(id)sender
{
if ([_movieView isControllerVisible]) {
[_movieView showController: NO adjustingSize: YES];
} else {
[_movieView showController: YES adjustingSize: YES];
}
}
Here we are using the NSMovieView method isControllerVisible to determine whether the controller bar is already visible; we're also using the showController method to set the appropriate visibility state.
We should of course set the menu item text to either "Show Controller" or "Hide Controller", depending on the current visibility state of the controller bar. To do that, we'll override NSDocument's validateMenuItem: method, as shown in Listing 12. We also enable the Save, Save As, and Select None menu items, and we enable the Revert menu item if the document has been edited.
Listing 12: Adjusting the application's menus
validateMenuItem
- (BOOL)validateMenuItem:(NSMenuItem *)item {
BOOL isValid = NO;
SEL action = [item action];
// always enable Save, Save As, and Select None
if (action == @selector(saveDocument:)) {
isValid = YES;
}
else if (action == @selector(saveDocumentAs:)) {
isValid = YES;
}
else if (action == @selector(selectNone:)) {
isValid = YES;
}
// enable Revert if the document has changed
else if (action == @selector(revertDocumentToSaved:)) {
isValid = ([self fileName] != nil) &&
[self isDocumentEdited];
}
// handle application-specific menu items
else if (action == @selector(toggleController:)) {
isValid = YES;
if ([_movieView isControllerVisible]) {
[item setTitle:@"Hide Controller"];
} else {
[item setTitle:@"Show Controller"];
}
}
return isValid;
}
I should mention that there is a mildly annoying bug in QuickTime that surfaces when we use NSMovieView to display and manage QuickTime movies in a Cocoa application. If we use any of the standard keyboard shortcuts for controlling movie playback (such as the space bar to start or stop the movie, or the left and right arrow keys to step backward and forward in the movie), we'll hear a beep. The movie controller handles the key presses correctly, but NSMovieView thinks that it hasn't done so and then passes the events to the application for processing; the application doesn't know what to do with these key presses, so it beeps. That's standard behavior for Cocoa applications.
The problem arises like this: NSMovieView calls MCIsPlayerEvent to handle the key presses. MCIsPlayerEvent is supposed to return a non-zero value if it handles an event passed to it; but for these key presses, it mistakenly returns false. That's why NSMovieView thinks that the event has not been handled and sends it elsewhere for processing. The best solution here would be to fix QuickTime, to make MCIsPlayerEvent return true for any of these key presses.
Movie Editing
Cocoa's NSMovie and NSMovieView classes work like charms to open and display QuickTime movies (with the minor exception of that annoying beep after certain key presses, which really isn't their fault at all). They are also pretty good with basic movie editing. The Cut, Copy, Paste, Clear, and Select All items in the Edit menu are handled by NSMovieView, which invokes the appropriate movie controller function (for example, MCCut to handle the Cut menu item). NSMovieView also enables and disables items in the Edit menu according to the state of the active movie. Once again, this all works pretty much automatically.
We can easily add items to the Edit menu. For instance, our C-based application framework (on which we've built applications like QTShell) adds the "Select None" item to the end of the Edit menu. To do this in MooVeez, we need to add that item to the Edit menu in the MainMenu.nib file and configure it to send the selectNone: message when the user chooses it. Our custom document class contains the definition of selectNone: shown in Listing 13.
Listing 13: Selecting none of a movie
selectNone
- (IBAction)selectNone:(id)sender
{
MovieController mc = NULL;
TimeRecord tr;
mc = (MovieController)[_movieView movieController];
if (mc != NULL) {
tr.value.hi = 0;
tr.value.lo = 0;
tr.base = 0;
tr.scale = GetMovieTimeScale([[_movieView movie]
QTMovie]);
MCDoAction(mc, mcActionSetSelectionDuration, &tr);
}
}
This function is virtually identical to the QTUtils_SelectNoneMovie function that we have called in our C-based applications.
File Manipulation
Let's turn now to consider the items in the File menu that relate to saving or discarding changes to a movie. NSDocument tracks changes to a document and "does the right thing" in various circumstances. For instance, if a user makes changes to a movie and then tries to close the movie window, NSDocument will display the sheet shown in Figure 18, asking the user to save or discard any changes.
Figure 18: The Save Changes sheet
Similarly, if the user selects Revert from the File menu, NSDocument will display the sheet shown in Figure 19.
Figure 19: The Revert Changes sheet
To take advantage of these behaviors in MooVeez, we need to override several NSDocument methods. We need to override saveDocument: so that we can call UpdateMovieResource to write the updated movie atom into an open movie file. Listing 14 shows our definition of saveDocument:.
Listing 14: Saving a document
saveDocument
- (IBAction) saveDocument: (id) sender
{
UpdateMovieResource([[_movieView movie] QTMovie],
_fileRefNum, _fileResNum, NULL);
[self updateChangeCount: NSChangeCleared];
}
Notice that we call updateChangeCount to tell NSDocument that the file has been saved.
We also want to override the dataRepresentationOfType: method, which should return an object of type NSData that holds the data that is to be written into the file (for instance, during a Save or Save As operation). Normally, dataRepresentationOfType: is called whenever data needs to be written into a file -- that is, during Save and Save As operations. But since we've overridden saveDocument:, dataRepresentationOfType: will be called only for Save As operations. Listing 15 shows our definition of dataRepresentationOfType:.
Listing 15: Returning the data in a document
dataRepresentationOfType
- (NSData *)dataRepresentationOfType:(NSString *)aType
{
// return an NSData instance that contains the data in this document
Handle movieHandle = NewHandle(0);
NSData *movieData = nil;
OSErr err = noErr;
// this creates a reference movie
if (movieHandle != NULL) {
err = PutMovieIntoHandle([[_movieView movie] QTMovie],
movieHandle);
if (err == noErr) {
HLock(movieHandle);
movieData = [NSData dataWithBytes: *movieHandle
length: GetHandleSize(movieHandle)];
HUnlock(movieHandle);
DisposeHandle(movieHandle);
}
}
return movieData;
}
The important step is the call to PutMovieIntoHandle, which writes the movie metadata (that is, the movie atom) into a block of memory. We then create an NSData object by passing the dataWithBytes:length: message to the NSData class. Since we are writing out only the movie data, a file created by invoking the Save As menu item will be a reference movie. (I'll leave it as an exercise for the reader to figure out how to write out self-contained movie files here.)
The last method we need to override is the loadDataRepresentation:ofType: method, which is called when data is being loaded from a file into a document. This is called before windowControllerDidLoadNib: is called and also when the user selects the Revert menu item. In the first case, we don't need to do anything, since windowControllerDidLoadNib: calls initializeMovieWindowFromFile: to load the data from the movie file. In the second case, when the NSMovie instance already exists, we need to call initializeMovieWindowFromFile: ourselves. Listing 16 shows our definition of loadDataRepresentation:ofType:.
Listing 16: Loading the data from a file
loadDataRepresentation
- (BOOL)loadDataRepresentation:(NSData *)data
ofType:(NSString *)aType
{
if ([_movieView movie])
[self initializeMovieWindowFromFile: YES];
return YES;
}
Application Enhancements
So, we've got a multi-document application that plays back and edits QuickTime movies pretty well. Let's now take a look at using Cocoa to add some enhancements to our MooVeez application. In particular, let's see how to add the movie information drawer we saw earlier in Figure 2 and how to load movies asynchronously.
Adding an Information Drawer
It's actually extremely simple to add a drawer to a document window. In Interface Builder, select the Windows portion of the objects palette and then drag one of the drawers onto the MyDocument.nib window. Then select the Containers portion of the objects palette and drag a custom view into that window. Control-drag from the NSDrawer instance to the Window instance and connect to the parentWindow outlet; then control-drag from the NSDrawer instance to the new view instance and connect to the contentView outlet. The drawer is now attached to the document window.
Next we'll add a button to our original document window. I used the "Rounded Bevel Button" because it allows me to specify the height and width of the button (to 10 pixel each). Some of the other button types do not appear to allow arbitrary sizing. Let's connect the button to the toggleDrawer: outlet of our custom document class. Finally, find some suitable artwork, add it to the project file, and specify the name of the art as the button's icon. Listing 17 shows MyDocument's toggleDrawer: method, which is called when the user presses the button.
Listing 17: Opening or closing the info drawer
toggleDrawer
- (IBAction)toggleDrawer:(id)sender
{
[_drawer toggle: sender];
}
All that remains, in Interface Builder, is to add some text items to the drawer content view to serve as labels and info strings. Figure 20 shows the desired layout of this pane.
Figure 20: The info drawer in Interface Builder
We also need to add some outlets to the File's Owner and make the appropriate connections; Figure 21 shows the appropriate connections.
Figure 21: The info drawer connections
Now we need a little bit of code to insert the correct information into the drawer. The normal and current size items are fairly easy to manage. Listing 18 shows our setNormalSize method, which gets the normal size of the movie (by calling GetMovieNaturalBoundsRect), constructs a suitable string of the form width x height, and then passes that string to the _normalSize text item.
Listing 18: Displaying the movie's normal size
setNormalSize
- (void)setNormalSize
{
Rect rect;
NSMutableString *sizeString = [NSMutableString string];
GetMovieNaturalBoundsRect([[_movieView movie] QTMovie],
&rect);
[sizeString appendFormat: @"%i", rect.right - rect.left];
[sizeString appendString: @" x "];
[sizeString appendFormat: @"%i", rect.bottom - rect.top];
[_normalSize setStringValue: sizeString];
}
And Listing 19 shows our setCurrentSize method; it's just like setNormalSize except that it calls the NSMovieView method movieRect to get the current size of the movie, which it passes to the _currentSize text item.
Listing 19: Displaying the movie's current size
setCurrentSize
- (void)setCurrentSize
{
NSRect rect = [_movieView movieRect];
NSMutableString *sizeString = [NSMutableString string];
if ([_movieView isControllerVisible])
rect.size.height -= kMovieControllerBarHeight;
[sizeString appendFormat: @"%.0f", rect.size.width];
[sizeString appendString: @" x "];
[sizeString appendFormat: @"%.0f", rect.size.height];
[_currentSize setStringValue: sizeString];
}
Setting the Current Time and Duration items is only slightly more complicated. We can call GetMovieTime and GetMovieDuration and then pass the returned values to our own timeStringFromLong: method, as shown in Listings 20 and 21.
Listing 20: Displaying the movie's current time
setTimeDisplay
- (void)setTimeDisplay
{
[_timeDisplay setStringValue: [self timeStringFromLong:
GetMovieTime([[_movieView movie] QTMovie], NULL)]];
}
Listing 21: Displaying the movie's duration
setDuration
- (void)setDuration
{
[_duration setStringValue: [self timeStringFromLong:
GetMovieDuration([[_movieView movie] QTMovie])]];
}
Both GetMovieTime and GetMovieDuration return a value expressed in the movie's time scale. We need to convert that value into a string of the form hours:minutes:seconds:centiseconds. Our method timeStringFromLong: (Listing 22) accomplishes this task.
Listing 22: Converting a time value to a time string
timeStringFromLong
- (NSString *)timeStringFromLong:(long)value;
{
int hours, minutes, seconds, hundredths;
// get the total number of centiseconds in the movie
hundredths = (value * kHundredthsPerSecond) / GetMovieTimeScale([[_movieView movie] QTMovie]);
// chop that number into hours, minutes, seconds, and centiseconds
hours = hundredths / kHundredthsPerHour;
if (hours > 99)
return [NSString stringWithFormat:@"(Unknown)"];
hundredths -= hours * kHundredthsPerHour;
minutes = hundredths / kHundredthsPerMinute;
hundredths -= minutes * kHundredthsPerMinute;
seconds = hundredths / kHundredthsPerSecond;
hundredths -= seconds * kHundredthsPerSecond;
return [NSString stringWithFormat:@"%02i:%02i:%02i:%02i",
hours, minutes, seconds, hundredths];
}
The final bit of information we want to display is the movie's source, that is, the full path of the movie file. As we've seen several times, it's easy enough to get a full pathname of a movie file by sending the fileName message to the document object. But it's preferable to display a Macintosh-style pathname instead of a UNIX-style pathname, so we need to perform this conversion. (There may very well be a method somewhere that converts UNIX pathnames into displayable Mac pathnames, but I haven't found any such method yet.) Listing 23 shows our definition of the setSource: method.
Listing 23: 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];
}
Of the five items in the info panel that we need to manually set, three will not change once they've been set initially: the movie duration, the normal size, and the source. (Well, strictly speaking, the duration can change if the user edits the movie and the source can change if the user performs a Save As operation; I'll leave handling these refinements as an exercise for the reader.) But we'll need a way to monitor the movie's current time and current size so that we can update our info drawer accordingly. NSWindow supports the windowDidResize notification, which is sent to all interested observers when a window has been resized. In windowControllerDidLoadNib, we can register for this notification by executing this code:
[[NSNotificationCenter defaultCenter] addObserver:self
selector:@selector(windowDidResize:)
name:NSWindowDidResizeNotification
object:[_movieView window]];
When a document window is resized, our windowDidResize: method (defined in Listing 24) is called. As you can see, it simply calls setCurrentSize: to set the current size.
Listing 24: Handling window resize notifications
windowDidResize
- (void) windowDidResize:(NSNotification*)notification
{
[self setCurrentSize];
}
Now, how do we update the Current Time item? There is (to my knowledge) no notification that is sent when the movie changes time, but it's easy enough to bring our movie controller action filter procedure to the rescue. We'll just look for mcActionIdle actions and update the Current Time item, as shown in Listing 25.
Listing 25: Handling movie controller actions
MyActionFilter
pascal Boolean MyActionFilter (MovieController mc,
short action, void* params, long refCon)
{
MyDocument *doc = (MyDocument *)refCon;
switch (action) {
// handle idle events
case mcActionIdle:
// update the current time display in the info drawer
if ([[doc drawer] state] != NSDrawerClosedState)
[doc setTimeDisplay];
break;
}
return false;
}
MyActionFilter is not a method of our custom document class MyDocument, so we cannot access the _drawer instance variable directly. Instead, as we mentioned earlier, we need to pass the document identifier to the filter procedure as the reference constant, and we can retrieve the drawer identifier by issuing the drawer message to that document.
One final task remains, which is to initialize the info drawer text items from inside windowControllerDidLoadNib:
[self setDuration];
[self setNormalSize];
[self setCurrentSize];
[self setSource: [self fileName]];
There is no need to initialize the current time, as it will be set on the next idle event anyway.
Loading Movies Asynchronously
Let's take a look at another easy enhancement to basic movie QuickTime playback and editing using Cocoa. In a recent article ("Loaded" in MacTech, September 2002), we learned how to load a movie asynchronously -- that is, continuing to process events without waiting for QuickTime to return with a valid movie identifier. NSMovie loads a movie in the standard synchronous way (by calling one of the NewMovieFrom... calls, without the newMovieAsyncOK flag). So calling initWithURL:byReference: will result in synchronous movie loading.
We've seen, however, that NSMovie also provides another method, initWithMovie:, which initializes an NSMovie object from an existing QuickTime movie. So we can indeed load a movie asynchronously within a Cocoa application, by using our own code to load the movie and then calling initWithMovie:.
I won't rework MooVeez to handle asynchronous movie loading, but I will show some code based on some functions from a real-life commercial application that does support it. The first thing we need to do, of course, is call NewMovieFromDataRef with a URL data reference that specifies a remote URL. Listing 26 shows the essential code for this.
Listing 26: Opening a remote movie
GetMovieFromURL
Movie GetMovieFromURL (NSString *url)
{
short flags = newMovieActive + newMovieAsyncOK;
Movie movie = NULL;
OSErr err = noErr;
// create a URL data reference
int length = [url cStringLength];
Handle dataRef = NewHandleClear(length + 1);
if (dataRef) {
[url getCString: (char*)*dataRef];
// create a movie from the data reference
err = NewMovieFromDataRef(&movie, flags, nil, dataRef,
URLDataHandlerSubType);
DisposeHandle(dataRef);
}
return err == noErr ? movie : NULL;
}
Here, url is an object of type NSString that specifies the remote file to open. We first create a URL data reference (dataRef) and then call NewMovieFromDataRef with the flag newMovieAsyncOK set.
Now we need to periodically check the movie load state of the movie. For this, we can install a timer procedure using Cocoa's NSTimer class, as shown in Listing 27.
Listing 27: Installing a timer to check the movie load state
loadAndPlayMovie
if (mMovieTimer == nil) {
mMovieTimer = [[NSTimer timerWithTimeInterval: 0.25
target: self
selector: @selector(movieTick:)
userInfo: nil
repeats: YES] retain];
[[NSRunLoop currentRunLoop] addTimer:mMovieTimer
forMode:NSDefaultRunLoopMode];
}
We create a timer that executes the movieTick: method every quarter second.
When the movie load state becomes at least kMovieLoadStateLoaded, we can create an NSMovie instance for the movie and then issue the setMovie: message to our NSMovieView instance. Listing 28 shows the essential steps here.
Listing 28: Checking the movie load state
movieTick
- (void) movieTick:(NSTimer *)inTimer
{
if (mQTMovie != nil) {
// We have a QuickTime Movie that hasn't been loaded enough
// to be assigned to an NSMovie.
long state = GetMovieLoadState(mQTMovie);
if (state == kMovieLoadStateError) {
// handle the error appropriately
} else if (state >= kMovieLoadStateLoaded) {
// We're ready to create our NSMovie.
NSMovie *movie = [[[NSMovie alloc]
initWithMovie: mQTMovie] autorelease];
[oDetailTrailerMovie setMovie: movie];
[self setQTMovie: nil]; // no longer managing the raw QT movie
} else {
MoviesTask(mQTMovie, 1);
}
}
}
Cocoa Enhancements
Cocoa supports several methods of extending or modifying the capabilities of the NSMovie and NSMovieView classes (or indeed, any of its built-in classes). Since we're working with an object-oriented language, we can of course subclass either of these classes to modify their existing behaviors or to add new behaviors (just like we did with NSDocument). But Cocoa also supports a way of adding instance methods to a class without subclassing, using what's called a category. In this section we'll take a look at using subclasses and categories to enhance NSMovieView.
Subclassing the Movie Classes
We would want to subclass NSMovie or NSMovieView principally to modify their default behaviors. A subclass can override methods of its superclass, and it can (if necessary) add instance variables to those of the superclass. Let's consider a couple of cases where we might want to subclass NSMovieView.
Our first example concerns the movieRect method supported by NSMovieView. This method returns the rectangle into which a movie is to be drawn, which by default is the view's bounding rectangle. In MooVeez, we never call movieRect to set a view's size, since we manually resize the movie window (and hence the enclosed movie view) to fit the movie being opened by the user. But other applications may want to draw a movie into a rectangle of a fixed, predetermined size. For instance, Sherlock draws movie trailers into a view of a set size, as shown in Figure 22.
Figure 22: Sherlock's movie trailers preview
QuickTime scales the movie to exactly fit the dimensions of the movie rectangle, which may result in a distortion caused by anamorphic scaling (where the amount of scaling in the horizontal direction differs from the amount of scaling in the vertical direction). Although it's a bit hard to see, Sherlock's movie trailers are scaled anamorphically.
By contrast, Watson overrides the movieRect method to ensure that the movie trailer is drawn at its natural proportions within the movie trailer view. Figure 23 shows the Watson movie trailer window. Here the movie is drawn without distortion (though once again it's hard to see that here).
Figure 23: Watson's movie trailers preview
Watson accomplishes this by subclassing NSMovieView and overriding the movieRect method. Listing 29 shows the override method. It scales the movie proportionally so that the longer side exactly fits the view rectangle; if the movie is smaller than the view rectangle, this method centers it in that rectangle.
Listing 29: Overriding the movieRect method
movieRect
- (NSRect) movieRect
{
NSRect viewRect = [super movieRect];
Movie qtMovie = [[self movie] QTMovie];
Rect movieRect = { 0,0,0,0 };
GetMovieNaturalBoundsRect(qtMovie, &movieRect);
float movieWidth = movieRect.right - movieRect.left;
float movieHeight = movieRect.bottom - movieRect.top;
if ((movieWidth <= viewRect.size.width) &&
(movieHeight <= viewRect.size.height)) {
// Movie is smaller than or equal to the view size; just center the movie.
viewRect.origin.y += (int)((viewRect.size.height -
movieHeight) / 2.0);
viewRect.size.height = movieHeight;
viewRect.origin.x += (int)((viewRect.size.width -
movieWidth) / 2.0);
viewRect.size.width = movieWidth;
} else {
// We need to scale down movie, centering horizontally/vertically.
float movieRatio = movieWidth / movieHeight;
float viewRatio = viewRect.size.width /
viewRect.size.height;
if (movieRatio > viewRatio) {
// The movie is wider than will fit, rescale.
float newHeight = viewRect.size.width / movieRatio;
viewRect.origin.y += (int) ((viewRect.size.height - newHeight) / 2.0);
viewRect.size.height = newHeight;
} else {
// The movie is taller than will fit (or equal aspect ratio), rescale.
float newWidth = viewRect.size.height * movieRatio;
viewRect.origin.x += (int) ((viewRect.size.width -
newWidth) / 2.0);
viewRect.size.width = newWidth;
}
}
return viewRect;
}
Let's briefly consider another case where we might want to override an NSMovieView method. NSMovieView supports a contextual menu. When the user control-clicks in a movie view, the contextual menu shown in Figure 24 pops up, allowing the user to control movie playback and perform some basic editing.
Figure 24: The contextual menu provided by NSMovieView
We can substitute our own contextual menu by overriding the class method defaultMenu, which is declared by NSView and implemented by NSMovieView. Better yet, we can override the menuForEvent: method, which allows us to vary the contextual menu according to the state of the view or the movie in the view. You'll notice that the contextual menu provided by NSMovieView is not sensitive to the media types in the movie; NSMovieView displays this menu for all QuickTime movies -- even for Flash and VR movies, where these menu items are not particularly useful. I'll leave implementing a menuForEvent: override as an exercise for the reader.
Extending the Movie Classes
A category on an Objective-C class is a way of adding methods to an existing class without having to create a subclass of that class. The added methods become part of the original class and can (if desired) use any of the instance variables of the original class. A category cannot, however, add any instance variables of its own.
We declare a category in much the same way we declare a subclass. We declare a subclass of the NSMovieView class like this:
@interface MyNSMovieView: NSMovieView
And we declare a category on the NSMovieView class like this:
@interface NSMovieView (NSMovieViewExtras)
The category name follows the class name, in parentheses.
NSMovieView provides a reasonably large set of methods for manipulating the movie and movie controller displayed in a movie view, but we might want to add even more methods in certain circumstances. Consider the contextual pop-up menu we just discussed; I'll bet that the large majority of Sherlock or Watson users have no idea that such a menu even exists, and that's not good. It might be better to provide some visible clue to the user.
One good solution here is to employ the custom controller bar button that we considered in an earlier article ("Movie Controller Potpourri" in MacTech, February 2000). We want to get NSMovieView to enable the custom button, so that our movie windows look like the one in Figure 25.
Figure 25: The custom button in the controller bar
And we want to attach the contextual menu to the custom button, so that clicking on the custom button causes that menu to pop-up, as in Figure 26.
Figure 26: The contextual menu attached to the custom button
This is a perfect opportunity to define a category on NSMovieView that adds two methods for showing and hiding the custom button. Listing 30 shows our category declaration.
Listing 30: Declaring a category on NSMovieView
NSMovieViewExtras.h
#import <Cocoa/Cocoa.h>
@interface NSMovieView (NSMovieViewExtras)
- (void) showCustomButton;
- (void) hideCustomButton;
@end
And Listing 31 shows our category implementation.
Listing 31: Implementing a category on NSMovieView
NSMovieViewExtras.m
#import <QuickTime/QuickTime.h>
#import "NSMovieViewExtras.h"
@implementation NSMovieView (NSMovieViewExtras)
- (void) showCustomButton
{
long flags = 0;
MovieController mc =
(MovieController)[self movieController];
if (mc) {
MCDoAction(mc, mcActionGetFlags, &flags);
MCDoAction(mc, mcActionSetFlags, (void *)
(flags | mcFlagsUseCustomButton));
}
}
- (void) hideCustomButton
{
long flags = 0;
MovieController mc =
(MovieController)[self movieController];
if (mc) {
MCDoAction(mc, mcActionGetFlags, &flags);
MCDoAction(mc, mcActionSetFlags, (void *)
(flags & ~mcFlagsUseCustomButton));
}
}
@end
Once we've added this category, we can send the showCustomButton and hideCustomButton messages to an instance of NSMovieView; for instance, we can add the following line to our windowControllerDidLoadNib method, to add the custom button to all of our movie windows:
[_movieView showCustomButton];
All that remains is to make the contextual menu pop up when the user clicks on the custom button. You may recall that the movie controller sends the mcActionCustomButtonClick action to our movie controller action filter procedure when the user clicks on this button. So all we need to do is add some code to that procedure; Listing 32 shows the new code.
Listing 32: Handling clicks on the custom button
MyActionFilter
case mcActionCustomButtonClick:
[NSMenu popUpContextMenu: [NSMovieView defaultMenu]
withEvent: [[[doc movieView] window] currentEvent]
forView: [doc movieView]];
break;
Here, we're sending the popUpContextMenu:withEvent:forView: message to the NSMenu class. The menu to be popped up is just the standard contextual menu provided by NSMovieView, which we obtain by sending the defaultMenu message to the NSMovieView class. If, as suggested above, we want to override that menu, we would send the defaultMenu message to our subclass of NSMovieView.
Conclusion
Cocoa evokes passion among developers who use it, and for good reason. It provides a powerful set of object-oriented frameworks that are accessible (when using Objective-C) with fairly limited extensions to the C programming language. The technique of invoking methods by sending messages to classes or instances leads to compact but reasonably readable code. And Cocoa is supported by a pair of tools, Project Builder and Interface Builder, that together form a mature and powerful integrated development environment.
In this article, we've taken a look at using Cocoa to develop QuickTime applications. We've seen how to use the NSMovie and NSMovieView classes to develop a simple multi-window movie playback and editing application, and we've investigated a few ways to enhance and extend those classes (by creating subclasses and categories). Altogether, Cocoa and QuickTime make a very good combination indeed.
Credits
Thanks are due to Adrian Baerlocher, Grahame Jastrebski, and Scott Kuechle for reviewing this article and offering some helpful suggestions. Special thanks are also due to Andrew Platzer for fielding a spate of technical questions and to Dan Wood of Karelia Software for allowing me to adapt snippets of code from Watson.
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.