duminică, 28 februarie 2010

OS X Preferences Tutorial

This tutorial explains a different way to add preferences to you application. To do this, we'll be using PreferencePanes.framework that Apple provides since OS X 10.1.

This same framework is used to create prefPane bundles that System Preferences application uses as plugins.

The ideea is simple, the main application (for example PrefsApp in our case) will contain two bundles after building it. When launching the application and selecting the Preferences... menu item, a window is created (if it doesn't already exist).

It will get the available bundles (using the NSBundle's pathsForResourcesOfType:inDirectory: method) and for each bundle, it will load it's icon, label and it's main class.

Then, it will create a NSToolbarItem for each available bundle, set it's label and icon accordingly.

When the user selects a toolbar item, the method selectItemIdentifier: is called with that itemIdentifier. If a different itemIdentifier has been selected than the one already displayed, we instantiate an instance of the new bundle's class, get it's view ...and set that view as the preference window's content view.

Requirements

In order to make everything work, you'll be needing Developer Tools installed along with Xcode and InterfaceBuilder.

About this document

This document was created using Sphinx documentation generator.

Dive In

Our application will have two components:

  1. main application
  2. preference panes

The principle is rather simple. We create the main application whose preferences will be constructed from prefPane bundles.

Creating two prefPane bundles

First, you need to create a prefPane bundle. In order to do this, you must create a new PreferencePane project which can be found in Xcode as Standard Apple Plug-ins.

In Xcode, select File, New Project, then select Standard Apple Plug-ins in the left panel. SelectPreferencePane in the right panel and click the Choose... button.

Give it a simple name, such as: general and click the Save button.

This newly created project already contains the necessary stuff. For example, Xcodeautomagically creates a class in the Classes group, entitled generalPref which is a subclass of NSPreferencePane. In the Resources group you have a xib file and a tiff file. We'll be using the tiff file as a toolbar item icon in the main application's preference window, so replace that tiff with tha one that you want to be displayed in PrefsApp preference window toolbar.

For now, simply open the xib file in InterfaceBuilder, drag a button or a text field, then, save the file.

Now, build the bundle either by clicking the Build toolbar button, or by using a keyboard shortcut (Apple key + B).

Do the same thing with another PreferencePane project, but this time, give it a different name, such as advanced.

Creating the main application

In Xcode, select File, New Project, then select Application in the left panel and select Cocoa Application in the right panel. Click the Choose... button, give it a name such as PrefsApp, then click the Save button.

This will create a default application. Now, we'll need to create two classes in the Classesgroup. One will serve as the application controller (AppController would be a good name), the other will be a subclass of NSWindowController (PrefsWindowController would be a good name).

Now, the workflow is relatively simple. AppController class will take care of showing the preference window (and other stuff you need to do). PrefsWindowController takes care of setting up the preference window.

Now, we need to define an instance method in AppController calledshowPreferences:(the name can be any of your choosing). This instance method will be the selector of the application's Preferences... menu item.

Listing of AppController.h

#import

@interface AppController : NSObject {
}

- (IBAction)showPreferences:(id)sender;

@end


Listing of AppController.m

#import "AppController.h"
#import "PrefsWindowController.h"

@implementation AppController

/**
* The "Preferences..." menu item should be connected
* to this method.
*/

- (IBAction)showPreferences:(id)sender
{
[[PrefsWindowController sharedInstance] showPreferences];
}

@end


Now, go into the Resources group and open MainMenu.xib file in InterfaceBuilder. Drag anObject Controller item into the Document window and set it's class to AppController.

Next, connect the App Controller object's showPreferences: method to the Preferences...menu item. You could also change all the menu items that contain the text NewApplication intoPrefsApp so it will look nice :).

Next, you need to edit PrefsWindowController.h and PrefsWindowController.mfiles as listed below.

Listing of PrefsWindowController.h

#import
#import


@interface PrefsWindowController : NSWindowController {
id oldPrefPaneObject;
id oldItemIdentifier;
}

+ (id)sharedInstance;
+ (NSArray *)availableBundles;

- (void)setupUI;
- (void)showPreferences;

- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar;
- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar;
- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar;
- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag;
- (IBAction)selectItemIdentifier:(id)sender;
- (void)changePrefPaneTo:(NSString *)newItemIdentifier;

@end


Listing of PrefsWindowController.m

#import "PrefsWindowController.h"


static id instance = nil;

@implementation PrefsWindowController

/**
* Call this class method each time you need the preference
* window controller instance.
*/

+ (id)sharedInstance {
if (!instance)
return [[PrefsWindowController alloc] init];

return instance;
}

/**
* This method returns the available prefPane bundles.
*/

+ (NSArray *)availableBundles {
return [[NSBundle mainBundle] pathsForResourcesOfType:@"prefPane" inDirectory:nil];
}

/**
* Used internally. Don't call this method unless you know what
* you are doing.
*/

- (id)init {

if (!(self = [super init]))
return nil;

instance = self;
return instance;
}

/**
* This instance method sets up the preference window.
*/

- (void)setupUI {

// We create the window and set the window
NSWindow *window = [[NSWindow alloc] initWithContentRect:NSMakeRect(0, 0, 300, 200)
styleMask:NSTitledWindowMask|NSClosableWindowMask
backing:NSBackingStoreBuffered defer:NO];
// We require this in order to receive windowShouldClose: message
[window setDelegate:self];

// We set the controller's window to window
[self setWindow:window];

// We remove the window's toolbar button
[window setShowsToolbarButton:NO];

// We create the window's toolbar
NSToolbar *toolbar = [[NSToolbar alloc] initWithIdentifier:@"PrefsToolbar"];

// We populate the toolbar with toolbar items
[toolbar setDelegate:self];

// We set the window's toolbar to this newly created toolbar
[[self window] setToolbar:[toolbar autorelease]];
// We center the newly created window
[[self window] center];

// We set the first pane as selected
NSArray *availableBundles = [PrefsWindowController availableBundles];
if (availableBundles && [availableBundles count] > 0)
[self changePrefPaneTo:[availableBundles objectAtIndex:0]];
}

/**
* This instance method should be called each time you need
* to display the preference window.
*/

- (void)showPreferences {

/**
* We only call setupUI if the window is not
* yet initialized, otherwise we simply display
* the window.
*/

if ([self window])
[[self window] makeKeyAndOrderFront:nil];
else {
[self setupUI];
[[self window] makeKeyAndOrderFront:nil];
}
}

/**
* See NSToolbar documentation.
*/

- (NSArray *)toolbarAllowedItemIdentifiers:(NSToolbar *)toolbar
{
return [PrefsWindowController availableBundles];
}

/**
* See NSToolbar documentation.
*/

- (NSArray *)toolbarDefaultItemIdentifiers:(NSToolbar *)toolbar
{
return [PrefsWindowController availableBundles];
}

/**
* See NSToolbar documentation.
*/

- (NSArray *)toolbarSelectableItemIdentifiers:(NSToolbar *)toolbar
{
return [PrefsWindowController availableBundles];
}

/**
* See NSToolbar documentation.
*/

- (NSToolbarItem *)toolbar:(NSToolbar *)toolbar itemForItemIdentifier:(NSString *)itemIdentifier willBeInsertedIntoToolbar:(BOOL)flag
{
// We try to get the prefPane bundle of the selected itemIdentifier
NSBundle *bundle = [NSBundle bundleWithPath:itemIdentifier];
if (!bundle)
return nil;

// We get the label from the prefPane's Info.plist file
NSString *prefPaneIconLabel = [bundle objectForInfoDictionaryKey:@"NSPrefPaneIconLabel"];

// We get the icon name from the prefPane's Info.plist file
NSString *prefPaneIconPath = [bundle pathForImageResource:[bundle objectForInfoDictionaryKey:@"NSPrefPaneIconFile"]];

// We create a new toolbar item for this itemIdentifier
NSToolbarItem *toolbarItem = [[NSToolbarItem alloc] initWithItemIdentifier:itemIdentifier];

// We set it's label and image
[toolbarItem setLabel:prefPaneIconLabel];
[toolbarItem setPaletteLabel:prefPaneIconLabel];
[toolbarItem setImage:[[[NSImage alloc] initWithContentsOfFile:prefPaneIconPath] autorelease]];

// When we click the toolbar item, it should call selectItemIdentifier
[toolbarItem setTarget:self];
[toolbarItem setAction:@selector(selectItemIdentifier:)];

return [toolbarItem autorelease];
}

/**
* This instance method gets called when the user selects a
* toolbar item from the preference window.
*/

- (IBAction)selectItemIdentifier:(id)sender
{
[self changePrefPaneTo:[sender itemIdentifier]];
}

/**
* This instance method does the actual panel switching.
*/

- (void)changePrefPaneTo:(NSString *)newItemIdentifier
{

// We don't switch if the oldItemIdentifier is the same as the
// newItemIdentifier
if ([oldItemIdentifier isEqualToString:newItemIdentifier])
return;

// If an old pane has been initialized, we get it's bundle, release
// it's resources and unload the bundle
if (oldPrefPaneObject) {
NSBundle *oldPrefPaneBundle = [NSBundle bundleWithPath:[[[self window] toolbar] selectedItemIdentifier]];

[[oldPrefPaneObject mainView] removeFromSuperview];
[oldPrefPaneObject release];
[oldPrefPaneBundle unload];
}

// We get the newItemIdentifier's bundle
NSBundle *newPrefPaneBundle = [NSBundle bundleWithPath:newItemIdentifier];

// We initialize an instance of the new bundle's class
id newPrefPaneObject = [[[newPrefPaneBundle principalClass] alloc] initWithBundle:newPrefPaneBundle];

oldPrefPaneObject = newPrefPaneObject;
oldItemIdentifier = newItemIdentifier;

[newPrefPaneObject loadMainView];
[newPrefPaneObject willSelect];

NSView *newPrefPaneView = [newPrefPaneObject mainView];

// This code makes the window to resize acordingly to the newPrefPaneView
NSRect newWindowFrame = [[self window] frameRectForContentRect:[newPrefPaneView frame]];
newWindowFrame.origin = [[self window] frame].origin;
newWindowFrame.origin.y -= newWindowFrame.size.height - [[self window] frame].size.height;
[[self window] setFrame:newWindowFrame display:YES animate:YES];

[[self window] setContentView:newPrefPaneView];
[[self window] setTitle:[newPrefPaneBundle objectForInfoDictionaryKey:@"NSPrefPaneIconLabel"]];

[[[self window] toolbar] setSelectedItemIdentifier:newItemIdentifier];

[newPrefPaneObject didSelect];
}

/**
* We re-implement this method in order to release the
* resources taken by the preference window.
*/

- (BOOL)windowShouldClose:(id)window
{
[[self window] autorelease];
[self setWindow:nil];

oldItemIdentifier = nil;
oldPrefPaneObject = nil;

return YES;
}

@end

Next, you need to add the two prefPane bundles to this project. Right click on the Resourcesgroup, select Add, then Existing Frameworks... . In this window, select one of the prefPanebundles you created above.

Now, in order to automatically build the two prefPane bundles you need to add them as Direct Dependencies. To do this, expand Targets group, and select the Get Info contextual menu item of the PrefsApp target (or whichever name you picked up for your preference application). Select the General tab and click the + sign of the Direct Dependencies table view.

After adding the two prefPane bundles as direct dependencies, you simply need to add them to the Copy Bundle Resources group of the PrefsApp target.

What we did is this, each time you build PrefsApp, it will automatically build the two bundles (because of the Direct Dependencies thing...), then, Xcode will automatically copy the twoprefPane bundles in the Resources directory of the PrefsApp application bundle (because of theCopy Bundle Resources thing...).

0 comentarii: