« Creating a PreferencePane

Posted by Andy Monitzer on November 12, 2001 [Feedback (2) & TrackBack (0)]

Writing a preference pane is pretty easy. The only thing you have to remember is that you are not writing an application, preference panes are bundles!

That means:

  • Don't mess with the window, it doesn't belong to your bundle.
  • Don't mess with the application delegate.
  • Since all preference panes are loaded into a single application (Usually /Applications/System Preferences.app), you have to use a prefix for all class names to avoid name conflicts (e.g. I'm using "AM" like in "AMPreferences"). If you're creating two panes, you have to take special care. Note that I didn't do it in this tutorial, but nobody else would call a class "CocoaDevCentralPanePref", right?
  • Localization doesn't work like in applications, since NSLocalizedString() only looks at the main bundle's strings-file. Use NSLocalizedStringFromTableInBundle(@"key",nil,[NSBundle bundleForClass:[self class]],"comment") instead.
  • For the same reason, NSUserDefaults doesn't work as usual. We'll get to that later.

 

// The Environment

When creating a preference pane using the Project Builder template (from the Standard Apple Plug-ins category), most things are already done for you.

Take a look at the nib-file: It contains a single window. That's your starting point. You can create buttons, text views, etc here. After loading the preference pane, the content view of that window is taken out and inserted into the shared preference panes window. The window itself is released afterwards, so it doesn't make sense to alter the title or other properties (except the content size of course).

Note: It's possible to use a view directly to avoid that useless window, but Apple seems to have decided that it's not intuitive enough. It requires some custom code, and isn't really worth it.

The main class in your pane is a child of NSPreferencePane. It has to be the bundle's NSPrincipalClass (you can change that in the target's expert bundle settings).

Take a look at Apple's documentation for methods you can override. Most of them are informing your pane about user actions (for instance selecting, unselecting). The setup (filling the text fields etc) should be done in -mainViewDidLoad.

 

// What About Some Code?

Ok, since most people like to get some examples, let's do a CocoaDevCentralPane where you can configure important things.

Open Project Builder and choose File->New Project, then select Standard Apple Plug-ins->PreferencePane.

New Project

Now open the new nib and create the UI:

Our window

The file's owner is the class Project Builder created for you. There are already four outlets (one connected) inherited from NSPreferencePane. You don't have to change them, but nobody blocks you from doing so. We'll just add some new ones: "website", "author" and "rating" (Double-click the nib's owner and press Cmd-1 to get the correct inspector). Connect them to the appropriate controls.

Of course, it's possible to create and instantiate custom classes just like in applications, but that's not necessary for our simple app.

Now open the header file "CocoaDevCentralPanePref.h" in PB and add our outlets and the second method (we'll get to that later):

@interface CocoaDevCentralPanePref : NSPreferencePane 
{
    IBOutlet NSTextField *website;
    IBOutlet NSMatrix *author;
    IBOutlet NSSlider *rating;
}

- (void) mainViewDidLoad;
- (void) saveChanges:(NSNotification*)aNotification;

@end

In "CocoaDevCentralPanePref.m", only two methods are required for our task, one for initializing, and one for saving the preferences to the defaults database.

For our initialization, we'll use -mainViewDidLoad:

- (void) mainViewDidLoad
{
    NSDictionary *prefs=[[NSUserDefaults standardUserDefaults]
      persistentDomainForName:[[NSBundle bundleForClass:[self class]]
      bundleIdentifier]];

Since NSUserDefaults usually loads its data from the application's defaults database, we can't use that functionality. Luckily, there are mechanisms for setting the domain's name and getting the correct dictionary (nil if it doesn't exist).

    [[NSNotificationCenter defaultCenter] addObserver:self
            selector:@selector(saveChanges:)
            name:NSApplicationWillTerminateNotification
            object:nil];

Since we can't change the NSApplication's delegate, we have to listen to this notification for storing our defaults.

    if(prefs) {
        [website setStringValue:[prefs objectForKey:@"website"]];
        [author setState:1 atRow:[[prefs objectForKey:@"author"] intValue]
                column:0];
        [rating setFloatValue:[[prefs objectForKey:@"rating"] floatValue]];
    }
}

That should do the trick. Saving the preferences is very similar:

- (void) saveChanges:(NSNotification*)aNotification {
    NSDictionary *prefs;
    int selauthor;
    
    // get selected radio button from matrix
    for(selauthor=[author numberOfRows];selauthor;selauthor--)
        if([[author cellAtRow:selauthor-1 column:0] intValue])
            break;
    selauthor--;
    
    prefs=[[NSDictionary alloc] initWithObjectsAndKeys:
        [website stringValue], 			@"website",
        [NSNumber numberWithInt:selauthor],	@"author",
        [NSNumber numberWithFloat:[rating floatValue]],	@"rating",
        nil];
    
    [[NSUserDefaults standardUserDefaults]
        removePersistentDomainForName:[[NSBundle bundleForClass:
        [self class]] bundleIdentifier]];
    [[NSUserDefaults standardUserDefaults] setPersistentDomain:prefs
        forName:[[NSBundle bundleForClass:[self class]] bundleIdentifier]];
    
    [prefs release];
}

That's it! The next step would be to create a nice icon for it (the default one is already included in the project), but I'll leave that up to you.

 

// Installation

Now compile our project. Note that when trying to execute it, you'll get the message "No executable associated with Target". That's ok, since your project is a bundle, not an application.

There are four locations where System Preferences.app will look for its panes (you have to create the folders if they don't exist):

  1. /System/Library/PreferencePanes: This one is reserved, you must not use it
  2. /Library/PreferencePanes: Install system-global panes here (if you got the permission)
  3. ~/Library/PreferencePanes: For user-specific panes
  4. /Network/Library/PreferencePanes: For Network-wide panes (this one has to be NFS-mounted from the server).
  5. Copy the build product in one of those places, and we're done!


Comments

Please pardon my ignorance on what would seem to be an extremely basic modification to the above script (I am new to cocoa, etc) BUT...

Could someone provide an example of how a checkbox could be implemented in the above PreferencePane?

I did what I thought should work, but it doesn't seem to behave as I had expected it to.

I believe I got the addition of the checkbox in IB and the connection correctly, my problem (I think) lies more specifically in what should be placed in the 'if(prefs)' clause and the 'prefs=' clause.

Thanks!

Posted by: jamie on April 17, 2003 08:27 PM

In reply to the above comment. I got it working. Going to post what I did here for others that are new to Cocoa and may find the following useful. I must point out that this "works" but I don't think it is the "best way to do it". First off, let me post the changes then I will discuess them below.

- Add a switch to the .nib and an outlet for it called testSwitch and make the proper connection for it.

- Open CocoaDevCentralPanePref.h and add the following to @interface CocoaDevCentralPanePref : NSPreferencePane

IBOutlet NSButton *testSwitch;

- Open CocoaDevCentralPanePref.m and add the following to the "if(prefs)" clause:

[testSwitch setState:[[prefs objectForKey:@"testSwitch"] intValue]];

- and the following to the "prefs=" clause (before "nil];"):

[NSNumber numberWithInt:[testSwitch state]], @"testSwitch",

This compiles w/out error and when run adds the state to the .plist file and doesn't crash System Preferences. The reason I think this is not the "best way to do it" is becuase I read that while setState will take int values it would really like to get NSOffState or NSOnState. I also do not fully understand why the lines in the "if(prefs)" and "prefs=" clauses will not work with:

[testSwitch setState:[prefs objectForKey:@"testSwitch"]];

[testSwitch state], @"testSwitch",

It will give the following warnings:

warning: passing arg 1 of 'setState:' makes integer from pinter without a cast
warning: passing arg 1 of 'initWithObjectsAndKeys' makes pointer from integer without a cast

Posted by: jamie on April 18, 2003 02:19 PM
Post a comment