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


Programming With Cocoa

Creating Toolbars for Mac OS X

by Michael Beam
03/15/2002

Today we’re going to continue our parade through the new Aqua interface with a look at toolbars. Toolbars are a great addition to the Macintosh GUI. They provide a consistent interface across all applications and are highly customizable by users, taking some of the UI burden off the developer.

To learn about toolbars, we’re going to revisit an old application -- our trusty Address Book. If you haven’t gone through these columns yet, don’t worry, we’re just using it as a base for today’s events, and you should be able to follow along without any problems. You can download the starting project folder here, which we will use for today's work.

Overview

In the course of learning Cocoa, we’ve seen how many of the more complex user interface elements have objects working behind the scenes to manage them. Recall that tableviews had data source objects to provide them with their contents to display.

Comment on this articleMike covers a lot of ground in this tutorial. How has it gone for you, and what are your questions?
Post your comments

Toolbars also require an object to supply the toolbar with the actual controls to display, as well as provide a list of item identifiers so the toolbar knows what’s available. These controls are instances of the class NSToolbarItem, and are referred to as toolbar items. Each toolbar item has a unique string identifier that the toolbar uses to keep track of each item. This object is the toolbar delegate, which will be fulfilled faithfully by our existing NSController object.

If we stop for a moment to think about how toolbars behave, we come up with the following: Toolbars contain a set of controls. This set of controls is customizable by the user via a customization palette. Contained in the palette is a collection of all possible controls that may be placed in the toolbar, as well as a predefined set of controls which makes up the default toolbar.

The toolbar delegate implements the following three required methods to support the behavior described above:

Our toolbar will have the following custom items, which we will make ourselves:

Additionally our toolbar will contain the following standard toolbar items, provided free of charge by Cocoa:

Three other standard toolbar items are available for use from NSToolbarItem:

The image below shows these seven default items in a toolbar customization palette.

Categories

As I mentioned earlier, NSController will serve as the toolbar delegate, but we’re not going to put the delegate methods in Controller.m. Rather, we’re going to add a category to Controller. What is a category you say? Essentially, it's an extension to a class’ implementation. The syntax for a category is only slightly different from class syntax. For our category let’s create a new empty file and name it ToolbarDelegateCategory.h. Below is what the interface file for a category looks like:


#import <Cocoa/Cocoa.h>
#import "Controller.h"

@interface Controller (ToolbarDelegateCategory)

- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
    itemForItemIdentifier:(NSString *)itemIdentifier
    willBeInsertedIntoToolbar:(BOOL)flag;
- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar;
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar;

@end

Notice that the code block for instance variable declarations is absent. This is because categories cannot add instance variables to a class interface, only methods. Also notice how we define the category. The category name follows the class name in parentheses. Finally, when declaring a category you must import the class’ interface if you wish to use any of the class’ instance variables in your category's methods, which we will do.

Now create another file with the same name except with an "m" extension, whose contents are the following:


#import "ToolbarDelegateCategory.h"

@implementation Controller (ToolbarDelegateCategory)

- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
    itemForItemIdentifier:(NSString *)itemIdentifier
    willBeInsertedIntoToolbar:(BOOL)flag
{
}

- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
{
}

- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
{
}

@end

Categories are useful for several reasons. The methods put in the category file are as good as being in the class implementation itself. Thus, we can extend any class’ behavior in a limited -- yet often times sufficient -- manner without going through the trouble of subclassing. As mentioned previously, categories are limited in the fact that they can’t add instance variables to a class definition. Like subclasses, categories can override methods declared in the class interface.

Categories are useful in breaking up large classes into more manageable files -- code reuse -- and they can save time in compilation. Additionally, if you make your categories right, there is no reason that you can’t reuse the files in another project with minimal changes. For more information on categories, check out Chapter Four of Object-Oriented Programming and the Objective-C Language entitled "More Objective-C."

Implementing the Delegate Methods

The Standard Items

Let’s go ahead and start implementing the toolbar delegate methods now. The first one we want to implement is -toolbarAllowedItemIdentifiers:. The purpose of this method is to return to the toolbar an array of strings containing the identifiers of all the toolbar items that will go in the toolbar. For now we’ll simply create an array of the four standard toolbar item identifiers we said our toolbar would have (we’ll add our custom items after we get everything working with the standard set of items). Note that the order of the identifiers in the array is the order that the toolbar items will appear in the customization palette. Here is the first method:


- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
{
    return [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
				     NSToolbarSpaceItemIdentifier,
				     NSToolbarFlexibleSpaceItemIdentifier,
				     NSToolbarCustomizeToolbarItemIdentifier, nil];
}

All we did was use NSArray’s -arrayWithObjects: method to create the return array. When using this method, remember that the list of objects must be terminated by nil. That’s all there is to it.

The next method we want to implement is -toolbarDefaultItemIdentifiers:. This method is similar in function to the method above. The difference is that it returns an array of identifiers for items that make up the default toolbar. Our default toolbar will consist of the customize toolbar item placed on the far right, which is achieved by inserting a flexible space item before the customize toolbar item. Again, the order of the identifiers in the array defines the order of the items in the default toolbar.


- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
{
    return [NSArray arrayWithObjects:NSToolbarFlexibleSpaceItemIdentifier,
				     NSToolbarCustomizeToolbarItemIdentifier, nil];
}

The third and final method we are required to implement is -toolbar:itemForItemIdentifier:willBeInsertedIntoToolbar: (aka -toolbar). This method contains the meat of a toolbar implementation. Here is where we actually create the toolbar items.

Initializing an instance of NSToolbarItem is done using the -initWithIdentifier: method. In this method we supply the identifier of the toolbar item given by the argument of itemforItemIdentifier:. If the identifier is one for the standard toolbar items, then an instance of NSToolbarItem is initialized, fully configured to perform the specified standard function.

If the identifier is one other than the standard identifiers, then only the identifier is set. It is the duty of the programmer to perform further configuration for the item, which we will do soon for our custom items. For now, let's set things up to work with just the standard items:


- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
    itemForItemIdentifier:(NSString *)itemIdentifier
    willBeInsertedIntoToolbar:(BOOL)flag
{
    NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
    return [item autorelease];
}

We send the item an autorelease method since the toolbar retains the item that is returned by this method. We have no use for it otherwise, so we give up ownership of the object to the toolbar. The reason I used two lines of code here rather than one is that we’re going to sandwich some additional code between these two lines when we get to building our own toolbar items.

With the required delegate methods taken care of, there are a couple of other things we need to do before all is ready to go. One is to actually create the toolbar instance. We will do this in Controller’s -awakeFromNib method by calling a method called -setupToolbar, which we implement in our category in the following way:


- (void)setupToolbar
{
    NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"mainToolbar"];
    [toolbar autorelease];
    [toolbar setDelegate:self];
    [toolbar setAllowsUserCustomization:YES];
    [toolbar setAutosavesConfiguration:YES];
    [mainWindow setToolbar:[toolbar autorelease]];
}

What we did here was to create a new instance of NSToolbar and initialize it with initWithIdentifier:. The toolbar identifier can be anything you like. It is used internally by NSToolbar, and we won’t have any direct interaction with it.

Related Reading

Learning Cocoa
By Apple Computer, Inc.

In the next line we set the delegate object of the toolbar to self, and then set some options for the toolbar -- the toolbar lets the user customize it, and it will automatically save any changes the user makes. In the last line of code we send a setToolbar message to mainWindow with [toolbar autorelease] as the argument. The same thinking that was behind autoreleasing the toolbar items before returning them applies here -- we want to relinquish ownership of toolbar to mainWindow.

The final thing we have to do is set up a menu item that will allow the user to open the customization panel. NSWindow provides us with an action method for running the toolbar’s customization palette called -runToolbarCustomizationPalette:. What we’ll do in Interface Builder (IB) is make mainWindow the target of a -runToolbarCustomizationPalette: action message sent by a "Customize Toolbar" menu item that we’ll add to the Window menu.

Now then, in IB add a "Customize Toolbar" menu item to the Window menu (you can create your very own ellipsis with the key combo option-semicolon). Connect this menu item to the -runToolbarCustomizationPalette: action of your main window. Save your work, compile the project, and you’re set to go with the basics.

When the application first launches you should see the customization toolbar item on the far right. You should be able to run the customization palette by clicking this item or via our menu item. Let’s keep going and add those three custom items we mentioned earlier.

The Custom Toolbar Items

Implementing custom toolbar items is only slightly more involved than using standard toolbar items. All that is required is a bit of post-initialization configuration. Recall that the three toolbar items we are going to make are an Add control, a Remove control, and a Search field. These toolbar items will have the identifier AddItem, RemoveItem, and SearchItem. To support this, add these three strings to the arrays returned by -toolbarAllowedItemIdentifiers: and -toolbarDefaultItemIdentifiers:. The -toolbarAllowedItemIdentifiers: method should look like this:


- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar*)toolbar
{
    return [NSArray arrayWithObjects:NSToolbarSeparatorItemIdentifier,
				     NSToolbarSpaceItemIdentifier,
				     NSToolbarFlexibleSpaceItemIdentifier,
				     NSToolbarCustomizeToolbarItemIdentifier, 
    @"AddItem", @"RemoveItem"
    @"SearchItem", nil];
}

And -toolbarDefaultItemIdentifiers: will look like:


- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar*)toolbar
{
    return [NSArray arrayWithObjects:@"AddItem", @RemoveItem", @SearchItem", 
NSToolbarFlexibleSpaceItemIdentifier,
NSToolbarCustomizeToolbarItemIdentifier, nil];
}

Now, let’s go back to the method where we create the items and see what we have to do.

As we last left it, -toolbar:itemForIdentifier:willBeInsertedIntoToolbar: had the following implementation:


- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
    itemForItemIdentifier:(NSString *)itemIdentifier
    willBeInsertedIntoToolbar:(BOOL)flag
{
    NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];
    return [item autorelease];
}

This worked fine for the standard items because to implement them all we had to do was initialize an instance of NSToolbarItem with the given identifier, and we got a complete toolbar item. Now we have some non-standard items which require more than a simple initialization.

To accomplish this we’ll add a chain of if-statements in which we will compare itemIdentifier to our three custom identifiers. Within each individual if-statement we’ll run the configuration code specific to the item whose identifier matches itemIdentifier. This chain of if-statements will be sandwiched between the two existing lines of code in -toolbar….

Let’s stay organized and first add the skeleton of the if-statement chain to this method:


- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar
    itemForItemIdentifier:(NSString *)itemIdentifier
    willBeInsertedIntoToolbar:(BOOL)flag
{
    NSToolbarItem *item = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];

    if ( [itemIdentifier isEqualToString:@"AddItem"] ) {
	// Configuration code for "AddItem"
    } else if ( [itemIdentifier isEqualToString:@"RemoveItem"] ) {
	// Configuration code for "RemoveItem"
    } else if ( [itemIdentifier isEqualToString:@"SearchItem"] ) {
	// Configuration code for "SearchItem"
    }

    return [item autorelease];
}

As I mentioned before, all configuration code unique to each of these three items will go in the corresponding if-statement. Implementing the first two of these three items will be done entirely in code, while implementing SearchItem will be done partially in Interface Builder. Let’s take a look at the first two now.

The Add and Remove Items

In configuring these two items, we set five properties that make up a minimum useable control. A toolbar item has two labels -- one for when the item is displayed in the toolbar, and the other for when the item is shown in the customization palette.

The methods -setLabel: and -setPaletteLabel: do this, so we’ll call these two methods as part of our configuration. Additionally, we want to give our toolbar item an image. You will see that I have included two images for use in this application in the project folder -- Add.tiff and Remove.tiff. Toolbar item images are set with the method -setImage:.

Finally, we want to set the target of the toolbar item and the action to be invoked in the target. This is done with -setTarget: and -setAction:, respectively. Here’s the code to do these five things for AddItem and RemoveItem:


if ( [itemIdentifier isEqualToString:@"AddItem"] ) {
	[item setLabel:@"Add Record"];
	[item setPaletteLabel:[item label]];
	[item setImage:[NSImage imageNamed:@"Add"]];
	[item setTarget:self];
	[item setAction:@selector(addRecord:)];
    } else if ( [itemIdentifier isEqualToString:@"RemoveItem"] ) {
	[item setLabel:@"Remove Record"];
	[item setPaletteLabel:[item label]];
	[item setImage:[NSImage imageNamed:@"Remove"]];
	[item setTarget:self];
	[item setAction:@selector(deleteRecord:)];
    } else if ( [itemIdentifier isEqualToString:@"SearchItem"] ) {
	// Configuration code for "SearchItem"
    }

There are several things to note here. One, notice that we set the palette label to be the same as the toolbar label. If you wanted, you could give the item a more descriptive palette label, since space is not as much of a premium there as in the toolbar. We used the -label method to obtain the label string rather than duplicating the static string. Not such a big deal, but just one less thing to change if we wanted to change the label.

Second, notice how we created an NSImage instance from the image file to pass along in the -setImage: message to the item. NSImage’s +imageNamed: is extremely convenient for this type of work. You don’t have to pass the file path or extension -- just the name -- and +imageNamed: will look for the image in the application’s bundle.

Finally, note that the actions of these two toolbar items are set to the same actions as the Add and Remove buttons in the interface. For the complete rundown on everything you can fiddle with in a toolbar item, check out the NSToolbarItem reference.

The Search Item

Now we get to the Search toolbar item. We’re going to use Interface Builder to do some of the work here. To prepare for that add the following outlet declaration to Controller.h:

IBOutlet id searchItemView;

Then go to Interface Builder and read in the Controller.h file to update the NSController class interface, making the outlet available to us. What we want to do is create a control in IB that will be encapsulated in a toolbar item. Since there is no toolbar or anything of that sort to put the text field into, we build our toolbar item in a standalone instance of NSView. We’ll add an NSView instance to our nib, like we learned how to do last time, and then place a text field within this view.

We also need to set the size of the view to match the size of the text field. We do this to make the toolbar item as compact as possible. A word of caution on this -- don’t manually size the view to fit the text field. Rather, you should size things in the Info Panel under Size. For our text field, change the bottom/left of the frame from its current setting to x = 0, and y = 0. This will wedge the text field into the bottom left-hand corner of the view. Next, take note of the width and height of the text field frame, and in the view’s size info, change the height and width to those values.

Sizing like this becomes especially important when you’re working with buttons or other objects with shadows. We don’t want to cut off the shadows of the controls, otherwise we would have some funky-looking controls. Now, to finish things off here make the connection between the outlet searchItemView and the NSView instance which contains the text field.

One last thing we need to do before closing up shop is to connect NSController to the delegate outlet of the text field. We are doing this so we can take advantage of a nifty delegate method of NSControl later on. Do this now, save your work, and return to Project Builder.

The code for configuring the search toolbar item starts out in the same way the others do by setting the labels of the item. After that, we do things differently. Rather than setting the toolbar item image, we set the item’s view. The toolbar item becomes the set of controls contained in the view object -- in our case the single text field. This makes it easy to create complex toolbar items that contain a variety of controls such as pop-up buttons or switch buttons.

After setting the toolbar item’s view, we have to tell the item how large and how small it may be sized. This is done using the -setMinSize: and -setMaxSize: methods. We will set both of these to the current size of searchItemView, which we get directly from the view.

Due to the nature of the search field, we don’t need to set the target or action -- we’re going to take advantage of Controller’s status as the text field’s delegate to work with this control. Let’s finally take a look at the code that makes all of this happen (this is the third else-if statement):


} else if ( [itemIdentifier isEqualToString:@"SearchItem"] ) {
    NSRect fRect = [searchItemView frame];
    [item setLabel:@"Search Records"];
    [item setPaletteLabel:[item label]];
    [item setView:searchItemView];
    [item setMinSize:fRect.size];
    [item setMaxSize:fRect.size];
}

First we obtained the frame of searchItemView and stored it in fRect -- this will come in handy when we need to set the minimum and maximum size of the control. In the next two lines we set the labels as we did previously. Following that, we invoke -setView: to set the view of the toolbar item to searchItemView. In the next two lines we use -setMinSize: and -setMaxSize: to set the corresponding sizes of the item. These methods both take NSSize arguments, which are provided courtesy of fRect.size. With that we’re finally ready to compile and try out our new toolbar.

Now, what I want to do is show you one more thing relating to toolbars, and then I’ll describe how the search function is implemented.

Toolbar Item Validation

Toolbar item validation refers to the method of providing some test on the state of your application to determine whether or not a toolbar item should be enabled or disabled. For example, in our situation, the tenets of good interface design would have us enable the remove item only when one or more records in the address book are selected. In this case the number of selected rows tells us something about the state of the application, which we can compare against specific validation criteria.

This behavior is conveniently supported using the NSToolbarItemValidation protocol’s -validateToolbarItem: method. For any toolbar item that has a valid target/action pair, this method is called in the target object. Since NSController (via self) is the target of the Remove item, we’ll implement -validateToolbarItem: in ToolbarDelegateCategory. In ToolbarDelegateCategory.h now add the declaration for this method.

The return value of this method is a BOOL, so we have to perform some test within the method implementation that returns a BOOL. We already said that we want the Remove item to be enabled only when there are records selected, so the test here would be to check if there are any selected records in the tableview. NSTableView provides the method -numberOfSelectedRows to do this for us. If the number is greater than 0, then we validate the item by returning YES. Right here we have all we need to validate the Remove toolbar item:


- (BOOL)validateToolbarItem:(NSToolbarItem *)theItem
{
    if ( [theItem action] == @selector(deleteRecord:) )
	return [tableView numberOfSelectedRows] > 0;
}

If you have several toolbar items with the same target invoking different actions, then it is imperative that you test, in some way, that the item is the toolbar item we want to validate before passing judgment on validation. A good way to do this is by comparing the action of the item to the method we want to validate. This is logical since what we really want to do is enable or disable a behavior rather than an item.

In this case, we checked to see if the action of the item in question was the deleteRecord: method. If so then we check to see how many rows are selected. If one or more are selected, then YES is returned, otherwise NO gets sent back. And that’s how toolbar item validation works!

Searching

Getting the search item to function requires some heavy modification to the way records are stored in the address book application. Rather than lead you through a detailed, step-by-step description of every change that was made, I will give you an overview of what’s going on and leave you with the code to download and study. The project folder that has all of the additions made can be downloaded here.

As it stands now, the individual records are stored in an instance of NSMutableArray assigned to the instance variable records. Whenever information is displayed in a tableview, records is the provider of that information via the NSTableDataSource methods.

Now, suppose we want to restrict the information displayed in the tableview to some subset of records based on the search criteria. How would we go about obtaining and using a subset of records as a data source without modifying the contents of records? The way we go about this is to introduce two more NSMutableArray instance variables called subset and activeSet. To see where activeSet occurs in the code, do a search for it in the Find panel.

Rather than using the variable records in the data source methods, we use activeSet. The trick is that the NSMutableArray object assigned to activeSet changes depending on the function being performed. When the application loads, activeSet is set to equal records immediately after a mutable array has been assigned to records. In this way both activeSet and records point to the same object in memory. Two variables, one object.

Now when we perform a search, we create another instance of NSMutableArray and assign it to the variable subset. The search will consist of enumerating records and comparing the first and last name strings in the record dictionary to the search string. If there is a partial match, then that record is added to subset. After records has been completely enumerated, we assign activeSet to subset, and the tableview will then display the contents of subset -- that is, the search results. When the search field is cleared, activeSet is once again set to equal records.

Also in Programming with Cocoa

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Build an eDoc Reader for Your iPod, Part 3

Understand now that when we speak of adding objects to subset, we’re actually adding the pointers to the particular objects to subset, and subset is sending retain messages to those objects. So now both records and subset have ownership over a shared set of objects. This is the same “two variables, one object” situation we had above, except that the variables are elements of subset and records. This is significant to the way the tableview works. In particular, when you edit information directly in the table, that information is not changed solely in records contained in subset (via activeSet); the changes are also reflected in object.

Here again is how it works: When records is first initialized, we set activeSet equal to it. When we search we assign activeSet to subset. Finally, in the search method we will have an if-statement checking to see if the length of the search string is 0. If so, then we reassign activeSet to records.

So that’s what’s going on with our change of variable names and introduction of new instance variables. How do we make the search function work? Recall that in Interface Builder we set NSController to be the delegate of the text field in searchItemView. The reason we did this is so we could implement a useful delegate method of NSControl called -controlTextDidChange:, which is invoked whenever the text in the text field changes. Thus, a new search will be performed after each letter is typed in the search field.

Add to Controller.m the following definition of -controlTextDidChange:


- (void)controlTextDidChange:(NSNotification *)aNotification
{
    NSString *searchString = [[[aNotification object] stringValue] lowercaseString];
    NSEnumerator *e = [records objectEnumerator];
    NSString *fnString, *lnString;
    id object;

    if ( [searchString length] == 0 ) {
	activeSet = records;
	[tableView reloadData];
	return;
    }

    [subset release];
    subset = [[NSMutableArray alloc] init];

    while ( object = [e nextObject] ) {
	fnString = [[object objectForKey:@"First Name"] lowercaseString];
	lnString = [[object objectForKey:@"Last Name"] lowercaseString];
	
	if ( [fnString hasPrefix:searchString] || [lnString hasPrefix:searchString] )
	    [subset addObject:object];
    }
    activeSet = subset;
    [tableView reloadData];
}

What we have here is an enumerator for records that checks every record contained in that array to see if the search string is a prefix of either the first name value or the last name value. If so, we add it to subset and continue enumerating. Once the enumeration has completed we set activeSet to point to subset and then tell tableView to reload its data.

There are a couple of things to note here. First, we make the search case insensitive by sending lowercaseString messages to all of the strings involved and use the returned strings in the actual search.

Additionally, note the argument of this method, NSNotification. This is a class which we have not discussed yet in this series of columns. If you read the class documentation on this method in the NSControl and NSTextField specifications you will see that the controlTextDidChange: method is called in the delegate by a notification. A notification is a way of communicating between objects who have no knowledge of each other using a broadcast paradigm. The way it works is that objects can register themselves with a notification center, which is an instance of the class NSNotificationCenter. By registering itself in this way, an object becomes an observer which responds to only certain named notifications. In this registration process the object indicates which one of its methods will be invoked in response to the notification.

You register your object with the default notification center using the method -addObserver:selector:name:object:. Here observer is the object to respond to the notification, selector is the method to be invoked in the observer, name is the name of the notification which observer shall respond to, and the object argument is used to restrict which notifications the observer can respond to. To respond to all notifications with the given name, pass nil as this argument. Otherwise, you can have your observer respond only to notifications that have been posted by an object indicated in this argument.

Conveniently, by assigning NSController as the delegate of the search text field, this has all been taken care of for us behind the scenes. All we had to do was implement -controlTextDidChange:.

The other side of the story with notifications is the object posting a notification. Objects post notifications to the notification center using either the -postNotification: or the -postNotificationName:object: of NSNotificationCenter. In the second method object is often self. When a notification is posted to the notification center it is then broadcast to all observers of that notification. The notification center acts as a middle man in this one-way communication between two stranger objects.

So, back to our search method. The object which sent the notification can be accessed by passing an object message to the notification object. This is how we gain access to the text field whose contents have changed and retrieve the string contained within.

Now, with this method added to your code, and the needed changes made as described above, you should have a cool little Search function in your application.

This concludes learning how to make toolbars. As always, I highly recommend reading all of the relevant class documentation as I have only touched on a few possibilities. See you next time!

Copyright © 2009 O'Reilly Media, Inc.