MacDevCenter    
 Published on MacDevCenter (http://www.macdevcenter.com/)
 See this if you're having trouble printing code examples


Programming With Cocoa

Movies and Menus

01/25/2002

A number of readers have been asking for a column describing how to use Cocoa and QuickTime together. I hope today's article appeases those requests. (Now, if I only knew a little more about networking ...)

So today I'll cover how to incorporate QuickTime media into Cocoa applications. I also have a few things to say about menus, which are not too closely related to QuickTime, but a movie player makes as good an app as any for using menus. Specifically, I'll show you how you can give your application contextual menus, and even a dock menu.

Before I get too far into the column, let me warn you that I'm not an expert with the QuickTime API. In fact, I'm not even a novice. This column is the first time I've really even come close to looking at the QuickTime API documentation. But that's not the point anyway; we're here to see how we can put movies into our application. So let's see how.

NSMovie and NSMovieView

The two classes that we will use to work with QuickTime movies are NSMovie and NSMovieView. NSMovie is a wrapper class that provides an object-oriented interface to QuickTime data. Specifically, NSMovie provides a simple interface for loading movie data into memory, and subsequently accessing it.

For those of you interested in hardcore QuickTime programming, let me say that you can obtain a pointer to the QuickTime data using the NSMovie method -movie. This will then allow you to use the QuickTime framework to access and manipulate the data. For our purposes, we will focus our efforts on using NSMovieView. NSMovie is more of a vehicle for loading data than manipulating it.

NSMovieView (or movie view) is a subclass of NSView; it is a view for displaying the data of NSMovie objects. By default, a movie view presents users with the standard set of controls for playing and editing a QuickTime movie. This can be disabled in favor of, or supplemented with, a custom interface. NSMovieView provides methods that give us complete control over the playback of the movie. Later in the column, we will do just this with a simple menu-driven interface.

For now, let's get our feet wet by building the interface, which is as simple as dragging a movie view container onto the window, and constructing a simple controller object with an outlet to the container.

Building the Interface

Let's start by creating a new project called SimpleMoviePlayer by selecting File > New Project. Open the file MainMenu.nib by double-clicking in Project Builder, and Interface Builder will open. The interface we intend to create is very straightforward. Within the Cocoa-GraphicsViews palette, you'll see the QuickTime logo; this is an NSMovieView, which is similar to the NSView container we had previously employed. Drag this onto your window and the container will appear. Resize it to fill the window.

Learning CocoaLearning Cocoa
By Apple Computer, Inc.
Table of Contents
Index
Sample Chapter
Full Description
Read Online -- Safari

Be aware of one thing regarding the NSMovieView: any movie played in the view will be resized to fit, so it's a good idea to resize the view to an appropriate size such as 320x240, or 480x256. If you want to easily resize a movie you'll need QuickTime Pro ($29.99) to make adjustments to your content. To resize simply go to File --> Export. Make sure you have "Movie to QuickTime Movie" selected in the dropdown menu, then click the Options button. In Options, click the Size button to set your new dimensions. For the best looking results, keep your proportions consistent as you resize.

As I said before, I'm not familiar with the QuickTime programming API, but I imagine that a function exists to return the dimensions of a movie. If we could do this, we could then resize the view to fit the movie when we load it. If anyone knows how to do this, feel free to post it.

At this point you should take a few moments to familiarize yourself with the available options in the Attributes of the NSMovieView container. With the movie view highlighted, type Command-1 to bring up the attributes. Today, we won't make any changes here, but feel free to muck around with them at will.

Now we need create a Controller class for the application. In the Classes tab of the nib window, highlight NSObject and press return to subclass it. As always, name the subclass Controller. Now press Command-1 to bring up the attributes in the Info panel. To the Controller, we want to add one outlet named movieView, and one action method named openMovie:. Next, instantiate Controller by selecting Classes > Instantiate.

The outlet movieView will be connected to the NSMovieView container in the window. The action openMovie: will, of course, be used to open a movie and load it into the view. Make sure the MainMenu.nib - Main Menu window is open. We will connect the Open... menu item to the openMovie: action of Controller. To do this, Control-drag from the Open... menu item onto the Controller and make the connection.

With that, we are now ready to create the files to be added to the project, and return to PB to do our coding. Choose Controller from the Classes tab, and then choose the menu item Classes > Create Files For Controller. Now let's get to come coding.

Loading the Movie

This next section has to do with the code that we'll write to load a movie into the view. This requires two fundamental steps. The first step is to create a new instance of NSMovie and initialize it to load our movie data from disk into memory. We create the new instance using +alloc, and we'll then initialize it using the NSMovie method -initWithURL:byReference:.

Comment on this articleLet's talk about this latest cocoa tutorial.
Post your comments

The second step will tell the NSMovieView in our window to display the NSMovie instance we just created. We do this by sending a -setMovie: message to the movie view with our NSMovie object as the argument.

Now we've got a missing link here -- we're kind of working backwards up to the final code for openMovie:. We have to supply initWithURL:byReference: a URL to the movie file, rather than a regular string file path.

This is a bit different for us. We've always used simple file paths to open files on our disks, never really URLs, but in this case we need to use URLs. But that's not a problem. Cocoa handles URLs with the class NSURL. With this class you can make URLs from standard strings. Rather than hardcode a URL into the application, we will allow the user to pick the file to open. This is made possible by the class NSOpenPanel.

NSOpenPanel

As its name implies, the class NSOpenPanel provides us with the standard Open panel for Cocoa applications. Using it is very straightforward, and thus very convenient. We will use an Open panel to allow the user to find a movie to open, and when the user clicks Open we will obtain from the NSOpenPanel object the URL for whichever file was selected by the user.

To run an Open panel, we first have to create an instance of NSOpenPanel using the class method +openPanel. We then use the method -runModalForDirectory:file:types:, which does the job of presenting the user with an Open panel on screen.

This method takes three arguments. The first argument is the directory we want the Open panel to default to when it appears. We will make this argument the home directory of the user, which is easily obtained with the function NSHomeDirectory(). The second argument is a file we want initially selected in the open panel; we'll just make this nil. The third argument is an array of file types that the Open panel will allow the user to select. For our purposes this array will contain strings representing the file types "mov," "mpg," "mp3," and "jpg." In an NSMovieView, you can open any file format supported by QuickTime.

Finally, note that -runModalForDirectory:file:types: returns an integer. This integer return value lets us know which button was pushed. This is analogous to what we've learned about how alert panels and other such devices work.

Let's look at the code for the method openMovie: and see how everything fits together:


- (IBAction)openMovie:(id)sender
{
  NSArray *fileTypes = [NSArray arrayWithObjects:@"mov", @"mpg", @"mp3", 
  @"jpg", nil];
  
  NSOpenPanel *oPanel = [NSOpenPanel openPanel];

  int result = [oPanel runModalForDirectory:NSHomeDirectory() file:nil types:fileTypes];
    
  if (result == NSOKButton) {
	NSArray *movieToOpen = [oPanel URLs];
	NSURL *movieURL = [movieToOpen objectAtIndex:0];

	NSMovie *movie = [[NSMovie alloc] initWithURL:movieURL byReference:NO];
	[movieView setMovie:movie];
  }
}

We see in the first line that we create an array containing the strings for the file types we will allow the user to open. Note that when we use the NSArray convenience constructor +arrayWithObjects:, the last object to be added to the array must be followed by nil.

In the next line of code all we did was create a new NSOpenPanel object and assign it to the variable oPanel. In the third line, we tell the Open panel to display itself. As we discussed above, we had the panel open to the user's home directory by passing the return value of the function NSHomeDirectory() as the first argument, and then we opted against selecting an initial file, and we passed our fileTypes array as the final argument.

In the next part of the method, we have an if statement that evaluates true if the user clicks on the panel's Open button. What we do in this statement is retrieve from oPanel the file that was selected. We obtain this information by sending oPanel a URLs message, which returns an array of URLs for the selected files (an array is returned to allow for the possibility that the user selected multiple items in the Open panel). In the following line we pick the first object in the array and store it in the NSURL variable movieURL. This is the variable we will pass when we initialize the NSMovie object movie.

Related Reading

Mac OS X: The Missing ManualMac OS X: The Missing Manual
By David Pogue
Table of Contents
Index
Full Description
Sample Chapter

Again, initialize movie by using the initWithURL:byReference: method, passing movieURL as the first argument. The second argument, byReference, is used to indicate whether or not we want to encode certain header information when the NSMovie object is archived. For our purposes here, we won't concern ourselves with this and simply pass NO. Finally, we set the movie view to display movie, and voila! Compile, run, watch some movies! Next we're going to move on to create a somewhat custom controls interface to the movie view.

Menus

In this next section, I'm going to discuss how to make menus, and in particular, how to assign a menu as the contextual menu of a view, as well as the Dock menu of the application, both of which are mindless tasks in Interface Builder. The menu we are adding to the application will be simple, consisting of three menu items labeled "Play," "Pause," and "Loop." To support these functions, add to the file Controller.h the following three action method declarations:

Save these changes, and let's go into Interface Builder to build the menu.

In Interface Builder, we want to first make IB aware of the changes we just made to Controller.h -- that is, add these three actions to the Controller class in IB. To do this, select the Controller class from the Classes tab, and choose Read Files from the Classes menu.

This will bring up a dialog asking you which file to read from. Controller.h should be selected as the default. If not, select it and press return (or the Parse button). Assuming none of the existing outlets and actions have changed (and they shouldn't have), the class will automatically update itself to reflect the changes we made to Controller.h in Project Builder. Now we're ready to build the menus.

To create a new menu, simply drag the menu icon from the Cocoa-Menus palette onto Instances tab of the nib window. This will show the default menu, with two menu items. You can edit the attributes of these menu items by selecting on and pressing Command-1 to bring up the Info panel. Change the names of these items from "Item 1" and "Item 2" to "Play" and "Pause."

For "Loop," we need to add a third menu item. In the same Cocoa-Menus palette you will see an object labeled "Item;" drag this onto the menu window just below the "Pause" menu item. Now we're set to make some connections.

As you can imagine, Play will be connected to the action playMovie:, Pause to pauseMovie:, and Loop to toggleLoopMode:. Make these connections by dragging a wire from each menu item onto the Controller instance.

Now we want to make a couple more connections. The first connection will be between the NSMenu instance and the File's Owner object. Drag a wire from File's Owner to the icon labeled NSMenu and a list of File's Owner's outlets will show up. The outlet we want to connect to is dockMenu. We've just enabled our application to make use of a custom dock menu! That easily!

Similarly, drag a wire from the NSMovieView to the NSMenu icon and a list of the movie view's outlets will appear. We want to make a connection to the outlet menu. Doing this will make our little menu a contextual menu for the movie view. Congratulations! We have a contextual menu! Notice that we made the connection here between the menu and the movie view. This means that the menu is the contextual menu only for the movie view and nothing else. So if you Control-click outside of the view, no menu will appear, but if you Control-click on the movie view, this menu will appear. By attaching menus to views, Cocoa lets us have contextual menus customized to each view in the application.

Implementing These Methods

With the Interface reconfigured to our needs, we return to Project Builder to implement these methods. If you've scanned over the NSMovieView documentation, you probably have a good idea of what's going to happen next. The first method we will implement is -playMovie:, and this is done with just one line of code:


- (IBAction)playMovie:(id)sender
{
    [movieView start:self];
}

Here we used the NSMovieView method start: to start playing the movie. The argument to the method is the message sender, which we set as self here. The method -pauseMovie: is nearly the same:


- (IBAction)pauseMovie:(id)sender
{
    [movieView stop:self];
}

You see here that all we are doing is invoking the stop method of NSMovieView; easy enough. The third method, -toggleLoopMode:, is slightly more complicated.

The idea is that we want to allow the user to toggle between not looping through the movie and looping through it. When the movie is in loop mode, there will be a check next to the Loop menu item. A check indicates that the menu item is in its on state; no check indicates that the menu item is in its off state. One can set and determine the state of a menu item using NSMenuItem's -setState and -state: methods, respectively.

A QuickTime movie has three play modes with respect to looping. The first mode is normal playback mode, the second is looping playback mode, and the third is loop back-and-forth playback mode; we will only toggle between the first two.

The idea of -toggleLoopPlayback is to check the state of the menu item and change the looping mode accordingly, in addition to changing the state to reflect the changed looping mode. Let's lay it out and take a look:


- (IBAction)toggleLoopMode:(id)sender
{
  if ( [sender state] == NSOnState ) {
	[sender setState:NSOffState];
	[movieView setLoopMode:NSQTMovieNormalPlayback];
  } else {
	[sender setState:NSOnState];
	[movieView setLoopMode:NSQTMovieLoopingPlayback];
  }
}

We checked to see whether or not the menu item was in its on state, and if so, we set it to the off state and set the playback mode to normal. If the menu item's state was off, then we changed it to on and set the playback mode to looping playback. NSQTMovieNormalPlayback and NSQTMovieLoopingPlayback are just constants used to represent these two modes.

Also in Programming with Cocoa

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Build an eDoc Reader for Your iPod, Part 3

Now compile and run you application and play around with it. With a little bit of exploration, it shouldn't be long before you notice that Loop in the dock menu acts a little screwy. If you look at Project Builder's runtime error messages, you'll notice that you get something telling you that NSApplication doesn't respond to state messages.

The source of these error messages and the undesirable behavior in the dock menu has to do with the nature of the dock menu itself. The Dock is a separate application that displays dock menus. Communication happens between the dock and your application to send messages as a result of selecting custom dock menu items, such as Loop. Unfortunately, what this means for us is that when we expect the sender argument variable to be assigned to the menu item itself, which does indeed respond to state, it is really assigned to the application instance, since that is the object communicating with the Dock and the dock menu.

The moral of the story is that we can't rely on the otherwise convenient sender argument variable being the menu item when working with dock menus. A solution to this problem is to create an outlet in Controller that is connected to the menu item, and then we can communicate directly with the menu item, rather than relying on the sender argument. I'll leave this for you to work out, and you can see it in my project available for download here.

With that, we've come to the end of today's column. I hope you enjoyed it and have found something useful in it. I mentioned earlier the issue of movies being resized to the NSMovieView size, and I would like to make a request to address this issue. See you next time!


Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.