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


Programming With Cocoa

Cocoa Diversions; More on Views

by Michael Beam
02/15/2002

Mac OS X gave us a host of interface enhancements, for better or for worse. Some are seemingly frivolous, such as the whiz-bang animations that dominate Aqua, and others haven proven truly useful, such as the Finder’s column view. Personally, I dig it all.

In today’s column we’re going to look at two of these Aqua enhancements—-one seemingly frivolous and the other not. One of them--animated window resizing--can be readily experienced within System Preferences: just choose a preference pane and you’ll see the System Preferences window do an animated resize while the selected preference pane loads.

The other Aqua interface element we’re going to look at can be seen in applications such as OmniWeb and Mail. In OmniWeb you see it whenever you open your bookmarks or history, and Mail uses it to keep your mailboxes tucked away when you don’t need them, but always close at hand when you do. No, “it” is not an overhyped and seemingly underwhelming technology (although it might be if I don’t get to my point). “It” is drawers. In addition to learning about animated window resizing, we’ll also delve into the wonderful world of drawers.

OmniWeb
Here is how OmniWeb uses drawers to hold bookmarks.

You may be asking yourself what these two things have in common for the humble programmer. After all, they’re pretty distinct UI elements, so why cover them in the same column? The answer lies in how they’re implemented in Interface Builder, specifically a neat trick you can use to make user interfaces independent of a window. Before I get ahead of myself, let's make ourselves a project.

Create a new Cocoa Application project and, for lack of a better name, call it Aqua Stuff. Then open MainMenu.nib in Interface Builder, and we’ll start things out with the drawer half of the column.

Adding a Drawer

Adding a drawer to a window is easy in Interface Builder. The Cocoa-Windows palette is the storehouse for drawers. First drag the drawer object onto the Instances tab in your nib window. For now disregard the window with the preattached drawer in the top-right of the palette-—I’ll talk about that after we do things the long way.

Related Reading

Learning Cocoa
By Apple Computer, Inc.

So what do we do with a drawer now that it's an object in our nib file? Let's take a look at some of the characteristics of this drawer, particularly what outlets are declared.

Selecting the NSDrawer instance and bringing up its Connections info panel (Command-2) reveals that an instance of NSDrawer initially has three outlets; they are named contentView, delegate, and parentWindow. We’re interested in the first and last of these three.

The parentWindow outlet simply connects to the window we want to have the drawer attached to. In our case, we’ll use the window that came with MainMenu.nib. Drag a connection wire from the drawer icon to the window icon and make the connection to parentWindow.

The second outlet we’re concerned with, contentView, is a link to the NSView that contains the user interface to be displayed in the drawer. To make this view we’re going to use the same NSView container that we used for drawing, which, if you recall, is found in the Cocoa-Containers palette. However, rather than dragging this view onto the main window like we did to set up a drawing view, we’re going to drop it on the Instances tab in the same way we did the drawer. Do this now and you will see the view open within a window.

This does not mean that the view is a part of the window in the actual interface, that’s just how we work with it in Interface Builder. The thick grey border around the view in the window indicates that the view is independent of the window. In fact, the view is not a subview of any other window or view; it is all alone. We’re going to change that, however, by setting this view to be the content view of the drawer. In the same way we connected the window to the drawer, connect the NSView object to the contentView outlet of the drawer.

With the NSView we can now go about making the user interface that we want tucked away in the drawer. The Aqua Human Interface Guidelines (http://developer.apple.com/techpubs/macosx/Essentials/AquaHIGuidelines/index.html) states that drawers should contain often-used controls that needn’t be visible at all times. In the OmniWeb screenshot above we see that the folks at Omni have put some variant of an outline view in the bookmarks drawers. You can put anything in a drawer content view that you can put in a window. There are some issues to consider when building your drawer interface. Primarily, there are size constraints to think about with a drawer.

Comment on this articleOK, so maybe animated window resizing isn't that frivolous, but what about using drawers? Questions and comments here.
Post your comments

Before we discuss how drawers are sized, let's take a look at the attributes of a drawer. You’ll see that the only attribute that we can change is the preferred edge of the parent window that the drawer will be attached to. The astute reader will notice the use of the word "preferred." What this means is that, by default, the drawer will be attached to the indicated edge of the window.

However, if when opening the drawer there is no room for it between the preferred edge of the window and the adjacent edge of the screen, the drawer will slide out from the opposite side of the window. Smart, huh? For the purposes of this discussion let's leave the preferred edge set to left and assume that drawers will be attached to the left or right side of the window.

Now let’s talk about how drawers are sized. If you open the Size Info panel for the drawer (Command-3) you will see the various size settings we have at our disposal. At the top we can set the width of the content size, but not the height. Rather, the height is calculated (if the preferred edge is top or bottom, the reverse will be true: you can set the height, but the width is calculated). The width should match the width of your own content view. The height (or width) of the drawer is calculated from the height of the window minus the leading offset and the trailing offset.

"But what exactly are these offsets?" you ask. Excellent question!

The leading offset is the distance from the left or top edge of the drawer to the left or top edge of the window. Likewise, the trailing offset is the distance from the right or bottom edge of the drawer to the right or bottom edge of the window. Whether we talk about right and left or top and bottom depends on which edge of the window the drawer slides out from. Note that in this context the top of the window is not where you think it is. Here, the top of the window is not at the top of the title bar, but rather it is at the bottom of the title bar. Have a look at the images below to see how this all fits together.

Offsets
What the offsets mean for a drawer attached to the left or right of the parent window.

Offsets
What the offsets mean for a drawer attached to the top or bottom of the parent window.

At any rate, the height of a drawer is calculated from these values. The offset distances are kept constant when resizing the window, which means the height of the drawer is resized with the window.

Drawers allow the user to change the width by dragging their outside border. We can restrict this behavior by setting the minimum and maximum content sizes of the drawer. Note that the size of the NSView we are using as the content view can be adjusted from the Sizes Info panel for that particular view. You should set the frame size to something appropriate to display within the drawer, based on the variables we just discussed. Finally, make sure that the springs and struts are set up for your various controls so they resize and reposition themselves nicely when the user resizes the drawer.

The last thing we have to consider when working with drawers is how to open and close them. If you look at the NSDrawer’s Class Info, you will find that it has three actions: open:, close:, and toggle:. If we send an open message to the drawer it opens (and does nothing if already open). If we send a close message to the drawer it closes (and does nothing if already closed). A toggle message to the drawer will open a closed drawer and close an open drawer.

We can set up a rudimentary interface for opening and closing the drawer by adding a button to the main window wired to the toggle: action method. Do this now with a button titled “Toggle”, and do a test run of your interface. Everything should work as advertised with the drawer.

Now about the other drawer object in the Windows palette, the one with the drawer already attached to the window. This item will add not only a drawer to the nib, but also the parent window and content view, with those two connections already made. So it saves a little work if you know from the outset that your window will have a drawer.

Now let's move onto the second half of our column and learn about animated window resizing.

Resizing Windows

Implementing this behavior is almost as easy as doing drawers. It just requires a little geometric know-how and a bit of code. We’ll work off of our current drawers project, so we’ll be hanging around in Interface Builder for a while longer.

What we want to do is create two additional NSView instances in the same manner that we did for the drawer’s content view. So, drag two more NSView containers into the nib window. You should probably change the names of your view icons in the nib window to keep everything straight in your head. I suggest naming our two newest additions smallView and largeView for reasons that will become apparent.

Using the Size Info Panel, make one of these views smaller and the other larger. In the small view add a button title “Go Large”, and in the large view add a button to the interface titled “Go Small”. These buttons will invoke action methods that trigger the resizing. Additionally, remove the drawer control button from the main window and input two similar buttons titled “Toggle” in each of the two new views. Make sure you reconnect each of these drawer-control buttons to the drawer’s "toggle: action." Your two views should look similar to the ones shown below.

Offsets
What our smaller window will look like.
Offsets
What our larger window will look like.
 

Now subclass NSObject to create a controller class-—named Controller—-and add three outlets: smallView and largeView for the two views, as well as mainWindow to connect to the main window. Also create two actions, goLarge: and goSmall:. Make the appropriate connections between the buttons and the actions, and create the files to be added to the project for this controller object. Now let's go do some coding.

In Project Builder

Now this is where the fun really starts! In Project Builder let's start out with the awakeFromNib method in Controller. Since we didn’t put an interface in the main window in Interface Builder, we want to set one of the views to be the initial content view of the window at start up. A content view in a window is the view that contains the window’s contents, much the same as the content view of a drawer. To set a content view for a window we use NSWindow’s -setContentView: method (fancy that), with the view we want to set as the content view supplied as the argument. We also have to resize the window to fit the view we’re setting as the content view, otherwise the window will be sized as it was in Interface Builder.

Another thing we want to do is obtain and store in instance variables the sizes of the frame rectangles of the two views we’re working with. To support this add the instance variables in Controller.h, shown below:

NSSize smallSize, largeSize;

Finally, we’re going to create a view that is blank with no controls. This will be used as an intermediate view to display while the window is resizing. Add the instance variable NSView *blankView to Controller.h as well. Controller.h should now look like the following:


@interface Controller : NSObject
{
    IBOutlet NSView *largeView;
    IBOutlet NSWindow *mainWindow;
    IBOutlet NSView *smallView;

    NSSize smallSize, largeSize;
    NSView *blankView;
}
- (IBAction)goLarge:(id)sender;
- (IBAction)goSmall:(id)sender;

@end

Now let's take a look at –awakeFromNib in Controller.m :


- (void)awakeFromNib
{
    smallSize = [smallView frame].size;
    largeSize = [largeView frame].size;

    [mainWindow setContentSize:smallSize];
    [mainWindow setContentView:smallView];
    
    blankView = [[NSView alloc] init];
}


In the first two lines we use the NSView method--a frame to return the receiver’s frame rectangle--then using the structure member operator, we obtain the size member of the NSRect and assign it to the appropriate variable. This is done for both the small and large views. In the next two lines we set the window’s content size to smallSize, and we set the contentView of mainWindow to be smallView. Finally, before leaving awakeFromNib we create and initialize an instance of NSView and assign it to blankView. With these preliminaries out of the way lets get into the meat of this class.

We’ll now create a method called – resizeWindowToSize:. The argument of this method is an NSSize datatype-—the size that we want to make the new window. Let's dive right into this method and then we’ll go through it piece by piece.


- (void)resizeWindowToSize:(NSSize)newSize
{
    NSRect aFrame;
    
    float newHeight = newSize.height;
    float newWidth = newSize.width;

    aFrame = [NSWindow contentRectForFrameRect:[mainWindow frame] 
styleMask:[mainWindow styleMask]];
    
    aFrame.origin.y += aFrame.size.height;
    aFrame.origin.y -= newHeight;
    aFrame.size.height = newHeight;
    aFrame.size.width = newWidth;
    
    aFrame = [NSWindow frameRectForContentRect:aFrame 
styleMask:[mainWindow styleMask]];
    
    [mainWindow setFrame:aFrame display:YES animate:YES];
}

The first thing we do after declaring a local NSRect variable to work with is to set the variables newHeight and newWidth to the values of the height and width members of the newSize argument. These values will be used in calculating the new dimensions and location of the window’s frame.

The next line is an important piece of code. What is does, in effect, is return the NSRect for the frame of the window’s current content view. Why don’t we just work with the rect returned by [mainWindow frame]? Because -frame returns the NSRect that defines the entire window-—we want to neglect the title bar and any other "style" elements of the window. We invoke in NSWindow the class method +contentRectForFrameRect:styleMask:. The NSWindow class object takes the frame of mainWindow as the first argument, and then the style mask of mainWindow. The style mask tells us what style elements the window uses, such as a title bar. We don’t have to worry about the particulars of style masks here—we just ask mainWindow what its style mask is using –styleMask and pass that along to +contentRect….In the image below –frame returns the rectangle drawn in green, and +contentRect… takes that rectangle and returns the rectangle drawn in red.

Offsets
What the offsets mean for a drawer attached to the left or right of the parent window.

In the next four lines we do some clever math on the dimensions of the frame returned by +contentRectForFrameRect:styleMask: to resize it to newWidth and newSize. The purpose of this is to ensure that the upper-left corner of the window doesn’t change when we resize the window.

Also in Programming with Cocoa

Understanding the NSTableView Class

Inside StYNCies, Part 2

Inside StYNCies

Build an eDoc Reader for Your iPod, Part 3

The next method, +frameRectForContentRect:styleMask: does the exact opposite of what +contentRectForFrameRect:styleMask: did. That is, it takes a content rectangle (red rect in the image above), and calculates the frame for a window (green rect above) by taking into account the style mask for the particular window.

Finally, we make the magic happen in the last line of code with - setFrame:display:animate:. This method takes aFrame—-the frame we want to resize the window to-—as the first argument and two booleans as the last two arguments. The display: argument tells the window whether it should redraw at each step of the animation, and animate: tells the receiver whether the resize operation should be animated.

So why all the hubbub just to set the frame of a window? Couldn’t we just have passed the frame of the desired view to setFrame:…? The reason is that frames, as NSRects, are more than sizes; they specify a location on the screen as well. Thus, blindly passing a the frame of smallView or largeView to this method would do more than resize the window; the window would fly across the screen as well, which is definitely what we don’t want. We want to maintain the position of the top-left corner of the window.

Now, let's get to those two long-forgotten action methods, goSmall: and goLarge:. With the creation of -resizeWindowToSize:, implementing these two methods will not be difficult. The implementations for these two methods are the same, except for using two different views. The following is the code for these two methods:


- (IBAction)goLarge:(id)sender
{
    [mainWindow setContentView:blankView];
    [self resizeWindowToSize:largeSize];
    [mainWindow setContentView:largeView];
}

- (IBAction)goSmall:(id)sender
{
    [mainWindow setContentView:blankView];
    [self resizeWindowToSize:smallSize];
    [mainWindow setContentView:smallView];
}

Notice that the first thing we do in each of these methods is to set the content view of mainWindow to blankView. The reason we do this is to make the resize transition a little easier on the eyes. If we didn’t do this we would see the old content view stay where it started while the resize happens around it.

System Preferences does something similar by having a transition view show the icon of the preference pane and a message indicating that the pane is loading. After you get things working, try commenting out these lines to see what happens. With blankView set as the contentView of mainWindow, we now invoke our –resize… method with the desired NSSize instance variable passed along as the argument. Finally, we set the content view of the window to the desired view.

So that’s the story. Give it a go and see how it works. With the drawer open you will see the effects of those offset variables-—the drawer resizes along with the window. I hope you’ve enjoyed learning about these two Aqua enhancements. Next time we're going to look into some more new stuff from Aqua as we learn about toolbars and how to add one to your application. The project folder can be downloaded here. I hope you’ve enjoyed learning about these two Aqua enhancements.

Copyright © 2009 O'Reilly Media, Inc.