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


Programming With Cocoa

Working with Sheets in Cocoa

10/05/2001

One of the features we saw with the advent of Mac OS X was a new way to handle dialog boxes in an application. This new way is called "sheets," which is a special kind of window that is actually attached to another window, and the result is a cleaner, less cluttered workspace. It turns out that this behavior isn't limited solely to open, save, and alert windows. Indeed, we can make any window we want into a sheet. We see this behavior in many applications where a simple preferences window is implemented as a sheet attached to the main window of an application, or in an authentication window that appears as a sheet, and so on.

In today's column we will explore two different ways to implement sheets in Cocoa, with particular emphasis on converting our record deletion alert panel and our preferences window into sheets. The first is easily done with a prebuilt function from the AppKit, while the second involves using a more flexible and more fundamental method. All of the work we do today will be based on the AddressBook application that we were left with at the end of the last column, which can be downloaded here.

Some preliminaries

Before we can implement methods that make sheets, we must have a means to send messages to our two windows within the Controller instance -- we need some outlets connected to our windows. Specifically, we want to add two outlets to our class Controller called prefsWindow and mainWindow. Additionally, we want to reconnect the "Preferences" menu item and the Close button in prefsWindow to actions in Controller, rather than to actions of prefsWindow, as we had done previously.

In Interface Builder, add these two outlets and connect them to their respective windows. Also add the actions openPrefsSheet: and closePrefsSheet:, and connect those to their appropriate controls. Note that before you can make connections between the preferences, the Close button, and action in Controller, you must disconnect the old connections. If you attempt to make a new connection without doing so, Interface Builder will show you the old connection, which you can then disconnect. Save these changes and return to Project Builder. In Project Builder, add to the file Controller.h the two instance variable declarations:

IBOutlet id mainWindow;
IBOutlet id prefsWindow;

And the two action method declarations:

- (IBAction)openPrefsSheet:(id)sender;
- (IBAction)closePrefsSheet:(id)sender;

We'll add the action method definitions a bit later, and with that we're set to make our sheets.

Alert sheets

Several columns ago, we made an alert panel appear when the user attempted to delete a record, asking for confirmation of that delete command. We did this by using the AppKit function NSRunAlertPanel. Today we will replace this function with another AppKit function that will allow us to create an alert sheet, rather than an alert panel. This function is called NSBeginAlertSheet, and it takes several arguments.

The arguments that can be used with NSBeginAlertSheet are listed below; there are 10 in all.

  1. NSString *title
  2. NSString *defaultButton
  3. NSString *alternateButton
  4. NSString *otherbutton
  5. NSWindow *docWindow
  6. id modalDelegate
  7. SEL willEndSelector
  8. SEL didEndSelector
  9. void *contextInfo
  10. NSString *message

The first four arguments, as well as the last argument are no different than the arguments in NSRunAlertPanel and serve the same purpose. The first argument is the title we wish to give the alert sheet. Arguments 2, 3, and 4 are the titles for each of the three possible buttons. As was true for NSRunAlertPanel, if you don't want a button to be used, simply pass "nil" as the argument value for that button's title. The last argument, 10, is the message text to be displayed in the alert sheet. Nothing too complicated or different here.

The middle five arguments are new to us. The fifth argument is the window we wish to attach the alert sheet to. Remember, sheets belong to windows, and this is where we indicate which window it should go with. In our case, we want the alert sheet to be attached to the main application window, so we will pass mainWindow for argument 5.

The next few arguments have to do with the structure of our new deleteRecord implementation and how it differs from our old one. Unlike NSRunAlertPanel, the return value of NSBeginAlertSheet is void. This means we can't use the same mechanism as NSRunAlertPanel to determine which button was pressed. Rather, pushing any of the buttons in the sheet causes a method to be called which then handles execution of select code based on which button was pressed. This is where arguments 6, 7, and 8 come into play. Argument 6 indicates which object contains this method. We will simply pass "self" as this argument.

Arguments 7 and 8 tell the function which method to call in modalDelegate when a button is pressed. The data type of these two arguments is a new type we haven't encountered here before, SEL. SEL stands for selector. Selectors are Objective-C's way of referring to method names outside of the context of a message. For example, whenever we use a method name in code (except for the declaration and definition syntax), it always occurs as part of a message:

[receiver methodName];

Comment on this articleAs we continue to work with our AddressBook application, this time adding sheets, what are your comments?
Post your comments

Also in Programming With Cocoa:

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Selectors allow us to refer to method names as an argument to a method or function, independent of a receiver object (although, in the case of NSBeginAlertSheet we are in a sense indicating the receiver object in the modalDelegate argument-self). However, we can't just pass the name of the method because the compiler will think we are passing a variable name. Rather, we have to use a special construct @selector( methodName ), which will convert a method name into a selector. This tells the compiler that whatever is in the parentheses is a method name, and should be treated as such. The role of selectors in Objective-C is quite a bit larger than this discussion may let on, so for a more in-depth discussion of selectors, I refer you to Object-Oriented Programming and the Objective-C Language.

So we put the method we want executed when the sheet ends in either argument 7 or argument 8. But which one? The difference between arguments 7 and 8 is the timing of the method call. If we put our selector in argument 7, that method will be executed right when a button is pressed in the sheet. If we put the selector in argument 8, the method is invoked after the sheet has closed. You could, if you wanted, have two different methods invoked here. If you don't use one of these arguments, simply pass "nil" as the value.

What then is this method that we're talking about? It can be anything you want it to be. However, if you want to obtain information about the sheet such as which button was pressed, or which sheet is invoking the method (if you had many sheets in your application), then you need to use the method sheetDidEnd:returnCode:contextInfo: (herein referred to as sheetDidEnd...). We'll talk more about this method in a minute when we actually implement this function.

This brings us to argument 9, contextInfo. The data type void * is simply that of a generic pointer that provides a convenient and flexible way of passing information between the method where NSBeginAlertSheet is called and the method sheetDidEnd...

With that, let's see how we can convert our alert panel into an alert sheet using the new function. Recall that the structure of our previous implementation of deleteRecord both ran the alert panel and handled code execution based on the return value of NSRunAlertButton. But we've already said that NSBeginAlertSheet doesn't work this way, so we will have to split up our old deleteRecord into a new deleteRecord and the method sheetDidEnd.... We will move all code specific to executing the record deletion out of deleteRecord: and into sheetDidEnd.... The action method deleteRecord will now look like the following:

- (IBAction)deleteRecord:(id)sender
{
  NSString *title = @"Warning!";
  NSString *defaultButton = @"Delete";
  NSString *alternateButton = @"Don't Delete";
  NSString *otherButton = nil;
  NSString *message = @"Are you sure you want to delete the selected record(s)?";

  if ( [tableView numberOfSelectedRows] == 0 )
    return;
  
  NSBeep();
  NSBeginAlertSheet(title, defaultButton, alternateButton, otherButton, mainWindow, self, @selector(sheetDidEnd:returnCode:contextInfo:), nil, nil, message);
}

That's the first part. Now we have to implement sheetDidEnd... Add the following method definition to your Controller.m file:

- (void)sheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
  NSEnumerator *enumerator;
  NSNumber *index;
  NSMutableArray *tempArray;
  id tempObject;

  if ( returnCode == NSAlertDefaultReturn ) {
      enumerator = [tableView selectedRowEnumerator];
      tempArray = [NSMutableArray array];
    
      while ( (index = [enumerator nextObject]) ) {
        tempObject = [records objectAtIndex:[index intValue]];
        [tempArray addObject:tempObject];
      }
    
      [records removeObjectsInArray:tempArray];
      [tableView reloadData];
      [self saveData]; 
  }
}

The code we use to delete the selected records hasn't changed at all -- it's just executed in a different place.

So that's how we make an alert sheet. You should be able to compile this code now and see the fruits of your labor. Your alert sheet should look like the one shown below.

Screenshot.
Our alert sheet.

Now we go on to a slightly more complex sheet implementation that will allow us to display the preferences window as a sheet attached to the main application window.

Sheets from arbitrary windows

In this next section, we will concern ourselves with converting our preferences window into a sheet. The method we will use to open our preferences window as a sheet attached to the main application window comes to us from NSApplication, and its name is beginSheet:modalForWindow:modalDelegate:didEndSelector:contextInfo: (we'll refer to it as beginSheet...). You should notice that many of the arguments are the same as what we had in NSBeginAlertSheet, such as modalDelegate:, didEndSelector:, and contextInfo:. The first argument is really the only new thing in this method, and all that is in the window we wish to make into a sheet. The rest of the arguments are the same as their counterparts in NSBeginAlertSheet. Let's write down openPrefsSheet with this method in it:

- (IBAction)openPrefsSheet:(id)sender
{
[NSApp beginSheet:prefsWindow 
                   modalForWindow:mainWindow 
                   modalDelegate:nil
                   didEndSelector:nil
                   contextInfo:nil];

    [NSApp runModalForWindow:prefsWindow];

    [NSApp endSheet:prefsWindow];
    [prefsWindow orderOut:self];
}

Recall that NSApp is a global constant provided to us by Cocoa that lets us send messages to our application's instance of NSApplication. You see in the example above that there is a lot of support code that allows us to work the sheet. Also notice that while beginSheet... carries many of the same arguments as NSBeginAlertSheet, we don't use them in the same way. In particular, we made the last three arguments "nil". When we used NSBeginAlertSheet we used the modal delegate and selector to indicate a method to invoke when the button is pressed. Here we don't do that. Rather we rely on the Close button's action closePrefsSheet to handle button-specific code.

The message beginSheet... attaches prefsWindow to mainWindow as a sheet, and starts what is known as a modal session. A sheet in a modal session stops all other windows and panels from receiving events such as mouse clicks, but it still allows the application to perform some long background process or operation. The next line is a runModalForWindow: message to NSApp, which creates what is known as a modal event loop for prefsWindow -- our sheet. What this means is that, like in a modal session, the application will not accept any events unless that input is directed at the window we're running a modal loop for -- prefsWindow, but unlike a modal session, the application will pause until the modal loop is ended. Note that we will still be able to interact with control in the sheet, which is crucial as a preferences window isn't much use if we can't interact with the controls!

You can see one of the practical differences between modal sessions and modal loops simply in the execution flow in openPrefsSheet:. When we invoke beginSheet... a modal session is started, but the application goes on with executing openPrefsSheet:. However, at the point where we invoke runModalForWindow:, the application's event loop pauses while the indicated window runs in a modal loop. That is, not only is prefsWindow the only window that can accept events, but execution of our method stops until the modal loop ends. The application will remain in this state until some action in prefsWindow sends one of three messages to NSApp: stopModal, abortModal, stopModalWithCode:. This will be taken care of in the action method connected to our Close button-closePrefsSheet. Let's take a look at that method now.

The method closePrefsSheet does one simple thing for us -- indicate that the sheet should be closed by stopping the modal event loop for prefsWindow. All we do in closeMethod is the following:

- (IBAction)closePrefsSheet:(id)sender
{
    [NSApp stopModal];
}

And with that our application resumes execution of the method openSheet at the point directly after where we started the modal event loop. Now -- back in openPrefsSheet: where we left off -- we simply send a couple of messages that ends the sheet's modal session, and removes it from the screen. endSheet: ends the document modal session began by beginSheet..., and orderOut: removes it from the window. The argument of orderOut: is the message sender, which in this case is self.

Try rebuilding now and see the results. You can download the completed project folder here. Your preferences sheet should look like the image below.

Screenshot.
The preferences sheet.

Because we only have one button in our window (other than the column-selector switches), the close button, stopModal, is sufficient for our purposes. If we had more than one button, however, than we could do several things to handle the different actions of different buttons. The obvious solution is to put the button-specific code in each button's action method. In that way, the button-specific code would be executed when its button is pushed, and each of these action methods would have a stopModal message to NSApp like we have above.

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

The other option is to have each button's action send a stopModalWithCode: message to NSApp, where the argument is an integer. What happens then, is that the integer passed as the argument to stopModalWithCode is returned by runModalForWindow:, and we can store that in a variable for later use. In this way, each button's action methods can take on a unique int identifier, allowing us to determine which button was pressed, and then executing code based on that.

In simple situations, it is probably better to just run whatever code you need within each button's action method, but I wanted to point out and make you aware of the available options.

As a fun aside, I want to tell you how to change the transparency of a window. You might have noticed that the transparency of our preferences window was changed as a result of it being a sheet. We could change it if we wanted to using one simple line of code, that we could put in awakeFromNib:

[prefsWindow setAlphaValue:0.50];

This would make a pretty transparent window, which is probably not so useful for a window with a lot of controls like we have. You can see the results of this in the image below:

Screen shot.
A very pale preferences sheet.

That's it for now! That's how we create sheets. Not too difficult, I think, so have fun with it. This column is the last time we'll talk about AddressBook for a while. Next time, we'll begin our foray into Cocoa graphics, which will last for some time. See you next then!

Michael Beam is a software engineer in the energy industry specializing in seismic application development on Linux with C++ and Qt. He lives in Houston, Texas with his wife and son.


Read more Programming With Cocoa columns.

Return to the Mac DevCenter.

Copyright © 2009 O'Reilly Media, Inc.