« Create a Preference Pane

Posted by Jason Marr on November 09, 2001 [Feedback (1) & TrackBack (0)]

// What is a Preference Pane

With the introduction of Mac OS X 10.1 Apple provided a new class, NSPreferencePane, that allows developers to create those spiffy panes that are in the System Preferences app. There are two places preference panes can be used. First an application can use preference panes in its own preferences window. OmniWeb is one application that does this. Preference panes can also be added to the System Preferences application as plug-ins. However, System Preferences plug-ins should only be used if you write a program that lacks its own user interface, such as a device driver or an application that runs invisibly in the background.

 

// Scroll Arrows

In the General pane under System Preferences, Apple provides an option to have the scroll arrows in one of two positions, both arrows at the bottom/right or one arrow at the top/left and one arrow at the bottom/right. But what if you want double scroll arrows at both ends of the scrollbar or both arrows at the top/left? That's where Cocoa and preference panes come in!

Before we begin creating our project, there are a couple of things we should know. The setting for the scroll arrow position is stored in an invisible file named .GlobalPreferences.plist which is found in your ~/Library/Preferences directory. Just like any other plist file there are various key/value pairs stored in this file. We are interested in the "AppleScrollBarVariant" key. There are 4 possible values for this key: Single, DoubleMin, DoubleMax, and DoubleBoth. Single and DoubleBoth are pretty self explanatory, DoubleMin means both arrows will be at the top/left of the scrollbar, and DoubleMax means both arrows will be at the bottom/right of the scrollbar.

With that in mind we could of course go to the command line and use the defaults command to change the scroll arrow location. But we're Cocoa programmers! So we could just wrap the the defaults command in a preference pane. However, there's another way that will be more efficient and provide an option for greater expandability. That way is to use dictionaries. It turns out a plist file can easily be read into an NSDictionary and an NSDictionary can also be written out to a plist file. Now let's begin!

 

// Setup

After opening PB you have two options when creating a new project. You can either create a new Cocoa Bundle Project or you can use the "PreferencePane" template found under "Standard Apple Plug-ins". This tutorial was written from the standpoint of creating a new Cocoa Bundle, but either one will work fine. If you choose to use the "PreferencePane" template you can skip ahead to the "Build the Interface" portion of this article.

Create a new Cocoa Bundle Project in PB called ScrollArrowsPane. Next, from the Project menu choose add frameworks and select /System/Library/Frameworks/PreferencePanes.framework. Now open Interface Builder and create an empty Cocoa nib. Save it in the English.lproj directory of your project as ScrollArrows.nib. Then IB will bring up a sheet asking if you want to add the nib file to your project. Theoretically the ScrollArrowsPane target should be shown in this sheet but I've found that a majority of the time it will not list any targets (See figure). So if ScrollArrowsPane is listed click "Add" and you're all set. If not click cancel and go back to PB. In PB go to the Project menu and choose "Add Files" and add the nib file.

No Targets
If the sheet IB shows you looks like this, you'll have to add the nib file manually.

Now in PB find the NSPreferencePane.h header file in PreferencePanes.framework. You can do this by clicking the disclosure triangle next to PreferencePanes.framework. The NSPreferencePane.h file is in the Headers folder. Drag the header file to the ScrollArrowsPane.nib window in IB. NSPreferencePane should now show up in the Classes section of the nib window. This will let you create a subclass of NSPreferencePane which we will do later.

 

// Build the Interface

Create a new window that is 595 pixels wide. The System Preferences app stays at a constant width of 595 pixels and only changes its height when loading preference panes. Next build the rest of the user interface:

Interface
This is how I designed my interface.

Note: When you initially drag the radio button matrix onto the window you will only have 2 buttons. In order to get 4 buttons, option drag the bottom middle dot of the selection marquee downward and more buttons will appear. Also after creating the text field with red text you will want to delete the text in the field so that it does not show up when the pane first loads. I left text in the field for the screenshot so you could see where it goes in the interface.

 

// Outlets and Connections

Next go to the Classes pane in the IB nib window and find the NSPreferencePane class and create a subclass of it called ScrollArrowsPane. Now create two outlets for ScrollArrowsPane one called scrollArrowsMatrix and another called redWarningText. Next create an action called scrollSwitchClicked:.

Now we need to change the class of "File's Owner" to the ScrollArrowsPane class. This step is critical because it allows us to send messages to "File's Owner" and have our class handle those messages. First select "File's Owner" from the Instances pane in the IB nib window. Then select ScrollArrowsPane from the list of classes in the Attributes section of the Info window.

Now we can make connections to "File's Owner." First, control-drag a selection from "File's Owner" to the "Window" object and select the _window outlet. Now make a connection from "File's Owner" to the radio button matrix and select scrollArrowsMatrix as the outlet. Then make a connection from "File's Owner" to the text field object and select redWarningText as the outlet. Finally make a connection from the radio button matrix to "File's Owner" and select scrollSwitchClicked: as the action.

The last thing to do in IB is to create the files for the scrollArrowsPane class. In the Classes pane of the nib window select the scrollArrowsPane class and choose Create Files from the Classes menu. Make sure that "Insert into targets" is selected in the save window. As mentioned above, if the ScrollArrowsPane target is not listed you can manually add the files to your project using the "Add Files" item under the "Project" menu in PB.

 

// Project Builder Settings

You can skip this section if you used the "PreferencePane" template.

The rest of the work on this project will be done in PB. So after going back to PB go to the Project menu and select Edit Active Target. In the Bundle Settings pane change the "Identifier" field to "Scroll Arrows". Under the "Cocoa-Specific" heading change "Principal class" to "scrollArrowsPane" and "Main nib file" to "ScrollArrows" (be sure to leave off the .nib extension).

If you want to use a custom icon, enter expert mode by clicking the "Expert" button at the top right of the window. Click the New Sibling buton and rename the key to "NSPrefPaneIconFile" and set its value to the name of your icon file.

Now go to the Build Settings pane and change the "WRAPPER_EXTENSION" entry value to "prefPane", you will have to scroll to the bottom of the window to see it. Now we need to add one line of code to the ScrollArrowsPane.h file before we can test the pane. Under #import <Cocoa/Cocoa.h> add the line #import <PreferencePanes/NSPreferencePane.h>. This line imports the NSPreferencePane class into our project.

If you want to see how your project looks you can build the project and then move the ScrollArrows file from the build directory of your project folder to ~/Library/PreferencePanes. You may have to manually create the PreferencePanes directory. Now open System Prefereneces and there should be a new category called Other with the Scroll Arrows pane in it.

 

// Create the Header File

Ok now its finally time to write some code! Update your ScrollArrowPane.h file to look like this:

#import <Cocoa/Cocoa.h>
#import <PreferencePanes/NSPreferencePane.h>


@interface ScrollArrowsPane : NSPreferencePane
{
    NSMutableDictionary *newGlobalPreferences;
    NSDictionary *oldGlobalPreferences;
	
    IBOutlet NSTextField *redWarningText;
    
    IBOutlet NSMatrix *scrollArrowsMatrix;
    NSString *currentScrollSetting;
}

- (void)willSelect;

- (IBAction)scrollSwitchClicked:(id)sender;

- (void)didUnselect;

@end

The first two lines import the necessary header files. The two dictionaries will be used for keeping track of the user's preferences. The newGlobalPreferences dictionary will be used for writing the preferences back to the disk and the oldGlobalPreferences dictionary will keep track of the initial values in the .GlobalPreferences.plist file. There are two IBOutlets, scrollArrowsMatrix is an outlet to the radio buttons and redWarningText is an outlet to the text field. Finally there is an NSString that will keep track of the current setting of the scrollbar position based on which radio button the user has selected.

There are only three methods needed for this preference pane. The willSelect method is sent to the preference pane just after the user clicks on its icon in the System Preferences app to let the pane know it is going to be displayed. The scrollSwitchClicked method is sent anytime the user clicks one of the radio buttons. Finaly the didUnselect method is called when the preference pane is going to be unselected either beacuse the user clicked on another preference pane or quit the System Preferences application.

 

// Create the willSelect Method

First we'll implement the willSelect method. Basically all we need to do in this method is initialize the two dictionaries and the radio button matrix. The implementation looks like:

- (void)willSelect
{
    oldGlobalPreferences = [[NSDictionary dictionaryWithContentsOfFile: 
        [@"~/Library/Preferences/.GlobalPreferences.plist" 
        stringByExpandingTildeInPath]] retain];
    
    newGlobalPreferences = [[NSMutableDictionary alloc] init];
    [newGlobalPreferences addEntriesFromDictionary:oldGlobalPreferences];
    
    currentScrollSetting = [oldGlobalPreferences objectForKey:
         @"AppleScrollBarVariant"];
    
    if([currentScrollSetting isEqualToString:@"Single"])
         [scrollArrowsMatrix selectCellAtRow:0 column:0];
    else if([currentScrollSetting isEqualToString:@"DoubleMin"])
             [scrollArrowsMatrix selectCellAtRow:1 column:0];
    else if([currentScrollSetting isEqualToString:@"DoubleMax"])
             [scrollArrowsMatrix selectCellAtRow:2 column:0];
    else if([currentScrollSetting isEqualToString:@"DoubleBoth"])
             [scrollArrowsMatrix selectCellAtRow:3 column:0];
    else
         [scrollArrowsMatrix selectCellAtRow:2 column:0];
}

The first line of code loads the contents of the .GlobalPreferences.plist file into the oldGlobalPreferences dictionary. A retain message is sent because the dictionaryWithContentsOfFile method returns an autoreleased dictionary and we'll need to use the oldGlobalPreferences dictionary in the other methods. The next two lines initialize the newGlobalPreferencesDictionary to the contents fo the oldGlobalPreferences dictionary. Next the currentScrollSetting is extracted from the oldGlobalPreferences dictionary using the objectForKey method. Finally the value of the currentScrollSetting is used to set which radio button should initially be selected when the preference pane loads.

 

// Create the scrollSwitchClicked Method

The next method that we'll implement is the scrollSwitchClicked method. First we'll want to determine which button the user selected, update the currentScrollSetting variable to reflect the change and then update the newGlobalPreferences dictionary. This looks like:

int row = [scrollArrowsMatrix selectedRow];
switch(row)
{
    case 0:
         currentScrollSetting = @"Single";
         break;
    case 1:
         currentScrollSetting = @"DoubleMin";
         break;
    case 2:
         currentScrollSetting = @"DoubleMax";
         break;
    case 3:
         currentScrollSetting = @"DoubleBoth";
         break;
    default:
         NSLog(@"Error in Selected Row");
         break;
}
    [newGlobalPreferences setObject:currentScrollSetting forKey:
         @"AppleScrollBarVariant"];

The other thing we need to do is see if we need to display text informing the user that a logout is required for the settings to take a effect. Basically we just need to compare the values for AppleScrollBarVariant in the newGlobalPreferencesDictionary and the oldGlobalPreferencesDictionary.

if([[newGlobalPreferences objectForKey:@"AppleScrollBarVariant"]
    isEqualTo: [oldGlobalPreferences objectForKey:
    @"AppleScrollBarVariant"]])
{
    [redWarningText setStringValue: @" "];
}
else
{
    [redWarningText setStringValue:
        @"Changes take effect after next login."];
}

 

// Create the didUnselect Method

The last thing we need to do is implement the didUnselect method. The only thing we need to do in this method is write the value in newGlobalPreferences to the .GlobalPreferences.plist file and release the dictionaries so we don't have a memory leak.

- (void)didUnselect
{
    [newGlobalPreferences writeToFile:[
        @"~/Library/Preferences/.GlobalPreferences.plist"
        stringByExpandingTildeInPath] atomically:YES];
    
    [newGlobalPreferences release];
    [oldGlobalPreferences release];
}

In case you were wondering, the "atomically" argument in the first line of the didUnselect method determines if the file is written directly or if it is first written to an auxillary file and then that file is moved to the correct path. Passing yes to the atomically argument writes an auxillary file first. The reason this is done is that if the computer crashes while writing the file the entire path to the file will not be corrupted.

Well, now you can build your project and test out your new preference pane.

I'd like to thank Smith Kennedy for having me stress the importance of changing the class of "File's Owner" to ScrollArrowsPane.


Comments

I was having weird issue with my control panel and I finally realized what it was thanks to your article. I hadn't changed my File Owner to my subclass of the PreferencePane, hence I had two instances of the PreferencePane which had caused numerous little issues that I hadn't understood before.

Anyway, many thanks.

-shane

Posted by: Shane Celis on January 28, 2003 07:23 PM
Post a comment