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


Adding a New Style Preferences Window to Your App, Part 2

by Martin Redington
01/27/2006

In part one of this series, I discussed how to create the necessary scaffolding for a new style preferences window. In this second part, I'll cover the creation of the actual preference panes.

Creating Preference Panes

To keep things tidy, I like to keep my preference panes code in a separate directory. In the Finder or the Terminal, create a new PreferencePanes directory, underneath your NewPreferencesExample project directory.

In Xcode, create a new PreferencePanes group under the NewPreferencesExample project.

Your First Preference Pane

Directory and Group

Now create another directory, GeneralPrefs, under the PreferencePanes directory, and a GeneralPrefs group in Xcode, under the PreferencePanes group.

A New Target

Still in Xcode, create a new target (from the Project Menu, or from the contextual menu for the project in the Groups and Files pane). Set the target's type to Cocoa "Loadable Bundle," and its name to GeneralPrefs.

Open the inspector for the main NewPreferencesExample target. In the General tab of the inspector, drag the new GeneralPrefs target from Xcode's Groups and Files pane to the "Direct Dependencies" list.

We also need to link our new target against the Cocoa framework. Expanding both targets in the Groups and Files pane, drag Cocoa.framework from the "Link Binary with Libraries" build phase of the NPEPreferencesController target to the corresponding build phase of the GeneralPrefs target.

Open the inspector for NPEPreferencePaneController.m, and add it to the GeneralPrefs target, as well as to NPEPreferencesExample.

A Preference Pane Icon

We will "borrow" GeneralPreferences.tiff from Safari.app--in the Terminal, cd to the top-level directory for your project, and run the following command:

cp /Applications/Safari.app/Contents/Resources/GeneralPreferences.tiff \
PreferencePanes/GeneralPrefs

Then, in Xcode, use the "Add to Project" item in the Project menu to add the GeneralPreferences.tiff file to your project, as an "Existing File." In the dialog that appears when adding the file, uncheck the "Copy items into destination group's folder" item, and make sure that GeneralPreferences.tiff is only being added to the GeneralPrefs target.

GeneralPreferencePaneController

Add a new Objective-C class, called GeneralPreferencePaneController, to the GeneralPrefs group, with target GeneralPrefs only. The source code for the GeneralPreferencePaneController.h class is shown below:

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

@interface GeneralPreferencePaneController : NPEPreferencePaneController {
}

@end

GeneralPreferencePaneController.m is even simpler:

#import "GeneralPreferencePaneController.h"

@implementation GeneralPreferencePaneController

@end

GeneralPrefs.nib

In Interface Builder, create a new empty Cocoa nib, and save it in the PreferencePanes/GeneralPrefs folder as GeneralPrefs.nib--when you are presented with a dialog asking if you want to add it to the NewPreferencesExample project, and which targets to add it to, add it to the GeneralPrefs target only.

Still in Interface Builder, select the Classes tab, and parse the NPEPreferencePaneController.h and GeneralPreferencePaneController.h header files (you need to parse the superclass header before you can parse the subclass). Then set the custom class of "File's Owner" to be the GeneralPreferencePaneController class.

Then drag a CustomView from the Interface Builder Cocoa Containers palette into your nib file. Hook this custom view up to the _prefsView outlet of File's Owner.

GeneralPrefs-Info.plist

Back in Xcode, In the Info panel for the new GeneralPrefs target, in the Properties tab, set the Principal Class to be GeneralPreferencePaneController, and the Main Nib File to be "GeneralPrefs."

In the Build tab of the Info panel for the new target, set "Wrapper Extension" to be "preferencePane." Also in the Build tab, set the Info.plist entry to read PreferencePanes/GeneralPrefs/GeneralPrefs-Info.plist instead of GeneralPrefs-Info.plist.

In the Finder or the Terminal, move the GeneralPrefs-Info.plist file to the PreferencePanes/GeneralPrefs subdirectory. Then find the GeneralPrefs-Info.plist entry in the Groups and Files pane of Xcode, drag it into the GeneralPrefs group, and using "Get Info" (Command-I) in Xcode, adjust the path for GeneralPrefs-Info.plist to match its new location.

We now need to add a few preferencePane-specific properties to the GeneralInfo.plist file, specifying at minimum the preference pane name, icon path, and tooltip.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" 
   "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <!-- existing entries -->
    ...

    <!-- new mandatory entries -->
    <key>paneName</key>
    <string>General</string>    
    <key>iconPath</key>
    <string>GeneralPreferences.tiff</string>        
    <key>toolTip</key>
    <string>General Preferences</string> 

    <!-- new optional entries -->
    <key>helpAnchor</key>
    <string>GeneralPreferencesHelp</string> 
    <key>allowsHorizontalResizing</key>
    <string>NO</string> 
    <key>allowsVerticalResizing</key>
    <string>NO</string> 

  </dict>
</plist>

Finishing Up

Now build and run the NPEPreferencesExample target in Xcode. As before, select the "Preferences..." item from the NewApplication menu. Once again, "Preferences are not available."

Although we have successfully built both the NPEPreferencesExample and GeneralPrefs products, NPEPreferencesExample cannot find the GeneralPrefs bundle.

Select the NPEPreferencesExample target in Xcode, and add a new Copy Files build phase. The destination will be Resources (which should be the default), but there is no need to enter anything in the path field. Instead, drag GeneralPrefs.preferencePane from the Products group into the new Copy File build phase.

Now, build and run the NPEPreferencesExample target again, and once the main window has launched, hit Command-Minus, to bring up the Preferences Window. Tada! You now have a new style preferences window.

figure

Figure 1. The first preference pane

Running Mac OS X Tiger

Related Reading

Running Mac OS X Tiger
A No-Compromise Power User's Guide to the Mac
By Jason Deraleau, James Duncan Davidson

Your Second Preference Pane

Repeat the above steps exactly, but substitute "Advanced" for "General" throughout. That is, your PreferencePanes subdirectory, Group, and the new target will be called "AdvancedPrefs," with AdvancedPreferences.tiff (again from Safari) as the icon, with AdvancedPrefs-Info.plist, an AdvancedPreferencePaneController class, and AdvancedPrefs.nib file.

Take care not to miss any steps, but hopefully it will be a bit quicker the second time around.

Build and run the project, and your preferences window should look like the one shown in Figure 2.

figure

Figure 2. Two preference panes

If you switch between preference panes, and then quit the application, note that the last selected preference pane is shown after the restart. SS_PrefsController handles this for you.

Adding Controls to Your Preferences Window

We now have a groovy new-style preferences window, but to make it useful we need to add some controls, so that we can read and set the values of our actual preferences.

General Preferences

We will represent our preferenceOne (a Boolean) value using a pair of radio buttons on the General preferences pane.

First, we will add some convenience methods to GeneralPreferencePaneController:

- (int) preferenceOneSelectedIndex{
    return [_controller preferenceOne] ? 0 : 1;
}

- (void) setPreferenceOneSelectedIndex:(int) selectedIndex{
    [_controller setPreferenceOne: (selectedIndex == 0)];
}

Remember to add the method prototypes to GeneralPreferencePaneController.h as well. You can also import NPEController.h to GeneralPreferencePaneController, to prevent some compiler warnings about the preferenceOne methods.

Then, in Interface Builder, open GeneralPrefs.nib, and drag a pair of radio buttons and a label from the palette into the custom view, and arrange them as shown Figure 3 below.

Open the Inspector for the NSMatrix object that contains the radio buttons, and bind selectedIndex to File's Owner, with Model Key Path set to preferenceOneSelectedIndex.

Save the nib file, and build and run your project again in Xcode.

The General Preference pane should now contain the controls that you added. If you change the value, and then restart your application, you will see that, as one would hope, the preferenceOne value is now set to the new value.

figure

Figure 3. The general preference pane, with controls

Advanced Preferences

We will represent our preferenceTwo value (an int) in a drop-down menu.

To support this, add the following methods to AdvancedPreferencePaneController.m (and add the prototypes and an import of NPEController.h to AdvancedPreferencePaneController.h).

- (NSArray *) preferenceTwoLabelArray{
    return [NSArray arrayWithObjects: @"Zero", @"One", @"Two", @"Three", nil];
}

- (int) preferenceTwo{ return [_controller preferenceTwo]; }

- (void) setPreferenceTwo:(int) preferenceTwo{
    [_controller setPreferenceTwo:preferenceTwo];
    if(preferenceTwo > 2){
        NSDictionary *errorDict = 
            [NSDictionary dictionaryWithObjectsAndKeys:
                @"preferenceTwo value alert", NSLocalizedDescriptionKey,
                @"preferenceTwo value is set to a high value.", NSLocalizedRecoverySuggestionErrorKey,
                nil];

        NSError *error = [NSError errorWithDomain:@"privateDomain" code:0 userInfo:errorDict];
        [self showWarningAlert:error];
    }
}

Then, in Interface Builder, open AdvancedPrefs.nib, and drag an NSPopupButton and a corresponding label from the palette, and arrange them as shown in Figure 4 below. You do not need to touch the menu items in the NSPopupButton--just leave the default ones in place.

Also, drag an Array Controller from the palette to the Instances pane of the AdvancedPrefs window in IB. Bind the content array of the array controller to File's Owner, with Model Key Path set to "preferenceTwoLabelArray."

figure

Figure 4. The advanced preference pane, with controls

Then add the following bindings to the NSPopupButton that you just added: bind both content and content values to the NSArrayController, with Controller Key arrangedObjects. Bind selectedIndex to File's Owner, with model key path preferenceTwo.

Save the nib file, and build and run your project again in Xcode.

The Advanced Preference pane should now contain the controls that you added. Again, changes to the value should persist between restarts of the application, just like a proper preference pane should.

If you set the value of preferenceTwo to be Three, you should receive the warning alert that was defined in the setPreferenceTwo method of AdvancedPreferencePaneController.

Congratulations!

You now have a fully functional new style preferences window.

There are a few more tips and techniques that you might find useful.

Accessing Preferences "Directly"

In many cases, you will not need to have your PreferencePaneController subclass mediate changes to the preferences values--in these cases, you can add an NSObjectController instance from the palette to the Instances pane of your preference pane's nib file, binding its contentObject to the File's Owner, with Model Key Path "controller."

Your preferences controls can then be bound directly to methods in the underlying controller object (in this example, NPEController).

Adding Help to Your Preferences Panes

In the preference pane's Info.plist file, there is an optional helpAnchor key (you may also remember from part one that we defined some help related methods in NPEPreferencePaneController).

If set, the value for this key should be a help anchor--a reference to a defined page or location in an Apple Help Book.

If you add a help button to your preference pane's custom view, and connect the button action to the showHelp: method of the File's Owner (i.e. the NPEPreferencePaneController subclass), then the help button will open the Application's Apple Help, and jump to the named anchor.

Setting the help anchor will also cause any alerts displayed by showWarningAlert to display a help button, which will direct users to the same help anchor.

Optimizing Code Size

In our example application, NPEPreferencePaneController.m is included in all three targets (the application, and the two preferences panes). This means that object code for this class will be included three times, increasing the size of the final application.

In our example this, is not such a big deal (the binary code for NPEPreferencePaneController is only 25k in size), but in a real-world application, there may be other common code, which can swell the final application size considerably.

You can eliminate this redundancy by creating a new Target, of the type Framework, in your project. All of the common code that is required by two or more preference panes can be added to this new target, and removed from all of the other targets. In the build properties for your new target, you will need to set the Installation Directory setting to @executable_path/../Frameworks.

You should add the new target to the dependencies of your main target, and each preference pane target. You also need to add the target's product (the framework) to the "Link Binaries with Libraries" build phase of the main application target, and to a new Copy Files build phase in the main application target, with a destination of Frameworks.

Final Thoughts

In part one of this article, I showed you how to set up the scaffolding for your new style preferences window. In this part, we covered the actual creation of the preference panes themselves.

Although the initial investment required to support new preference windows is relatively high, Matt Gemmell's SS_PrefsController has already done most of the hard work.

The techniques shown here add a few useful interface tweaks, to make your windows more like Apple's, and reduce the amount of code that you need to write to make use of SS_PrefsController.

Martin Redington is a long-time Mac user who recently started writing Mac shareware. His first product, MySync, provides Mac-to-Mac syncing without .Mac and is currently in public beta.


Return to the Mac DevCenter

Copyright © 2009 O'Reilly Media, Inc.