DBPrefsWindowController

Sunday, March 11, 2007

Application preference windows aren’t difficult to create. Just create an NSWindowController, set up a toolbar, create a nib file with either a series of custom views, or with an NSTabView object. The problem is, just setting up the toolbar and view swapping mechanism takes a lot of code, and it’s basically the same for every application. Sounds like an excellent opportunity to create a new class.

If you’re not already familiar with how to set up a preferences window, check out John Devor’s excellent Simple Preferences example. His example shows exactly how most preference windows are created in Cocoa applications.

The problem is, through no fault of John or anyone else that uses this technique, that it’s high maintenance code. If you take a look at his example’s PreferencesController.m file (an NSWindowController subclass), you’ll see that you need to add ten lines of code to five different methods, plus add a new static variable, to add one additional toolbar icon. If you want to move this code into another project, you’ll have to modify 50 lines of code to change the labels on the five toolbar icons (assuming you want the identifiers in the source code to still make sense).

My goal was to try to reduce this to one line of code for each toolbar icon. As with John’s example, I wanted each view to be an individual custom NSView in Interface Builder. I find this much easier to work with than trying to manage multiple tabs in an NSTabView.

It’s also important to have all of the preference views in their own nib file, so that they aren’t loaded into memory unless and until the user displays the preferences window.

So I’ve created an NSWindowController subclass named DBPrefsWindowController.

Download: DBPrefsWindowController 1.1.2[1]

Using DBPrefsWindowController

If you’re comfortable with subclassing and working with Interface Builder, you’ll be able to figure out how to use it from the included example project. If not, check back tomorrow when I post a full tutorial on how to use DBPrefsWindowController.

Basically you just subclass it and connect your .h file to a nib file of custom views. The nib file should be named “Preferences.” Then just call -addView:label: to specify which views to display and what labels to use:

- (void)setupToolbar
{
    [self addView:generalPreferenceView label:@"General"];
    [self addView:colorsPreferenceView label:@"Colors"];
    [self addView:playbackPreferenceView label:@"Playback"];
    [self addView:updatePreferenceView label:@"Update"];
    [self addView:advancedPreferenceView label:@"Advanced"];
}

When setup this way, DBPrefsWindowController will look for image files with names that match the labels supplied here. If your application is localized, you can instead call -addView:label:image: to specify the images to use for the toolbar icons.

That’s really all there is to it. If you’ve already created a nib file with separate views for your preferences window, you can just use it instead of creating a new nib file. Either rename your nib file “Preferences,” or override the +nibName class method so it returns the name of your existing nib file.

Cross-fading Views

I’ve designed the DBPrefsWindowController class so that it follows both Apple’s HIG and the IndieHIG as closely as possible. However, there was one feature that I wanted that violates the IndieHIG. I wanted the window’s contents to fade between views. I’m pretty hard-core about sticking to Apple’s HIG, but I’ve seen this effect used in the preferences window of many other applications, so I don’t think I’m violating any norms here.

Here’s a short list of applications that I was able to find that cross-fade views in their preferences windows:

 - Apple’s DVD Player
 - AppZapper
 - CSSEdit
 - FireFox
 - Transmit
 - Xtorrent

After doing a bit of research, I decided that using Cocoa’s NSViewAnimation class was the best approach.

The most difficult part was getting the fading views to stay put while the window is resizing. The combination of the view animation routines and the window resizing animation cause the fading views to scroll up and down as the window resizes. In fact, in all of the applications listed above, save for the one by Apple, you’ll notice the view move up or down as the window resizes. Actually FireFox avoids the problem by fading out a view, resizing the window, then fading in the next view.

After many more days of playing with this than I should have spent, I found a solution. So you’ll notice that the fading views stay put, just as you would expect, while the window resizes.

Documentation

The DBPrefsWindowController class is designed to be subclassed for each application in which it is used. The subclass should be set as the File’s Owner for a nib file named “Preferences.” The subclass should have NSView instances which are IBOutlets connected to views in the nib file.

The nib file does not need to be connected to a window. If it is, it will be ignored and a new window will be created to display the preference views.

+ (DBPrefsWindowController *)sharedPrefsWindowController

This class method returns a shared instance of the DBPrefsWindowController class.

+ (NSString *)nibName

Override this class if you want to use a nib file named something other than “Preferences.”

- (void)setupToolbar

Override this method with calls to -addView:label: or -addView:label:image: to populate the toolbar.

- (void)addView:(NSView *)view label:(NSString *)label

Call this method as many times as needed from -setupToolbar to add new toolbar icons and custom views to the preferences window. An image with a name that matches the label should be available in the application bundle. It will be used as the toolbar icon.

- (void)addView:(NSView *)view label:(NSString *)label image:(NSImage *)image

This method can be used instead of -addView:label: if the application is localized, or if you just want to use icons with names that differ from the toolbar button labels.

- (IBAction)showWindow:(id)sender

Call this method to display the preferences window. For example:

[[AppPrefsWindowController sharedPrefsWindowController] showWindow:nil];

- (void)setCrossFade:(BOOL)fade

Call this method to enable or disable the cross-fade effect when switching views. The default value is YES.

- (void) setShiftSlowsAnimation:(BOOL)slow

Call this method to enable or disable the use of the shift key to slow down the animation when switching views. The default value is YES.


Footnotes:

1. After this was written, the class was updated. You can find out more about the changes here and here and here.


Comments

Sweet! This is awesome and the cross-fading is extra nice. Thanks!

Posted by David Paul Robinson on March 12, 2007 5:17 PM


In playing with your code, it was bugging me that (at least on my 10.4.8 box) the fading and window resizing are not simultaneous. So, I changed it.

The changes were:
- Refactoring the windowFrame calculation out displayViewForIdentifier:animate: into a helper method.
- Call this from crossFadeView:withView:
- Instead of using NSWindow's animated resizing call, add this to the list of animations:

NSDictionary *windowDictionary = [NSDictionary dictionaryWithObjectsAndKeys:
[self window], NSViewAnimationTargetKey,
[NSValue valueWithRect: [[self window] frame]], NSViewAnimationStartFrameKey,
[NSValue valueWithRect: windowFrame], NSViewAnimationEndFrameKey,
nil];

I'll be happy to send you the diff if you'd like; drop me an email.

Thanks for the hard work on this!

Posted by Cliff L. Biffle on March 12, 2007 8:27 PM


Thanks for the code, Cliff. John Poole also pointed this out to me, so it was on my to-do list to fix. But now I can just copy and paste your code. :-)

At one point the transition was simultaneous, but I guess I did something that changed that, and didn't even notice.

I'll get a revised version up in a few days.

Is there anything else you'd like to see? I'm thinking of changing the code so -setupToolbar is called each time the window is displayed, rather than just once. With this, for example, you could display an additional debugging view or something if the user held down some modifier keys when opening the preferences window.

Posted by Dave Batton on March 13, 2007 2:16 AM


Allowing the app to reconfigure the toolbar on each window launch would be great for me. My app supports plugins, each of which can (in theory) display their own preference pane.

(Yeah, this won't scale well past about half-a-dozen panes -- look at Xcode.)

Since my plugins can be loaded at any time, making the Prefs window somewhat dynamic would rock. (The other approach would have had me posting a notification that a pane had become available. Advantage: live updates. Disadvantage: I have to write the code! :-)

Posted by Cliff L. Biffle on March 13, 2007 10:04 AM


hi

thans for this great classs. i have a problem with it: when switching between sections in the preferences window it resizes the window but the content (buttons etc) is partially hidden. it' like some part of the content is masked. do you know how i culd fix this?

thanks
iwan

Posted by Iwan on April 13, 2007 4:55 AM


@Iwan: Somebody else said that the 300x300 default size I set for the window causes problems. Maybe this is also what you're seeing.

Try changing the default 300 x 300 size to something bigger. I'll also be fixing this in a future update.

Posted by Dave Batton on April 13, 2007 6:10 AM


@dave

hey thanks for the reply! i set the size to something bigger and now it works. hopefully you find out what ight be the bug here.

unfortunately i have another bug: one preference pane is called "iTunes" and the corresponding icon is named "iTunes.png" but it doesn't show the icon. i renamed all to "Itunes" and it worked but i don't wnat my label to be "Itunes". is there a possibility to define the filename of a preference pane icon independendly from the labelname?

thanks for your help again!

btw: when will there be an updated version?

Posted by Iwan on April 17, 2007 12:28 PM


@Iwan: Sorry, I don't know why you're having naming problems. I'll try to reproduce the problem.

But you can easily use a different label. Just use the -addView:label:image: method instead. It lets you specify a different icon and label.

Posted by Dave Batton on April 17, 2007 1:32 PM


Thank you for this great class.

If I use the method addView:label:image to localize the labels - the call is: [self addView:generalPreferencesView label:NSLocalizedString(@"generalPane", @"") image:@"General"] - I get the following error: -[NSCFString setScalesWhenResized:]: selector not recognized [self = 0x23b02c and now Preferences window]. After replacing NSLocalizedString(@"generalPane", @"") with @"General" everything is OK, the Prefferences window opens ...

I stepped through the code - it's after the call [item setImage:image] in addView:label:image of DBPrefsWindowController. Any idea? Am I wrong?

Posted by Dieter on May 26, 2007 2:44 PM


@Dieter:You're passing an NSString as the last parameter to the -addView:label:image: method, but it's expecting an NSImage. Change your call to this and it should work:

[self addView:generalPreferencesView label:NSLocalizedString(@"generalPane", @"") image:[NSImage imageNamed:@"General"]];

Posted by Dave Batton on May 27, 2007 8:38 AM


Hi - thanks for this set of classes - it works great. A small question though:

I would like to add a separator (NSToolbarFlexibleSpaceItemIdentifier?) just before my last toolbar item so that it gets pushed over to the right. It is a 'Revert' preference and I think it looks better over to the side. As I'm pretty new to Cocoa I was wondering what would be the best way to do this?

thanks!

Peter

Posted by Peter on May 4, 2008 5:45 PM


@Peter: The class's -addView:label: method that you call in your subclass basically just adds toolbar item identifiers to a mutable array. So you just need to stick a flexible space into that array in the -setupToolbar method:

[toolbarIdentifiers addObject:NSToolbarFlexibleSpaceItemIdentifier];

Posted by Dave Batton on May 6, 2008 9:21 AM


Thanks Dave - tried your suggestion and it works great, just what I wanted. I'm quite new to this and was thinking maybe I needed to add a 'fake' view for the spacer as well. Glad I don't.

Anyway, thanks again for this very useful set of classes.

Peter

Posted by Peter on May 6, 2008 4:35 PM


Hi Dave,

Thank you very much for providing this useful class. Looks very impressive.

I am currently involved in self-studying Objective-C / Cocoa and in this regard I would like to kindly ask if you have the Simple Preferences example by John Devor still laying around somewhere. It looks like the blog where it was linked was deleted.

Again, thanks for making your code available.

Andre

Posted by Andre on November 8, 2008 8:14 PM


Hi, great code!

One small thing I've noticed. If I try to add a new controller class to the preference.nib, I always get an error:

[NSCFArray insertObject:atIndex:]: attempt to insert nil

The error occurs in [DBPrefsWindowController showWindow:] when it calls [self window]. Can't really figure out what's going wrong...

Posted by Per on May 28, 2009 6:55 AM


Hi Dave, this class is very useful and flexible!

I have one question:

What utility does the DBPrefsWindowController receive from subclassing NSWindowController instead of NSObject?

I ask because it doesn't use the window passed to it and there is nothing in the .nib file that is attached to the DBPrefsWindowController instance.

The one thing I can see from the NSWindowController documentation is automatic saving of the window coordinates in the defaults, but everything else seems related to NSDocument.

I just didn't know if this was normal. It seems strange to me that the class would throw away the passed-in window.

Thanks!

_ michael

Posted by Michael Bishop on November 28, 2009 3:14 PM


Add a Comment
Please fill in the fields below to add a comment:
Name*:
Email Address*:
URL:
Spam Protection*:     Type "denver" without the quotes.
Message*:
  Your email will not be published and the URL provided will be used as
a link on your name.

* indicates a required field.