Omnipreferential: Rolling My Own Prefs Window, Instead of Using Omni Frameworks

After a long hiatus, I am once again looking at the project that first got me using both the Omni frameworks and unit tests.

Why now? An application already exists that does what my project’s app does, but that existing app is Classic-only. Classic seemed to be moseying along just fine until splat! it hit the brick wall of Macintel. No more Classic.

I was using the Omni frameworks primarily for how they made your application’s preferences window easier to implement. The trouble is, the Omni framework projects are sorely outdated. The version you’re supposed to use don’t have native targets, which is a requirement for building Universal binaries.

So it’s time to give them the heave-ho. Does that mean I have to make my own preferences implementation from scratch?

One alternative is to use the Preference Panes framework from Apple. It’s primarily for System Preferences panes, but the documentation makes it clear that it can be used for application preferences, too. The trouble? If you use it for your own application, you have to manage the window and the toolbar yourself. Since that’s most of the work, and since, if you use the Preferences Pane framework, you need to package each pane as a separate bundle, which is even more work, I give it a pass.

Uli Kusterer has UKPrefsPanel and UKToolbarFactory available in the source code portion of his Web site. They look promising. The prefs panel even uses an NSTabView as an easy way to deal with the prefs panes, which is how I was thinking of doing it. But it doesn’t support automatic resizing of a preferences window to the height of each individual preference pane, which is something I definitely want.

In fact, I want my preferences window to look and behave exactly like the preferences windows of standard Apple applications like Safari and Address Book.

So I decide to work directly with NSToolbar, and see what sort of progress I can make.

Apple’s ADC Web site has ToolbarSample source code that’s a good starting point.

Following that sample, I make the NSToolbar myself in code, and add it to the window by sending the window the message setToolbar:. That API actually expands the window by the height of the toolbar: I don’t have to leave room for it myself in the window’s nib. The window also fully takes care of the user hiding and reshowing the toolbar, and saving the selected toolbar item, as long as I send it a setAutosavesConfiguration:YES message. Neat.

There are a lot of things that need some tweaking, though:

• I want to use an NSWindowController to encapsulate the preferences window nib, a straightforward Cocoa technique. But the controller doesn’t seem to use NSWindow’s frameAutosaveName value, which is settable in the nib. In fact, it seems to set it to nil. Instead, if I want to go that route (which I don’t, see below) I need to set the controller’s windowFrameAutosaveName value, which can only be done in code. Annoying.

• The preferences windows of Safari et al erase themselves and then use a cool drawer-like animation to resize between preference panes. I can get the latter using setFrame:display:YES animate:YES, but not the former. I handle this by adding an extra, empty NSTabViewItem after all the other meaningful ones. I switch to that, do the resize, then switch to the NSTabViewItem I really want afterwards.

• I want the individual preference pane’s size info to be in the nib, not something I have recompile to alter. But every NSTabViewItem within an NSTabView is the same size. I work around this by putting my own custom NSView inside each NSTabViewItem, and then putting all my widgets in that subview.

In my code, I check for that subview’s size, and resize the window to match it, when switching to that NSTabViewItem.

• I need to be careful with resizing: it order to get the the effect I want — which is that the window title bar stays stationary, and the bottom of the window slides up or down — in the normal Quartz coordinate system, I need to change both the window size and its origin, because the origin is in the lower left-hand corner of the window. So, every time I select a toolbar item, as far as Cocoa is concerned, I’m both resizing and moving the window. But NSWindowController doesn’t catch this, leading to erroneous positioning when I close and reopen the application.

• If I handle user defaults manually instead of relying on NSWindowController, I can save the window’s frame to the user defaults (a) when I choose a new toolbar item (see the sample for how to associate toolbar items with actions), (b) when the window is moved around by the user (via the notification NSWindowDidMoveNotification), and (c) when the window is resized (via the notification NSWindowDidResizeNotification). Now, the window is not user-resizable, but this notification is still invoked when the user shows or hides the toolbar, because that automatically changes the size of the window.

But why do that much work? Instead, I listen for the notification NSApplicationWillTerminateNotification, and save off the window frame then. Since user defaults are generally only saved to disk upon application termination, this doesn’t lead to a mismatch even if you force-quit the application, because neither the toolbar user default changes that have already happened, nor my code’s user default changes that haven’t happened yet, are saved.

2 comments

  1. Marc Charbonneau

    An NSTabView seems like it would be a good choice for this, but I’ve never had a need for it in my applications. Instead, I just put everything inside separate NSViews directly in the NIB, and create an IBOutlet variable to access the views in code. Then, in my controller class, I can simply switch between them using NSWindow’s setContentView. I assign each NSToolbarItem a tag number, which makes selecting the correct NSView easy. Pretty much the only thing that required much thought is this method:

    – (void)setWindowContentView:(NSView *)aView
    {
    NSWindow *window = [self window];
    NSRect newWindowRect = [window frameRectForContentRect:[aView frame]];
    NSRect oldWindowRect = [window frame];
    float heightDifference = NSHeight( newWindowRect ) – NSHeight( oldWindowRect );
    newWindowRect.origin = NSMakePoint( NSMinX( oldWindowRect ), NSMinY( oldWindowRect ) – heightDifference );

    [window setContentView:[[[NSView alloc] initWithFrame:newWindowRect] autorelease]];
    [window setFrame:newWindowRect display:YES animate:YES];
    [window setContentView:aView];
    }

    I’ve never tried any third party preference window frameworks; but I’ve never really had a need for them, either!