Free Trial

Safari Books Online is a digital library providing on-demand subscription access to thousands of learning resources.


  • Create BookmarkCreate Bookmark
  • Create Note or TagCreate Note or Tag
  • DownloadDownload
  • PrintPrint
Share this Page URL
Help

Recipe 10. Add a Preferences Window > Step 5: Configure the Chef’s Diary Tab Vi...

Step 5: Configure the Chef’s Diary Tab View Item

The Chef’s Diary tab view item contains four sections. Two of them, at the top, duplicate functionality you have already implemented for other views. I will outline what you need to do to implement them here, leaving most of the work as an exercise for the reader. In both cases, the code and Interface Builder settings are fully implemented in the downloadable project file for this recipe.

1.
The first section of the Chef’s Diary pane is a group of controls that are identical to the controls that you just implemented in the Recipes pane. You should implement the Chef’s Diary controls exactly the same way you implemented the corresponding controls in the Recipes pane, but use the diary window controller instead of the recipes window controller. I won’t repeat the instructions here, but they are fully implemented in the downloadable project file for this recipe. Be sure to follow all of the tasks in Step 4, including using Interface Builder to make the necessary outlet and action connections and to reset the class of the Use Current Size button. In addition to adding outlet accessors and action methods, there are several methods you wrote in Step 4 that must be modified to work with the diary window as well as the recipes window. For example, set the initial standard size of the diary window in +[VRApplicationController initialize] to 600.0 by 900.0 pixels by adding another object and the VRDefaultDiaryWindowStandardSizeKey key to the call to +dictionaryWithObjectsAndKeys:.

2.
The controls in the Printing section of the Chef’s Diary pane are identical to the corresponding controls in the All Print Jobs section of the DiaryPrintPanelAccessoryView nib file that you created in Recipe 9.

Their accessor methods are identical to those you wrote in Recipe 9 as well. Simply copy and paste the declarations and the implementations of the -printTagsCheckbox, -printHeadersAndFootersCheckbox, and -printTimestampRadioGroup accessor methods from the DiaryPrintPanelAccessoryController header and implementation files into the PreferencesWindowController header and implementation files, and connect all of them in the PreferencesWindow nib file. Copy and paste the instance variables corresponding to the accessor methods.

The action methods are different. In the diary Print panel accessory controller, the action methods set the printing defaults through a represented object. Here, you set the defaults directly, just as you did in the action methods for the General and Recipes panes. Declare them in the PreferencesWindowController.h header file like this:

- (IBAction)setDefaultDiaryPrintTags:(id)sender;
								- (IBAction)setDefaultDiaryPrintHeadersAndFooters:(id)sender;
								- (IBAction)setDefaultDiaryPrintTimestamp:(id)sender;

Define them in the PreferencesWindowController.m implementation file like this:

- (IBAction)setDefaultDiaryPrintTags:(id)sender {
								[[NSUserDefaults standardUserDefaults] setBool:[sender state]
								forKey:VRDefaultDiaryPrintTagsKey];
								}
								- (IBAction)setDefaultDiaryPrintHeadersAndFooters:(id)sender {
								[[NSUserDefaults standardUserDefaults] setBool:[sender state]
								forKey:VRDefaultDiaryPrintHeadersAndFootersKey];
								}
								- (IBAction)setDefaultDiaryPrintTimestamp:(id)sender {
								[[NSUserDefaults standardUserDefaults] setInteger:[sender selectedRow]
								forKey:VRDefaultDiaryPrintTimestampKey];
								}

					  

You must import the DiaryPrintPanelAccessoryController.h header file into the PreferencesWindowController.m implementation file in order to use the keys for the user defaults printing settings, like this:

#import "DiaryPrintPanelAccessoryController.h"

Connect the action methods in Interface Builder.

To display the user defaults printing settings when the user opens the preferences window, you must first set them in the preferences window controller’s +initialize method, in the PreferencesWindowController.m implementation file. You wrote the necessary +initialize method in Recipe 9, but you placed it in the DiaryDocument.m implementation file. That is too late if the user opens the preferences window before opening the Chef’s Diary. Delete the +initialize method from the diary document implementation file, and instead initialize its objects with the appropriate keys in the +[VRApplicationController initialize] method you started writing in Step 4. It should now look like this:

+ (void)initialize {
								if (self == [VRApplicationController class]) {
								NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
								NSDictionary *initialUserDefaults = [NSDictionary
								dictionaryWithObjectsAndKeys:
								NSStringFromSize(NSMakeSize(1200.0, 800.0)),
								VRDefaultRecipesWindowStandardSizeKey,
								NSStringFromSize(NSMakeSize(600.0, 900.0)),
								VRDefaultDiaryWindowStandardSizeKey,
								[NSNumber numberWithBool:NO],
								VRDefaultDiaryPrintTagsKey,
								[NSNumber numberWithBool:YES],
								VRDefaultDiaryPrintHeadersAndFootersKey,
								[NSNumber numberWithInteger:0],
								VRDefaultDiaryPrintTimestampKey, nil];
								[defaults registerDefaults:initialUserDefaults];
								}
								}

You could leave out the settings for NO and 0 because NSUserDefaults defaults to those values, but I find my code easier to maintain if I initialize all related defaults explicitly.

Don’t forget to import the DiaryPrintPanelAccessoryController.h header file into the VRApplicationController.m implementation file, in order to use the printing defaults keys, like this:

#import "DiaryPrintPanelAccessoryController.h"

Now add statements at the end of the -windowDidBecomeKey: method in the PreferencesWindowController.m implementation file to display the printing user defaults when the user opens the preferences window. The printing controls, like the General pane checkboxes, have to be displayed in the -windowDidBecomeKey: method instead of the -windowDidLoad method because the printing controls can be changed externally, in the custom accessory view of the Chef’s Diary Print panel. Here are the statements to add:

[[self printTagsCheckbox] setState:([defaults boolForKey:
								VRDefaultDiaryPrintTagsKey]) ? NSOnState : NSOffState];
								[[self printHeadersAndFootersCheckbox] setState:([defaults boolForKey:
								VRDefaultDiaryPrintHeadersAndFootersKey]) ? NSOnState : NSOffState];
								[[self printTimestampRadioGroup] setState:NSOnState atRow:
								[defaults integerForKey:VRDefaultDiaryPrintTimestampKey] column:0];

					  

The printing section of the Chef’s Diary pane in the preferences window is now working, except for one issue. In Step 3, which also involved user defaults settings that could be changed in two places, you arranged to observe the NSUserDefaultsDidChangeNotification notification so that a change in one place would be immediately reflected onscreen in the other place. For the sake of consistency and a good user experience, you should implement the same behavior here in case the user has the Chef’s Diary Print panel and the Chef’s Diary pane of the preferences window open at the same time. I won’t repeat in full the explanation of how to synchronize these settings—this is left as an exercise for the reader. Just follow tasks 9 and 10 of Step 3 with appropriate changes. The necessary code is in place in the downloadable project file for Recipe 10.

Here’s a hint for updating the preferences window when the user makes changes in the Print panel and dismisses it: The preferences window controller is already registered to observe the NSUserDefaultsDidChangeNotification notification, and you have already written the code to respond to the notification and to remove the observer. All you have to do to update the Chef’s Diary pane when the user changes the Print panel and dismisses it is to add three statements to the existing -userDefaultsDidChange: method. In fact, you’ve already written them, so this requires nothing more than copying and pasting the statements or writing a method. They’re in the -windowDidBecomeKey: method.

Updating the Print panel when the user changes settings in the Chef’s Diary pane will take almost as little work. Most of the code that you wrote in the preferences window controller to update the preferences window when the user changes settings in the Print panel can be copied and pasted into the diary Print panel accessory controller, because the instance variables for the accessor methods for the two checkboxes and the radio group, as well as for the userDefaultsDidChangeObserver notification observer, are named the same in both files. Copy and paste the following declarations and implementations from the PreferencesWindowController class into the DiaryPrintPanelAccessoryController class: the userDefaultsDidChangeObserver instance variable; the -setUserDefaultsDidChangeObserver: and -userDefaultsDidChangeObserver accessor methods; and the -dealloc method to unregister the observer.

There are only two places where you need to write new code, and even it has already been written. At the end of the -loadView method in the DiaryPrintPanelAccessoryController.m implementation file, add these statements to register the observer—you can copy them from the -windowDidLoad method in the PreferencesWindowController.m implementation file and paste them into the -loadView method:

								NSNotificationCenter *defaultCenter =
								[NSNotificationCenter defaultCenter];
								#if MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_6
								[defaultCenter addObserver:self selector:
								@selector(userDefaultsDidChange:)
								name:NSUserDefaultsDidChangeNotification  object:nil];
								#else
								[self setUserDefaultsDidChangeObserver:[defaultCenter
								addObserverForName:NSUserDefaultsDidChangeNotification
								object:nil queue:nil
								usingBlock:^(NSNotification *notification) {
								[self userDefaultsDidChange:notification];
								}]];
								#endif

The other method you have to write is the notification method itself, to be added after the Override Methods section of the DiaryPrintPanelAccessoryController.m implementation file:

#pragma mark NOTIFICATION METHODS
								- (void)userDefaultsDidChange:(NSNotification *)notification {
								NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
								[[self printTagsCheckbox] setState:([defaults
								boolForKey:VRDefaultDiaryPrintTagsKey])
								? NSOnState : NSOffState];
								[[self printHeadersAndFootersCheckbox] setState:([defaults
								boolForKey:VRDefaultDiaryPrintHeadersAndFootersKey])
								? NSOnState : NSOffState];
								[[self printTimestampRadioGroup] setState:NSOnState
								atRow:[defaults integerForKey:VRDefaultDiaryPrintTimestampKey]
								column:0];
								}

Again, you can copy the body of this method from the corresponding method in the preferences window controller and paste it in. Declare it in the DiaryPrintPanelAccessoryController.h header file like this:

#pragma mark NOTIFICATION METHODS
								- (void)userDefaultsDidChange:(NSNotification *)notification;

You’re now done with the Printing section of the Chef’s Diary pane. When you change these settings in the preferences window, the Print panel recognizes them either immediately, if it was open, or later when the user opens it. Changes to these settings in the Print panel are reflected in the preferences window, either when the user closes the Print panel to commit the changes, or later when the user opens the preferences window.

3.
Next, implement the Autosaving section of the Chef’s Diary pane of the preferences window. This is very straightforward.

In Recipe 7, you implemented the -applicationDidfinishLaunching: delegate method in VRApplicationController to set the autosaving delay to 5.0 seconds. This was a stopgap to turn on autosaving and set the interval to a very short period for purposes of testing. Now you should give the user the option to turn off autosaving by setting the interval to 0.0 as well as some more realistic intervals.

As with all user defaults settings, you need a key under which to store the value of the interval, and an action method to connect to the pop-up button in the Autosaving section of the Chef’s Diary pane. You should also set the initial default value in the +initialize method in VRApplicationController, and you should arrange for the preferences window to display the current setting when the user opens it.

Start by defining the key. This preference is to apply to any and all documents, so a good place to define it is in the VRDocumentController class. That’s where Cocoa declares the -setAutosavingDelay: method, too. Add this declaration before the @interface directive at the top of the VRDocumentController.h header file:

extern NSString *VRDefaultDiaryAutosaveIntervalKey;

Define it at the end of the VRDocumentController.m implementation file like this:

NSString *VRDefaultDiaryAutosaveIntervalKey =
								@"diary document autosave interval";

Write the action method in the preferences window controller. Declare it at the end of the Action Methods section of the PreferencesWindowController.h header file:

- (IBAction)setDefaultDiaryAutosaveInterval:(id)sender;

Define it in the implementation file:

- (IBAction)setDefaultDiaryAutosaveInterval:(id)sender {
								NSTimeInterval interval;
								switch ([sender indexOfSelectedItem]) {
								case 0:
								interval = 15.0;
								break;
								case 1:
								interval = 30.0;
								break;
								case 2:
								interval = 60.0;
								break;
								case 3:
								interval = 300.0;
								break;
								case 4:
								interval = 0.0;
								break;
								}
								[[NSUserDefaults standardUserDefaults] setDouble:interval
								forKey:VRDefaultDiaryAutosaveIntervalKey];
								}

					  

The user defaults value is saved as a double because the NSTimeInterval type is declared as a double.

The VRDocumentController.h header file is already imported into the PreferencesWindowController.m implementation file, so the VRDefaultDiary AutosaveIntervalKey key is available here.

Build the project and connect the action method to the First Responder proxy in the PreferencesWindow nib file in Interface Builder.

To display the preference setting when the user opens the preferences window, you need an instance variable and accessor method for the pop-up menu. Declare the instance variable and getter in the PreferencesWindowController.h header file separately like this:

IBOutlet NSPopUpButton *diaryAutosaveIntervalButton;
								- (NSPopUpButton *)diaryAutosaveIntervalButton; // ADDED IN RECIPE 10

					  

Define it in the PreferencesWindowController.m implementation file like this:

- (NSPopUpButton *)diaryAutosaveIntervalButton {
								return diaryAutosaveIntervalButton ;
								}

Build the project and connect the outlet.

Now add the following code to the end of the -windowDidLoad method in the PreferencesWindowController.m implementation file:

double interval = [defaults doubleForKey:
								VRDefaultDiaryAutosaveIntervalKey];
								NSInteger idx;
								if (interval == 15.0) {
								idx = 0;
								} else if (interval == 30.0) {
								idx = 1;
								} else if (interval == 60.0) {
								idx = 2;
								} else if (interval == 300.0) {
								idx = 3;
								} else if (interval == 0.0) {
								idx = 4;
								}
								[[self diaryAutosaveIntervalButton ] selectItemAtIndex:idx];

Finally, revise the existing -applicationDidFinishLaunching: delegate method so that it sets the current autosaving interval according to the user’s preference:

- (void)applicationDidFinishLaunching:(NSNotification *)notification {
								[[VRDocumentController sharedDocumentController]
								setAutosavingDelay:[[NSUserDefaults standardUserDefaults]
								doubleForKey:VRDefaultDiaryAutosaveIntervalKey]];
								}

You’re done. Build and run the application, open the preferences window, and select the Chef’s Diary pane. The selected pop-up menu item is Never, because you did not set an initial default value for the autosaving interval and it therefore defaults to 0.0 seconds. The code you just wrote therefore sets the index to 4, which is the menu item titled Never.

I don’t feel safe without autosaving, so make one more change to the code. In the +initialize method in VRApplicationController, add the object [NSNumber numberWithDouble:30.0] and the key VRDefaultDiaryAutosaveIntervalKey just before the nil at the end of the call to +ictionaryWithObjectsAndKeys:.

4.
The last section of the Chef’s Diary pane contains a text field where the user can set the current Chef’s Diary document. When the user first opens the preferences window, this text field should display the full path to the current Chef’s Diary document, if there is one, or be left blank if there is not. The user can type in the full path to another diary document, such as a backup, or the full path to any PDF file, to change the current Chef’s Diary to the other file.

To display the file’s path, you need an instance variable and getter for the text field. In the PreferencesWindowController.h header file, separately declare them like this:

IBOutlet NSTextField *currentDiaryDocumentTextField;
								- (NSTextField *)currentDiaryDocumentTextField;

Define the getter in the PreferencesWindowController.m implementation file like this:

- (NSTextField *)currentDiaryDocumentTextField {
								return currentDiaryDocumentTextField;
								}

Build the project and connect the outlet in the PreferencesWindow nib file.

Add the following statements to the end of the -windowDidBecomeKey: delegate method in the PreferencesWindowController.m implementation file to display the current Chef’s Diary’s path in the text field. This has to be done in the -windowDidBecomeKey: delegate method instead of the -windowDidLoad method because the current diary document can be set externally, when the user saves the Chef’s Diary.

NSString *path = [[[VRDocumentController sharedDocumentController]
								currentDiaryURL] path];
								if (path) [[self currentDiaryDocumentTextField] setStringValue:path];

					  

You check whether the path is nil because it might be if the file is not there. Setting the string value of a text field raises an exception if the argument is nil.

Add the same two statements at the end of the -userDefaultsDidChange: notification method in the PreferencesWindowController.m implementation file. Now the text field will update immediately if the user saves a new current diary document, even if the preferences window remains in the background.

The last task is to write an action method that allows the user to enter a new path in the current diary text field and make the file at that path the new current Chef’s Diary. Declare the action method at the end of the Action Methods section of the PreferencesWindowControler.h header file like this:

- (IBAction)setDefaultCurrentDiaryDocument:(id)sender;

Define it like this at the end of the same section of the PreferencesWindowController.m implementation file:

- (IBAction)setDefaultCurrentDiaryDocument:(id)sender {
								NSString *path = [sender stringValue];
								BOOL isDirectory;
								BOOL fileExists = [[NSFileManager defaultManager]
								fileExistsAtPath:path isDirectory:&isDirectory];
								if (fileExists & !isDirectory) {
								NSString *type;
								BOOL success = [[NSWorkspace sharedWorkspace] getInfoForFile:path
								application:NULL type:&type];
								if (success && ([type isEqualToString:@"vrdiary"]
								|| [type isEqualToString:@"rtf"])) {
								[[VRDocumentController sharedDocumentController]
								setCurrentDiaryURL:[NSURL fileURLWithPath:path]];
								} else {
								[[self alertWrongFileType] beginSheetModalForWindow:
								[self window] modalDelegate:nil didEndSelector:NULL
								contextInfo:NULL];
								}
								} else {
								[[self alertNoSuchFile] beginSheetModalForWindow:
								[self window] modalDelegate:nil didEndSelector:NULL
								contextInfo:NULL];
								}
								}

					  

Connect the action method in the PreferencesWindow nib file.

The action method uses both of Cocoa’s file system classes, NSFileManager and NSWorkspace, to determine whether the path the user entered is valid. The NSFileManager method -fileExistsAtPath:isDirectory: is called to determine whether a file exists at that path and, if so, whether it is a directory. Neither the diary document nor a Rich Text Format (RTF) file that can be used as a diary document is expected to be a bundle or directory. If a flat file exists at the given path, the NSWorkspace method -getInfoForFile:application:type: is called to get its type, which is its file extension. You could just as well have called [path pathExtension]. If the type is either @“vrdiary” or @“rtf”, the existing -setCurrentDiaryURL: method in VRDocumentController is called to set the new user defaults value for the current Chef’s Diary.

It is very easy to type an incorrect file path, whether due to typographical errors or mistaken understanding of the file’s path. It would therefore be friendly to the user to provide some feedback about the reason for any error. As alternatives to displaying an alert, there are other user interface elements for entering path information that may be easier to use, but Vermont Recipes already supports using the Finder to make any file the current Chef’s Diary simply by opening it. There is no need to make this preferences window setting more robust, so simply display an informative error if a problem arises.

Declare the two alerts at the end of the primary class in the PreferencesWindowController.h header file like this:

#pragma mark ALERTS
								- (NSAlert *)alertNoSuchFile;
								- (NSAlert *)alertWrongFileType;

Define them at the end of the primary class in the PreferencesWindowController.m implementation file like this:

#pragma mark ALERTS
								- (NSAlert *)alertNoSuchFile {
								NSBeep();
								NSAlert *alert = [[[NSAlert alloc] init] autorelease];
								[alert setMessageText:NSLocalizedString(@"The file was not found.",
								@"message text for NoSuchFile alert")];
								[alert setInformativeText:[NSString stringWithFormat:
								NSLocalizedString(@"No file exists at %@. Check the path and try
								again.", @"informative text for NoSuchFile alert"),
								[[self currentDiaryDocumentTextField] stringValue]]];
								return alert;
								}
								- (NSAlert *)alertWrongFileType {
								NSBeep();
								NSAlert *alert = [[[NSAlert alloc] init] autorelease];
								[alert setMessageText:NSLocalizedString(@"The file is the wrong
								type.", @"message text for alertWrongFileType alert")];
								[alert setInformativeText:[NSString stringWithFormat:
								NSLocalizedString(@"The file at %@ is not a Vermont Recipes
								Chef's Diary file or a Rich Text Format file. Enter a path
								ending in ".vrdiary" or ".rtf" and try again.",
								@"informative text for alertWrongFileType alert"),
								[[self currentDiaryDocumentTextField] stringValue]]];
								return alert;
								}

					  

5.
You have finished the Chef’s Diary pane of the preferences window. Build and run the project to test it. To test the Chef’s Diary Window section, perform the same tests you performed at the end of Step 4, but using the diary window instead of the recipes window. To test the Printing section, perform the same tests you performed on the All Print Jobs section of the Print panel in Recipe 9.

To test the Autosaving section, simply choose a setting; then make some changes to the Chef’s Diary and see how long it takes for an autosaved copy to appear.

To test the Document section, make sure that you have saved a Chef’s Diary; then open the preferences window and go to the Chef’s Diary pane. You should see the path to the document in the text field. Type a spurious letter into the field so that the path is invalid, and then press Return. You see an alert sheet explaining the error. Enter the path of an existing file that is neither a Vermont Recipes diary document nor an RTF file, and you see a different alert. In either case, the text field reverts to its prior content after you dismiss the alert. Try saving a good Chef’s Diary document under a different name to make the new file the current Chef’s Diary document. If you left the preferences window visible on the screen, you saw the path in the text field change the instant you dismissed the Save panel. Close the new Chef’s Diary, and then type the path of the old Chef’s Diary document into the field and press Return. Now when you choose File > Open Chef’s Diary, the old Chef’s Diary opens because you made it the current Chef’s Diary by typing its path into the text field.


  

You are currently reading a PREVIEW of this book.

                                                                                        

Get instant access to over
$1 million worth of books and videos.

  

Start a Free Trial