« Building Easy Sheets

Posted by Erik J. Barzeski on April 10, 2001 [Feedback (2) & TrackBack (0)]

// What Is a Sheet?

Cocoa, and in fact Mac OS X, introduced a few new GUI elements. Drawers are one of the new elements, and aren't used by too many application right now. They are used nicely, however, in Mail to house your mailboxes and in OmniWeb to store bookmarks.

One of the other elements now available to Mac developers are called "sheets." Sheets are one of the more heavily "demoed" elements. We've all seen Steve Jobs show off the save panels, open panels, and so on. In the vernacular, sheets are "those things that slide out of the title bar of a window." Truth is, they're "windows" themselves, but... let's not get into that just yet.


// Consult Your References

Let's get a little background information. Sheets "slide out" from the title bar "over" a "regular" window. NSAlertPanel and some others are available as sheets, but this article won't talk about "error" sheets per se.

However, I am going to diverge a bit to talk about modality. Part two will actually get to the "sheets" tutorial. Modal windows are those that require a user interact with them before anything else can be done. For example, look at the picture here:

Software Update
Software Update throws a sheet at you when it can't connect to the Internet (which is nearly all the time, it seems).

By throwing this sheet at the user and specifying that it be modal, Apple prevents people from clicking on the "Screen Saver" module in the toolbar or from trying to change their update schedule to "Weekly" until the user has acknowledged that they've noticed the error by clicking "OK."

As Apple's documentation for NSWindow states, there are two mechanisms for operating a modal window or panel. The first, and simpler, is to simply use NSApp's runModalForWindow: method. This monopolizes events for the specified window until stopModal, abortModal, or stopModalWithCode: is invoked (usually as the result of a button's action). But what do each of those do?

stopModal ends the modal status of the window or panel from within the event loop. What's that mean? Well, for one thing, you can't use the stopModal method from a distributed object like an NSTimer because they're outside the event loop.

To terminate the modal loop in those situations, you can use the abortModal method. While stopModal is usually called when a user presses an "OK" button, abortModal is typically used when the user presses a "Cancel" button or presses the escape key. The stopModalWithCode: method is equivalent to the prior two, but gives you the option of specifying a code that can be used to generate the proper error message (or whatever you want to do to handle the code).

The second mechanism is called a modal session. It allows the application to perform a long operation while it still sends events to the modal window or panel. Modal sessions are useful for panels that allow a user to cancel or modify an operation. You begin modal sessions with NSApp's beginModalSessionForWindow: method, which sets the window up for the session and returns an identifier used for other session-controlling methods. The method description for runModalForWindow: in the NSApplication class specification includes sample code illustrating a modal session. Look there for more on this: we won't cover it in this tutorial.

Skip along to the next section where we will make a simple application that uses a sheet.


// Lay the Groundwork

I'm a fan of sample applications, so let's pretend that we're creating a simple game called "Wumpus." It's played in one window (i.e. it's not document-based or anything goofy like that). Apple's HIG (human interface guidelines) allow us to use a sheet for preferences in such a case, so we'll do just that.

Create a new Cocoa application project from within Project Builder. You'll be presented with a standard Cocoa app with "main.m" as the only file. Create a new Cocoa class and call it "mySheetController" (the .h and .m files will be made for you and inserted into your project).

Wumpus' preferences sheet won't actually do anything, so we needn't worry about creating actions and outlets for the various controls we'll create so Wumpus gives a nice demo. Instead, we need only two outlets - one to the sheet and one to the main window. We also need two actions - one each for the window's and the sheet's button. Edit your header file to look like this:

#import <Cocoa/Cocoa.h>

@interface mySheetController : NSWindowController
    IBOutlet id configureSheet;
    IBOutlet id mainWindow;
- (IBAction)configureGame:(id)sender;
- (IBAction)doneConfiguring:(id)sender;

Of course, if you like to work from within Interface Builder, simply subclass NSWindowController and add the appropriate outlets and actions and generate the files. Otherwise, double-click "MainMenu.nib" in your project's Resources folder to launch Project Builder, and drag your "mySheetController.h" file into the MainMenu.nib window in IB. IB will then parse the header and switch to the "Classes" view. While you're here, instantiate a "mySheetController" object (use the "Classes" menu).

MainMenu.nib will contain both Wumpus' main window as well as the sheet. Sheets are of type "NSPanel," typically. Below you can see how I chose to lay out the elements in Wumpus: do something similar with yours. As I said, the only controls we'll care about are the two buttons, one in each window.

Wumpus' Layout
Lay out your version of Wumpus similarly to this (click for a full-size image). Note: "SheetController" is equivalent to "mySheetController." I made a boo-boo. <grin>

Connect (control-drag) from each of the buttons to your controller and attach them to the correct action. Likewise, control-drag from your controller to each of the windows (either in the MainMenu.nib window or to the actual on-screen layout representation of your windows). Save your .nib file and quit Interface Builder. It's time to write some code!


// Write the Code

We could write our sheet to run "modally," but there's really no point in it. We'll cover that in another article sometime. However, keep modality in mind as you build and run this application. Note how the simplest of sheets have some useful features... and what features they're lacking (specifically, open your sheet and then check out Wumpus' menus).

Let's get the easy stuff out of the way quick:

#import "mySheetController.h"

@implementation mySheetController

- (IBAction)configureGame:(id)sender
    [NSApp beginSheet:configureSheet modalForWindow:mainWindow
        modalDelegate:self didEndSelector:NULL contextInfo:nil];

So this is how you begin a sheet. If you've never seen "NSApp" before, it's essentially a pointer to your application object. You'll use it from time to time. The next two portions of the method, beginSheet: and modalForWindow: tell your application which window/panel is the sheet, and which window to which it should attach the sheet. We haven't defined a delegate, so our sheet can act as its own delegate. The didEndSelector: and contextInfo: pieces are not necessary for simple sheets, so we pass them NULL and nil.

And that's all that's needed. The sheet will "roll out" from the mainWindow's title bar. But how do we get rid of it? This is even easier!

- (IBAction)doneConfiguring:(id)sender
    [configureSheet orderOut:nil];
    [NSApp endSheet:configureSheet];


That's it. Tell the sheet to orderOut (go away) and then tell the application to endSheet: - note that this is like a bookend to beginSheet:. And that's literally all you've got to do. Of course, if you wanted to read in the changes made in the sheet, you'd need outlets to all of the controls, buttons, text fields, etc. But we'll save that for another tutorial. For now, hey, you've got a working sheet!


// Run The Bugger

Save, Build, Run. Click your button. Watch your sheet appear. Amaze your friends. It should look like this:

Booger is Running!
Ring. Ring. Is your Wumpus running? You'd better catch it!

Of course, clicking the "Done" button should close your sheet nicely. You're done. I'm not one for a summary "lessons learned" or an overview, so this is the end. Deal with it. <grin>


Great...thankx...just what I was looking for, but without all the useless fluff...

Posted by: Carla on March 23, 2003 11:57 PM

Great stuff!

Just as a side note, if someone for the same details I was:
You can use Windows exact same way as panels, and there
is no requirement to create a separate class for the sheet.

I had a Preferences class with sliding sheets for adding and removing accounts in a single connector.

Posted by: Lirm on July 22, 2003 03:14 PM
Post a comment