How To Build VocableTrainerX
A.	Change file and class names for the document class
1.	Open a new Cocoa document-based application project in Xcode: VocableTrainerX
2.	In the project window, rename MyDocument.h and MyDocument.m to VTXDocument.h and VTXDocument.m (it’s easier if you don’t change the names in the Finder first)
3.	In the project window, rename MyDocument.nib to VTXDocument.nib
4.	Open VTXDocument.nib in Interface Builder and rename the document class from MyDocument to VTXDocument in the class browser
5.	In VTXDocument.h and VTXDocument.m, change the name of the class declarations to VTXDocument; in VTXDocument.m, import the renamed header file VTXDocument.h instead of MyDocument.h
6.	In the windowNibName method of VTXDocument.m, temporarily set the method to return a value of @"VTXDocument"
7.	Select the target VocableTrainerX and in its Info window, change the Document Class to VTXDocument; build and run the project to test the new naming scheme
B.	Preliminary layout of the user interface using Interface Builder to add a table view to the main window and an inspector panel with text fields for data entry
1.	In VTXDocument.nib, add the NSWindow-NSDrawer combination object to the Instances pane
2.	In the nib file, set the drawer to open along the right edge of the window as the preferred edge (use the Info window for the NSDrawer, Attributes pane)
3.	Select the new window and set an appropriate minimum size for it using the Size panel of NSWindow Info; make the window’s default size about 20% larger than the minimum
4.	Add a table view to the document window's content area; leave space at the top of the content area for a small text field; expand the table view to fill the window, set its auto-sizing behavior to resize with the window
5.	This example presents a table with four columns, but if your needs differ, it is trivial to give it a different size, with different labeling and model, etc.
6.	Assign default header cells for the table columns: Some Language, Another Language, Topic, Score; make the columns appropriately resizable; set table properties to include multiple selection and to auto-resize the columns to fit
7.	Add an NSPanel object to VTXDocument.nib; this panel will not be resizable by default
8.	Add four editable text fields to the panel in a column orientation; make them wide enough to accommodate several words of text in each one
9.	Add a non-editable text field to the right of each editable text field to serve as a label; for now, just match the labels in the table column header cells
C.	Create a subclass of NSWindowController
1.	In VTXDocument.nib, locate NSWindowController in the class browser in the Classes pane of IB; choose Classes —> Subclass NSWindowController and name the new class VTXMainWindowController
2.	Select File's Owner in the Instances pane; select the Custom Classes tab in the Info window and designate VTXMainWindowController as the new class for File's Owner
3.	Change the connection between File's Owner and its window outlet to point to the new window, then delete the original window from the Instances pane
4.	For VTXMainWindowController in VTXDocument.nib, add an outlet that points to the drawer (topicsDrawer); connect the outlet by control-dragging from File's Owner to NSDrawer in the Instances pane and clicking Connect
5.	Create an action to operate the drawer (toggleDrawer:); install the new action in the Attributes pane of the VTXMainWindowController Info window in VTXDocument.nib
6.	Add an item to the Window menu in MainMenu.nib, labeled Toggle Drawer with shortcut CMD-K; add the action to the list of actions known to First Responder in MainMenu.nib; to target the action on First Responder in MainMenu.nib control-drag and click Connect
7.	Connect the delegate outlet of the main window to VTXMainWindowController in the Instances pane in VTXDocument.nib by control-dragging to File's Owner and clicking the Connect button
8.	Create the files for VTXMainWindowController from IB and add them to the project in Xcode; for now just log the toggle action in its implementation in VTXMainWindowController.m
D.	Modify the Finder-related information in project settings, strings, and property list files
1.	Open the Properties pane in the Target Info window; change the Document Types Name field to Vocable Trainer X Document; this modifies the string for the CFBundleTypeName key in Info.plist; make sure the string value associated with the NSDocumentClass key is VTXDocument
2.	Also in Info.plist, change the CFBundleVersion string to 1 (use version number 0.8.0d1 for the CFBundleShortVersionString in the InfoPlist.strings file)
3.	Appropriately modify the copyright information in InfoPlist.strings and make similar changes in Credits.rtf, or replace it with a Credits.html file
4.	In the Target Info window Properties pane, change the Identifier to an appropriate value (in this application it is net.sourceforge.vocx); this will set the name for the app’s Preferences file in ~/Library/Preferences
5.	Revise the menus: Change occurrences of New Application to VocableTrainerX in all the menu bar items that require it
E.	Replace and/or modify initialization methods in the document class
1.	Remove the windowNibName method from VTXDocument.m; override the method makeWindowControllers instead, with:
- ( void ) makeWindowControllers
{
VTXMainWindowController *controller = 
[ [ VTXMainWindowController alloc ] init ];
[ self addWindowController: controller ];
[ controller release ];
}
2.	Remove the delegate method windowControllerDidLoadNib: in VTXDocument.m; instead, use standard initialization and cleanup methods in VTXMainWindowController:
- ( id ) init
{
if (self = [ super initWithWindowNibName: @”VTXDocument” ])
{
// close document if main window closes
[ self setShouldCloseDocument: YES ];
}
return self;
}
- ( void ) dealloc
{
[ [ NSNotificationCenter defaultCenter ]
removeObserver: self ];
[ super dealloc ];
}
F.	Set up basic connections for the table, drawer, and inspector panel
1.	Add a setupDrawer method to post notifications when the drawer opens and closes; invoke setupDrawer in awakeFromNib in VTXMainWindowController
2.	Declare an outlet to the table view (lexiconTable) in VTXMainWindowController and in VTXDocument.nib; connect the outlet in IB; implement an accessor that returns the id of the table view in VTXMainWindowController
3.	Declare an outlet to the inspector panel in the window controller and nib file; make an accessor to return the id of the inspector panel; create an action to display the inspector panel and connect it to a menu item (Inspector) in the Window menu (shortcut CMD-I); you can use the panel’s makeKeyAndOrderFront: method to display it; if you set the panel’s Utility window (Panel only) property, it will close when the main window does
4.	Now that everything is in place, implement the toggleDrawerAction: method: in VTXMainWindowController.m and test the skeleton of the application
- ( IBAction ) toggleDrawerAction: ( id ) sender
{
[ topicsDrawer toggle: self ];
}
G.	Implement the document window toolbar category and toolbar stub action methods
1.	Add the files VTXToolbarDelegateCategory.h and VTXToolbarDelegateCategory.m; add the associated button image files to a new project group called Images (see project files)
2.	Implement stub action methods for the toolbar actions; declare the toolbar actions and an outlet for a toolbar search field instance variable; place a search field inside an auxiliary view added to VTXDocument.nib in order to connect an outlet to it
3.	Implement the setupToolbar method and a stub controlTextDidChange method in VTXMainWindowController (invoke the toolbar setup in awakeFromNib)
4.	Connect the search field’s delegate outlet to the window controller; set the search field delegate to nil in dealloc
5.	Implement windowTitleForDocumentDisplayName: to add the name of the application to the title bar of the document window and inspector panel
H.	Use the Cocoa Controller Layer to bind the table view to data in a model class
1.	Create a KVC-enabled model class named VTXVocable that conforms to <NSCoding>, with strings (and accessors for) firstL, secondL, topic, and score (the latter will be supported by int variables numGuesses and numCorrect, which are described below); the getter accessors for the strings should not be able to return nil; return @”” instead
2.	In Interface Builder, select an instance of NSArrayController from the Cocoa-Controllers palette; drag it into the Instances pane of VTXDocument.nib and label it Lexicon Control; create a subclass of NSArrayController named VTXLexArrayController; create the source files and add them to the project; import the new class into the nib file; change the array controller instance’s class to VTXLexArrayController in IB
3.	Bind the controller to the data model by opening the Attributes pane of the Info window for VTXLexArrayController and changing the Object Class Name to VTXVocable
4.	In the same Attributes pane insert the keys that correspond to the relevant instance variables of the VTXVocable class: firstL, secondL, topic, and score
5.	Bind the array controller to the columns in the table view: select column labeled Some Language and open the value component in its Bindings inspector in the Info window; set the Bind to: field Lexicon Control; set the Controller Key: field to arrangedObjects, the method of VTXLexArrayController that will return its array of VTXVocable objects. Then, select firstL from the popup associated with the Model Key Path: field; perform a similar series of steps to bind the other table columns to their model key path variables
6.	To bind the Insert and Delete toolbar buttons to appropriate array controller methods, create the outlet lexiconController in VTXMainWindowController; declare it in the window controller in IB and connect it; for appropriate items in the Toolbar Delegate category, set the target to lexiconController and insert: or remove: action as appropriate
7.	Test the application to confirm that the data model is bound to the table view
I.	Implement Undo support for record insertion and deletion and for table cell editing
1.	Declare the following methods in the VTXMainWindowController header
- (void) insertObject: ( id ) v inVocablesAtIndex: ( int ) index;
- (void) removeObjectFromVocablesAtIndex: ( int ) index;
- (void) startObservingVocable: (VTXVocable * ) v;
- (void) stopObservingVocable: ( VTXVocable * ) v;
- (void) changeKeyPath: ( NSString * ) keyPath
ofObject: ( id ) obj toValue: ( id ) newValue
- (void) observeValueForKeyPath:(NSString*) kp ofObject:(id) obj
change: ( NSDictionary * ) change context: ( void * ) con
2.	Declare an NSMutableArray vocables ivar in VTXMainWindowController; declare and implement getter/setter accessors for it; create it in init and destroy it in dealloc
- ( void ) setVocables: ( NSMutableArray * ) array
{
NSEnumerator *e;
VTXVocable *v;
if ( array == vocables )	return;
e = [ vocables objectEnumerator ];
while ( v = [ e nextObject ] )
[ self stopObservingVocable: v ];
[ vocables release ];
vocables = [ array retain ];
e = [ vocables objectEnumerator ];
while ( v = [ e nextObject ] )
[ self startObservingVocable: v ];
}
3.	Implement KVO in VTXMainWindowController.m with these methods; add coverage for all object ivars, not just firstL as shown in the example below
- ( void ) startObservingVocable: ( VTXVocable * ) v
{
[ v addObserver: self forKeyPath: @"firstL"
options: NSKeyValueObservingOptionOld context: NULL ];
}
- ( void ) stopObservingVocable: ( VTXVocable * ) v
{
[ v removeObserver: self forKeyPath: @"firstL" ];
}
- ( void ) insertObject: (id) v inVocablesAtIndex: ( int ) index
{
// Add the inverse of this operation to the undo stack
NSUndoManager *undo = [ doc undoManager ];
[ [ undo prepareWithInvocationTarget: self ]
removeObjectFromVocablesAtIndex: index ];
if ( ![ undo isUndoing ] )
[ undo setActionName: @"Insert Vocable" ];
// Add the vocable to the array
[ self startObservingVocable: v ];
[ vocables insertObject: v atIndex: index ];
}
- ( void ) removeObjectFromVocablesAtIndex: ( int ) index
{
VTXVocable *v = [ vocables objectAtIndex: index ];
NSUndoManager *undo = [ doc undoManager ];
// Add the inverse of this operation to the undo stack
[ [ undo prepareWithInvocationTarget: self ] insertObject: v
inVocablesAtIndex: index ];
if ( ![ undo isUndoing ] )
[ undo setActionName: @"Delete Vocable" ];
// Remove the vocable from the array
[ self stopObservingVocable: v ];
[ vocables removeObjectAtIndex: index ];
}
- ( void ) changeKeyPath: ( NSString * ) keyPath
ofObject: ( id ) obj toValue: ( id ) newValue
{
// This method is its own inverse op for undo stack with edits
// setValue: forKeyPath: invokes KVO, and takes care of undoing
[ obj setValue: newValue forKeyPath: keyPath ];
}
- ( void ) observeValueForKeyPath: ( NSString * ) kp
ofObject: ( id ) obj
change: ( NSDictionary * ) change context: ( void * ) con
{
NSUndoManager *undo = [ doc undoManager ];
id oldValue = [ change objectForKey: NSKeyValueChangeOldKey ];
[ [ undo prepareWithInvocationTarget: self ]
changeKeyPath: kp ofObject: obj toValue: oldValue ];
[ undo setActionName: @"Edit" ];
}
4.	Manually bind the vocables array to the array controller in the awakeFromNib method:
[ lexiconController bind: @"contentArray"
toObject: self withKeyPath: @"lexiconArray" options: nil ];
Send a matching unbind message and set the array controller’s content to nil on closing; do NOT bind the array to the controller via the File’s Owner object in IB
J.	Create the VTXLexiconModel class to support archiving of a document’s Model data
1.	Create the class files and implement the designated initializer; include an accessor to return the document that creates this model object; this class should also conform to <NSCoding>
2.	Declare an NSArray lexiconArray in VTXLexiconModel; create accessors for it; create the instance in the init method of VTXLexiconModel and destroy it in dealloc; ; this ivar will store a copy of the records managed by the array controller when a document is saved; declare additional ivars to store supporting data for the document (e.g,, the labels for the table column header cells)
3.	Include an instance variable in VTXLexiconModel to save the document window’s frame, (an NSRect containing its origin and size); encode these data when archiving a document and decode them when loading a document
4.	Declare a VTXLexiconModel ivar in VTXDocument.h; declare and implement an accessor for this instance variable

5.	Instantiate a VTXLexiconModel object in the initialization of the document object and release it in the dealloc method
6.	Declare an ivar for a VTXLexiconModel object in VTXMainWindowController; create a reference to it via the window controller's document accessor in the init method and release it in the dealloc method
7.	Bind the model object data to the data entry text fields in the inspector panel: In the value section set each Model Key Path to the appropriate model ivars; set the Controller Key field to selection, a method that returns the currently-selected object in the array; test that the text fields for data entry respond for both inspection and editing
K.	Implement data archiving for the model objects
1.	To make VTXLexiconModel conform to the NSCoding protocol, implement the methods encodeWithCoder: and initWithCoder:
- ( void ) encodeWithCoder: ( NSCoder * ) encoder
{
[ encoder encodeObject: lexiconArray ];
[ encoder encodeObject: firstLangLabel ];
[ encoder encodeObject: secondLangLabel ];
[ encoder encodeObject: topicLabel ];
[ encoder encodeObject: frame ];
}
- ( id ) initWithCoder: ( NSCoder * ) decoder
{
if ( self = [ super init ] )
{
[ self setLexiconArray: [ decoder decodeObject ] ];
[ self setFirstLangLabel: [ decoder decodeObject ] ];
[ self setSecondLangLabel: [ decoder decodeObject ] ];
[ self setTopicLabel: [ decoder decodeObject ] ];
[ self setFrame: [ decoder decodeObject ] ];
}
return self;
}
2.	Implement the document methods to save and load the document and allow for multiple root objects in archive
- ( NSData * ) dataRepresentationOfType: ( NSString * ) type
{
// Get the arranged objects from the NSArrayController
VTXMainWindowController *winCtl =
[ [ self windowControllers ] objectAtIndex: 0 ];
[ lexiconModel setLexiconArray: [ winCtl lexiconArray ] ];
[ lexiconModel setFrame: [ [ winCtl window ] frame ] ];
if ([type isEqualToString: @"Vocable Trainer X Document"])
{
return [ NSArchiver archivedDataWithRootObject:
[ self lexiconModel ] ];
}
else
return nil;
}
- ( BOOL ) loadDataRepresentation: ( NSData * ) data
ofType: ( NSString * ) type
{
VTXLexiconModel *model = nil;
if ([type isEqualToString: @"Vocable Trainer X Document"])
{
model = [ NSUnarchiver unarchiveObjectWithData: data ];
if ( model != nil )
{
[ lexiconModel setFirstLangLabel:
[ model firstLangLabel ] ];
[ lexiconModel setSecondLangLabel:
[ model secondLangLabel ] ];
[ lexiconModel setTopicLabel: [ model topicLabel ] ];
[ lexiconModel setLexiconArray: [model lexiconArray] ];
[ lexiconModel setFrame: [ model frame ] ];
return YES;
}
else
return NO:
}
else
return NO;
}
3.	Disable Undo registration when loading data from a file: modify the section for updating the table view in the window controller's awakeFromNib; add a line to update the view when loading data from file
// Bypass Undo registration when loading data from a file
[ [ doc undoManager ] disableUndoRegistration ];
[ [ self lexiconController ] addObjects:
[ [ doc lexiconModel ] lexiconArray ] ];
[ [ doc undoManager ] enableUndoRegistration ];
[ lexiconTable deselectAll: self ];
4.	In order to position the window properly when restoring it from frame data loaded from a file, you need to turn off window cascading in the window controller, just before you set the frame of the window using the data from the file in awakeFromNib, as follows:
[ self setShouldCascadeWindows: NO ];
[ [ self window ] setFrame:
[ [ doc lexiconModel ] frame ] display: YES ];
5.	At this point you should be able to save and load data to files; required accessors include getter and setter for the VTXLexiconModel class in the document class, a document class getter in VTXLexiconModel, a getter and setter for lexiconArray in VTXLexiconModel, and getters for VTXLexiconModel, lexiconTable, and lexiconController in the window controller class (include the headers for these different classes as required in each class)
6.	Now that you are saving and restoring documents, add the icons for the application and document (supplied); these do not go in the localizable folders; in the Target Inspector, set the Document Extension and OS Types to vtxd
L.	Implement saving and loading of CSV data prepared for or in other programs
1.	Implement the saveAsCSV: action in VTXMainWindowController
- ( IBAction ) saveAsCSV: ( id ) sender
{
NSSavePanel *panel = [ NSSavePanel savePanel ];
[ panel setRequiredFileType: @"csv" ];
if ( [ panel runModal ] == NSOKButton )
[ [ self getCSVData ] writeToFile: [ panel filename ]
atomically: YES ];
}
- ( NSString * ) getCSVData
{
NSMutableString *csvString = [ NSMutableString string ];
NSEnumerator *vocablesEnumerator =
[[lexiconController arrangedObjects] objectEnumerator];
VTXVocable *v;
while ( v = [ vocablesEnumerator nextObject ] )
{
[ csvString appendString:
[NSString stringWithFormat: @"\"%@\",\"%@\",\"%@\"\n",
[ v firstL ],[ v secondL ], [ v topic ] ] ];
}
return [ NSString stringWithString: csvString ];
}
2.	Put a menu item in the File menu Export to CSV... and connect it to this action; disable this menu item in a validateMenuItem: method if the table contains no entries
if ( [ theItem action ] == @selector( saveCSV: ) )
return [ [ lexiconController arrangedObjects ] count ] > 0;
In an identical manner, also disable the toolbar item that triggers this action when the table is empty.
3.	Put a menu item in the File menu Import from CSV... and connect it to the following action implemented in VTXMainWindowController
- ( IBAction ) importFromCSV: ( id ) sender
{
NSMutableString *line;
NSString *lineBuffer;
NSString *rawString;
NSArray *lines;
NSArray *arr;
NSEnumerator *lineEnumerator;
VTXVocable *v;
NSOpenPanel *panel = [ NSOpenPanel openPanel ];
if ( [ panel runModalForTypes: nil ] == NSOKButton )
rawString = [ NSString stringWithContentsOfFile:
[ [ panel filenames ] objectAtIndex: 0 ] ];
else
return;
lines = [ rawString componentsSeparatedByString: @"\n" ];
lineEnumerator = [ lines objectEnumerator ];

while ( lineBuffer = [ lineEnumerator nextObject ] )
{
line = [ NSMutableString stringWithString: lineBuffer ];
[ line replaceOccurrencesOfString:
@"\"" withString: @"" options: NSLiteralSearch
range: NSMakeRange( 0, [ line length ] ) ];
arr = [ line componentsSeparatedByString: @"," ];
if ( [ arr count ] < 3 )
continue;
else
{
v = [ [ VTXVocable alloc ]
initWithFirstLanguage: [ arr objectAtIndex: 0 ]
secondLanguage: [ arr objectAtIndex: 1 ]
topic: [ arr objectAtIndex: 2 ] ];
[ lexiconController addObject: [ v autorelease ] ];
}
}
[ doc updateChangeCount: NSChangeDone ];
[ lexiconTable deselectAll: self ];
}
M.	Subclass NSArrayController and name the class VTXLexArrayController
1.	Give the class an outlet to VTXMainWindowController and connect it in IB; declare an NSString instance variable named searchString (characters to be retrieved from the search field); declare accessors for the search string; disconnect the search field delegate outlet from the window controller and reconnect it to the array controller
2.	Initialize the search string to an empty string in awakeFromNib and release it in dealloc.
3.	Implement a method controlTextDidChange: to intercept keystrokes in the search field and from cells in the table view
- ( void ) controlTextDidChange: ( NSNotification * ) note
{
if ( [ [note object] isKindOfClass: [NSSearchField class] ] )
{
[ self setSearchString:
[ [ [ note object ] stringValue ] lowercaseString ] ];
[ self rearrangeObjects ];
}
if ( [ [ note object ] isKindOfClass: [ NSTableView class ] ] )
{
NSLog( @”Edited table view cell” );
}
}
4.	In VTXMainWindowController, delete the body of controlTextDidChange: and also override the preservesSelection: method of NSArrayController to return NO or else set this property in the Attributes pane for the array controller in IB
5.	Import the source files into the project and add references to VTXLexArrayController at appropriate locations (custom class in IB, and return types, arguments or declarations in the sources); you will now have to import its header in any file that uses this class
6.	Override the arrangeObjects: method in VTXLexArrayController to use the search string to filter the controller's array (and the table view will automatically update)
- ( NSArray * ) arrangeObjects: ( NSArray * ) lexicon
{
VTXVocable *vocable;
NSEnumerator *ve = [ lexicon objectEnumerator ];
NSMutableArray *matchedObjects =
[ NSMutableArray arrayWithCapacity: [ lexicon count ] ];
NSString *lowerSearch = [ searchString lowercaseString ];
if ( [ lowerSearch length ] == 0 )	// case insensitive search
{
return [ super arrangeObjects: lexicon ];
}
else
{
while ( vocable = [ oEnum nextObject ] )
{
NSString *lowerFirstL =
[ [ vocable firstL ] lowercaseString ];
NSString *lowerSecondL= [ [ vocable secondL ]
LowercaseString ];
if ( [ lowerFirstL rangeOfString:
lowerSearch ].location != NSNotFound
|| [ lowerSecondL rangeOfString:
lowerSearch ].location != NSNotFound )
{
[ matchedObjects addObject: item ];
}
}
return [ super arrangeObjects: matchedObjects ];
}
}
7.	Override the insert: and remove: methods to call the super's corresponding method
- ( void ) insert: ( id ) sender
{
[ super insert: sender ];
}
- ( void ) remove: ( id ) sender
{
[ super remove: sender ];
}
N.	Implement validation methods for toolbar and menu items to disable table insertion and deletion, and simple saving of files when the search field is active
1.	Validate toolbar items for adding and deleting records in VTXLexArrayController
- ( BOOL ) validateToolbarItem: ( NSToolbarItem * ) theItem
{
// Disable Add & Delete buttons during active filtering
// Modification of arrangedObjects would corrupt the array
if ( [ theItem action ] == @selector( insert: ) )
return ( [ searchString length ] == 0 );
// Also disable the Delete button if no items are selected
if ( [ theItem action ] == @selector( remove: ) )
return ( [ [ self selectedObjects ] count ] > 0
&& [ searchString length ] == 0 );
else
return true;
}
2.	Use validateMenuItem: in VTXDocument to disable the document's Save and Revert actions when the search field is active, since the filtered array would overwrite the file's data without asking the user to confirm the replacement
- ( BOOL ) validateMenuItem: ( NSMenuItem * ) theItem
{
NSSearchField *searchf = [ [ [ self windowControllers ] objectAtIndex: 0 ] toolbarSearchField ];
if ( [ theItem action ] == @selector( saveDocument: ) )
return [ [ searchf stringValue ] length ] == 0;
if ([theItem action] == @selector(revertDocumentToSaved:))
return [ [ searchf stringValue ] length ] == 0;
else
return true;
}
Implement the same method in VTXMainWindowController to disable importing data from a text file if the search field is active by using the following test:
if ( [ theItem action ] == @selector( import: ) )
return ( [ [ searchf stringValue ] length ] == 0 );
O.	Implement a selection status text field in the space between the table and the toolbar
1.	Make an outlet to the text field in the window controller. Set VTXMainWindowController as the delegate of lexiconTable; implement the following method and update the string in awakeFromNib and also in delegate method tableViewSelectionDidChange: which you should implement in VTXMainWindowController
- ( void ) updateSelectionStatus
{
int s, a;
s = [ lexiconTable numberOfSelectedRows ];
a = [ lexiconTable numberOfRows ];
[ selectionStatus setStringValue:
[NSString stringWithFormat: @"%d of %d selected", s,a] ];
}
You will also need to invoke this method whenever a row is added to or deleted from the table.
2.	Make sure the text field autosizes correctly. To display a correct selection status message when the table view is filtered, update it in VTXMainWindowController at the end of the controlTextDidChange: method in VTXLexArrayController.
3.	Add a VTXAppController class to customize the application's startup behavior, with the following method:
// Application should not open an empty document on launch
- ( BOOL ) applicationShouldOpenUntitledFile:
( NSApplication * ) theApplication
{
return NO;
}
4.	Instantiate VTXAppController in MainMenu.nib and make it the delegate of File’s Owner.
P.	Implement the VTXPasteboardCategory on VTXMainWindowController to permit cutting and pasting of table rows within and between tables
1.	Implement the copy: action
- ( IBAction ) copy: ( id ) sender
{
NSPasteboard *pb = [ NSPasteboard generalPasteboard ];
[ self writeStringToPboard: pb ];
}
- ( void ) writeStringToPboard: ( NSPasteboard * ) pb
{
if ( [ lexiconTable numberOfSelectedRows ] < 1 )
{
NSBeep( );
}
// Declare types
[ pb declareTypes: [ NSArray arrayWithObjects:
NSStringPboardType, NSTabularTextPboardType,
@"VTXPboardType", nil ] owner: self ];
// Copy data to the pasteboard
[ pb setString: [ self prepareNSStringForPboard:
[ lexiconController selectedObjects ] ]
forType: NSStringPboardType ];
[ pb setString: [ self prepareNSStringForPboard:
[ lexiconController selectedObjects ] ]
forType: NSTabularTextPboardType ];
[ pb setString: [ self prepareVTXPboardType:
[ lexiconController selectedObjects ] ]
forType: @"VTXPboardType" ];
}
// This type is used for pasting text to other programs
- (NSString *) prepareNSStringForPboard: (NSArray *) vocables
{
NSEnumerator *e = [ vocables objectEnumerator ];
NSMutableString *result = [ NSMutableString string ];
VTXVocable *vocable;
while ( vocable = [ e nextObject ] )
{
[ result appendString: [ NSString stringWithFormat:
@"%@\t%@\t%@", [ vocable firstL ],
[ vocable secondL ], [ vocable topic ] ] ];
[ result appendString: [ NSString stringWithFormat:
@"%c", NSCarriageReturnCharacter ] ];
}
return result;
}
// Custom pasteboard format for copying inside VTX
// Preserves the vocable scores data
- ( NSString * ) prepareVTXPboardType: ( NSArray * ) vocables
{
NSEnumerator *e = [ vocables objectEnumerator ];
NSMutableString *result = [ NSMutableString string ];
VTXVocable *vocable;
while ( vocable = [ e nextObject ] )
{
[ result appendString: [ NSString stringWithFormat:
@"%@\t%@\t%@\t%d\t%d", [ vocable firstL ],
[ vocable secondL ], [ vocable topic ],
[ vocable numCorrect ], [ vocable numGuesses ] ] ];
[ result appendString: [ NSString stringWithFormat:
@"%c", NSCarriageReturnCharacter ] ];   
}
return result;
}
2.	Implement the cut: action
- ( IBAction ) cut: ( id ) sender
{
NSArray *selection = [ lexiconController selectedObjects ];
NSEnumerator *se = [ selection objectEnumerator ];
DDVLexItem *item;
[ self copy: sender ];
while ( item = [ se nextObject ] )
{
[ [ doc undoManager ] disableUndoRegistration ];
[ lexiconController removeObject: item ];
[ [ doc  undoManager ] enableUndoRegistration ];
}
[ lexiconTable reloadData ];
[ lexiconTable deselectAll: self ];
[ [ self document ] updateChangeCount: NSChangeDone ];
}
3.	Implement paste: action; pasteboard contents are implemented as strings that are parsed back to VTXVocable objects
- ( IBAction ) paste: ( id ) sender
{
NSPasteboard *pb = [ NSPasteboard generalPasteboard ];
if ( ![ self readStringFromPasteboard: pb ] )
{
NSBeep( );
}
}
- ( BOOL ) readStringFromPasteboard: ( NSPasteboard * ) pb
{
NSArray *types = [ pb types ];
NSMutableArray *result;
NSEnumerator *e;
VTXVocable *vocable;
int selectedRowIndex = [ lexiconTable selectedRow ];
if ( [ types containsObject: @"VTXPboardType" ] )
{
result = [ self readVocableStringFromPboard: pb ];
}
else if ( [ types containsObject: NSStringPboardType ] )
{
result = [ self readNSStringFromPboard: pb ];
}
else
{ 
return NO; 
}
if ( selectedRowIndex == -1 )
{
[ [ doc undoManager ] disableUndoRegistration ];
[ lexiconController addObjects: result ];
[ [ doc undoManager ] enableUndoRegistration ];
}
else
{
// Insert the vocables at the selected row
e = [ result reverseObjectEnumerator ];
while ( vocable = [ e nextObject ] )
{
[ [ doc undoManager ] disableUndoRegistration ];
[ lexiconController insertObject: vocable
atArrangedObjectIndex: selectedRowIndex ];
[ [ doc undoManager ] enableUndoRegistration ];
}
}
return YES;
}
- (NSMutableArray *) readNSStringFromPboard:(NSPasteboard *) pb
{
NSString *value;	NSArray *lines;
NSEnumerator *linesEnumerator;
NSString *line;	NSArray *words;	VTXVocable *newVocable;
NSMutableArray *result = [ NSMutableArray array ];
value = [ pb stringForType: NSStringPboardType ];
lines = [ value componentsSeparatedByString:
[ NSString stringWithFormat:
@"%c", NSCarriageReturnCharacter ] ];
linesEnumerator = [ lines objectEnumerator ];
while ( line = [ linesEnumerator nextObject ] )
{
words = [ line componentsSeparatedByString: @"\t" ];
if ( [ words count ] >= 2 )
{
newVocable = [ [ VTXVocable alloc ]
initWithFirstLanguage: [ words objectAtIndex: 0 ]
secondLanguage: [ words objectAtIndex: 1 ]
topic: @"None" ];
if ([words count]>2 && [words objectAtIndex: 2]!=@"")
[ newVocable setTopic: [ words objectAtIndex: 2 ] ];
[ result addObject: [ newVocable autorelease ] ];
}
}
return result;
}
- (NSMutableArray *) readVocableStringFromPboard:
(NSPasteboard *) pb
{
NSString *value;	NSArray *lines;
NSEnumerator *linesEnumerator;
NSString *line;	NSArray *words;	VTXVocable *newVocable;
NSMutableArray *result = [ NSMutableArray array ];
value = [ pb stringForType: @"VTXPboardType" ];
lines = [ value componentsSeparatedByString:
[ NSString stringWithFormat: @"%c",
NSCarriageReturnCharacter ] ];
linesEnumerator = [ lines objectEnumerator ];
while ( line = [ linesEnumerator nextObject ] )
{
words = [ line componentsSeparatedByString: @"\t" ];
if ( [ words count ] == 5 )
{
newVocable = [ [ VTXVocable alloc ] init ];
[ newVocable setFirstL: [ words objectAtIndex: 0 ] ];
[ newVocable setSecondL: [ words objectAtIndex: 1 ] ];
[ newVocable setTopic: [ words objectAtIndex: 2 ] ];
[ newVocable setNumCorrect:
[ [ words objectAtIndex: 3 ] intValue ] ];
[ newVocable setNumGuesses:
[ [ words objectAtIndex: 4 ] intValue ] ];
[ result addObject: [ newVocable autorelease ] ];
}
}
return result;
}
4.	Implement a VTXSearchFieldFormatter subclass of NSFormatter to format the pasteboard contents in case the user inadvertently tries to paste table row strings into the search field. Override the following two methods:
- ( BOOL ) getObjectValue: ( id * ) obj forString:
( NSString * ) string errorDescription: ( NSString ** ) e
{
*obj = string;
if ( string != NULL )
return YES;
else
{
if ( e != NULL )
*e = @"Error in search field formatter";
return NO;
}
}
- ( NSString * ) stringForObjectValue: ( id ) obj
{
if ( [ obj isKindOfClass: [ NSString class ] ] )
{
NSMutableString *s =
[ NSMutableString stringWithString: obj ];
[ s replaceOccurrencesOfString: @"\r" withString: @" "
options: NSCaseInsensitiveSearch
range: NSMakeRange( 0, [ s length ] ) ];
return [ [ s copy ] autorelease ];
}
else
return [ obj description ];
}
5.	In the window controller’s awakeFromNib method, send these messages as well:
[ [ toolbarSearchField cell ] setWraps: NO ];
[ [ toolbarSearchField cell ] setScrollable: YES ];
VTXSearchFieldFormatter *f =
[ [ VTXSearchFieldFormatter alloc ] init ];
[ toolbarSearchField setFormatter: f ];
[ f release ];	// Formatter is retained by the search field
Q.	Create a Document Settings sheet so that the user can customize table header cells and other document options
1.	Add the Document Settings panel to VTXDocument.nib; make a settingsPanel outlet in VTXMainWindowController; add three labeled text fields to the panel and a button to dismiss the panel; declare the outlet in VTXMainWindowController.h
2.	Add a Document Settings menu item to the Window menu in MainMenu.nib; add a new action to the First Responder object in IB (implement openDocumentSettings: and dismissDocumentSettings: in VTXMainWindowController
3.	When the user creates a new document, immediately open the language settings panel; add the following action to the nib file and implement it in VTXAppController.m: connect the File —> New menu item to the following openNewDocument: action
- ( IBAction ) openNewDocument: ( id ) sender
{
NSDocumentController *dc =
[ NSDocumentController sharedDocumentController ];
[ dc newDocument: self ];
[ [ [ [ dc currentDocument ] windowControllers ]
objectAtIndex: 0 ] openSettingsPanel: self ];
}
4.	Add outlets for the editable text field outlets in the document settings sheet and getter accessors for them; add outlets to the customizable table columns in the window controller and connect them in IB (first language, second language, and topic). Add outlets to the static text labels associated with the editable text fields in the inspector panel and connect them
3.	Implement the updateLabelStrings method in VTXMainWindowController; you may wish to include additional code that sets the document changed status if the strings differ between the table view header cells and the document settings sheet
- ( void ) updateLabelStrings
{
[ [ firstLColumn headerCell ] setStringValue:
[ firstLSetting stringValue ] ];
[ [ secondLColumn headerCell ] setStringValue:
[ secondLSetting stringValue ] ];
[ [ topicColumn headerCell ] setStringValue:
[ topicSetting stringValue ] ];
[ firstLangLabel setStringValue:
[ firstLSetting stringValue ] ];
[ secondLangLabel setStringValue:
[ secondLSetting stringValue ] ];
[ topicLabel setStringValue:
[ topicSetting stringValue ] ];
}
4.	Invoke updateLabelStrings in dismissSettingsPanel and in awakeFromNib in VTXMainWindowController; flag the document changed status if strings in settings sheet differ from current values; when restoring a document from disk, fill them with data from VTXLexiconModel in awakeFromNib to refresh the views properly
5.	Create instance variables and accessors for these three strings in the VTXLexiconModel class; update the strings in the interface when loading data from an archive, in the awakeFromNib method
6.	When archiving data in the document, update the VTXLexiconModel class to write the current strings before archiving it, in dataRepresentationOfType:, as follows:
[ lexiconModel setFirstLangLabel:
[ [ myWindowController firstLSetting ] stringValue ] ];
[ lexiconModel setSecondLangLabel:
[ [ myWindowController secondLSetting ] stringValue ] ];
[ lexiconModel setTopicLabel:
[ [ myWindowController topicSetting ] stringValue ] ];
R.	Implement a simple capability for printing lexicon lists
1.	Override printDocument: in VTXDocument.m
- ( void ) printDocument: ( id ) sender
{
[ [ [ self windowControllers ] objectAtIndex: 0 ]
printShowingPrintPanel: TRUE ];
}
2.	In VTXMainWindowController, implement printShowingPanel:
- ( void ) printShowingPrintPanel: ( BOOL ) showPanels
{
NSPrintOperation *op;
// Obtain a custom view that will be printed
NSMutableString *title =
[ NSMutableString stringWithString: [doc displayName] ];
NSView *printView = [ [ VTXPrintView alloc]
initWithVocables: [ lexiconController arrangedObjects ]
andTitle: title printInfo: [ doc printInfo ] ];
// Construct the print operation and setup Print panel
op = [ NSPrintOperation printOperationWithView:
printView printInfo: [ doc printInfo ] ];
[ op setShowPanels: showPanels ];
// Run operation, showing Print panel if showPanels was YES
//[ myDocument runModalPrintOperation: op delegate: nil
// didRunSelector: NULL contextInfo: NULL ];
[ op runOperation ];
[ printView release ];
}
3.	Implement VTXPrintView, a subview of NSView, and add the files to the project; the work is done in drawRect:, as usual
S.	Implement the Quiz Window interface
1.	Create QuizWindow.nib with an NSPanel (closeable but not resizeable or minimizing), two labeled editable text fields and a button labeled Skip this one; set the custom class for File's Owner of this nib to VTXQuizController; set the window outlet for File's Owner to the NSPanel
2.	Add MMRandomEnumerator class source files to project; add VTXQuizController class source files to project; create and connect outlets for the quiz window text fields and labels and a static text field to the quiz window for the quiz answer feedback
3.	Add ivars to VTXQuizController to hold an NSNumber, an NSArray (quizVocables), an individual VTXVocable object, an MMRandomEnumerator object, two NSColor objects (rightColor, wrongColor), and a BOOL (isCorrect)
4.	Implement the designated initializer for VTXQuizController and cleanup when the quiz controller object is destroyed; implement a setter accessor for quizVocables
- ( id ) initWithWindowNibName: ( NSString * ) windowNibName
quizVocables: ( NSArray * ) theVocables
{
self = [ super initWithWindowNibName: windowNibName ];
if ( self )
{
quizVocables = [ [ NSArray alloc ] init ];
[ self setQuizVocables: theVocables ];
}
rightColor = [ [ NSColor colorWithCalibratedRed: 0.0
green: 0.6 blue: 0.0 alpha: 1.0 ] retain ];
wrongColor = [ [ NSColor colorWithCalibratedRed: 0.6
green: 0.0 blue: 0.0 alpha: 1.0 ] retain ];
return self;
}
- ( void ) dealloc
{
[ rightColor release ];
[ wrongColor release ];
[ quizVocables release ];
[ super dealloc ];
}
6.	Set up the quiz after the quiz window has loaded (draw labels for the labeled text fields from the document that the quiz belongs to); randomize the array of vocables that will be passed from the document; display the first item in the quiz sequence
- ( void ) windowDidLoad
{
int index;
NSNumber *num;
NSMutableArray *seq = [ NSMutableArray array ];
srandom( time( NULL ) ); // Seed the random number generator
// Set text field labels to match the document’s labels
[ firstLangLabel setStringValue:
[ [ [ self document ] lexiconModel ] firstLangLabel ] ];
[ secondLangLabel setStringValue:
[ [ [ self document ] lexiconModel ] secondLangLabel ] ];
[ promptField setFont: [ NSFont userFontOfSize: 24 ] ];
[ solutionField setFont: [ NSFont userFontOfSize: 24 ] ];
// Create a sequence from zero to the number of vocables
for ( index = 0; index < [ quizVocables count ]; index++ )
{
num = [ NSNumber numberWithInt: index ];
[ seq addObject: num ];
}
// Create an enumerator for the random sequence
// Release the enumerator in windowShouldClose:
// For picking items at random from the array of vocables
quizEnum = [[MMRandomEnumerator initWithArray: seq] retain];
[ self showNextVocable ];
}
7.	Implement the method for color-coding the quiz answer feedback string
- (NSMutableAttributedString *) feedbackString: (NSString *) s
{
NSMutableAttributedString *fb;
fb = [[NSMutableAttributedString alloc] initWithString: s];
[ fb addAttribute: NSFontAttributeName
value: [ NSFont userFontOfSize: 24 ]
range: NSMakeRange( 0, [ s length ] ) ];
if ( isCorrect )
[ fb addAttribute:
NSForegroundColorAttributeName value: rightColor
range: NSMakeRange( 0, [ s length ] ) ];
else
[ fb addAttribute:
NSForegroundColorAttributeName value: wrongColor
range: NSMakeRange( 0, [ s length ] ) ];
return [ fb autorelease ];
}
8.	Declare an outlet to the Skip this one button in the source files and IB; disable the Skip this one button at the same time as the solution field is disabled for input if the last quiz item is answered
T.	Run the quiz when the user presses the appropriate toolbar button
1.	Implement a validateToolbarItem: method in VTXMainWindowController to disable Start Quiz button if table is empty
- ( BOOL ) validateToolbarItem: ( NSToolbarItem * ) theItem
{
if ( [ theItem action ] == @selector( startQuiz: ) )
return [[lexiconController arrangedObjects] count] > 0;
else
return true;
}
2.	Add an ivar to VTXMainWindowController.h that points to a VTXQuizController object (a forward @class declaration is necessary); name the ivar quizCtl
3.	Implement the startQuiz: action in VTXMainWindowController which will create a new VTXQuizController and display the Quiz Window
- ( IBAction ) startQuiz: ( id ) sender
{
NSArray *arr = [ lexiconController arrangedObjects ];
NSArray *sel = [ lexiconController selectedObjects ];
if ( [ sel count ] == 0 )
quizCtl = [ [ VTXQuizController alloc ]
initWithWindowNibName: @"QuizWindow" quizVocables: arr ];
else
quizCtl = [ [ VTXQuizController alloc ]
initWithWindowNibName: @"QuizWindow" quizVocables: sel ];
[ doc addWindowController: quizCtl ];
[ quizCtl showWindow: self ];	[ quizCtl autorelease ];
[ [ self window ] orderOut: self ];
}
4.	Implement the showNextVocable: action in VTXQuizController to display a random sequence of quiz items (ranseq is declared as an NSNumber in the header); vocables are omitted from the test for certain values of the success rate and number of trials:
- ( void ) showNextVocable
{
if ( ranseq = [ quizEnum nextObject ] )
{
v=[[quizVocables objectAtIndex:[ranseq intValue]]retain];
if ([[v score] floatValue] > 0.8 || [v numGuesses] < 25)
{
[ promptField setStringValue: [ v firstL ] ];
[ solutionField setStringValue: @"" ];
}
else
{
[ v release ];	[ self showNextVocable ];
}
}
else
{
[ solutionField setEnabled: FALSE ];
[ skipButton setEnabled: FALSE ];
}
}
5.	Implement the solve: method (class declares a BOOL variable that is set TRUE here, and was initialized as FALSE in the init method)
- ( IBAction ) solve: ( id ) sender
{
attemptedAnswer = YES;
// A somewhat-forgiving caseInsensitveCompare:
if ( [ [ v secondL ] caseInsensitiveCompare:
[ solutionField stringValue ] ] == NSOrderedSame )
{
[ v setNumGuesses: [ v numGuesses ] + 1 ];
[ v setNumCorrect: [ v numCorrect ] + 1 ];
isCorrect = YES;
}
else
{
[ v setNumGuesses: [ v numGuesses ] + 1 ];
isCorrect = NO;
}
[ feedback setAttributedStringValue:
[ self feedbackString: [ v secondL ] ] ];
[ NSTimer scheduledTimerWithTimeInterval: 2 target: self
selector: @selector( showNextVocable ) userInfo: nil repeats: NO ];
[ v release ];	// This was retained in showNextItem
}
6.	Implement the skip: action for when the user presses the Skip this one button
- ( IBAction ) skip: ( id ) sender
{
[ self showNextVocable ];
[ feedback setStringValue: @"" ];
}
7.	Implement the windowShouldClose: delegate method in VTXQuizController to run when the user dismisses the quiz window
- ( BOOL ) windowShouldClose: ( id ) sender
{
[ [ [ [ self document ] windowControllers ]
objectAtIndex: 0 ] endQuizWithFlag: attemptedAnswer ];
// Put the main document window back in front
[ [ [ [ [ self document ] windowControllers ]
objectAtIndex: 0 ] window ] orderFront: self ];
[ quizEnum release ];	// Don’t forget to release this
return TRUE;
}
8.	Implement the endQuizWithFlag: method in VTXMainWindowController
- ( void ) endQuizWithFlag: ( BOOL ) answersAttempted;
{
if ( answersAttempted )
[ doc updateChangeCount: NSChangeDone ];
}
9.	For a few debugging notes regarding the presentation of the quiz, see Appendix 4.
U.	Enable addition and deletion of vocable items via menu or via keyboard shortcuts, and resetting of score values or switching data between languages for selected vocables
1.	Implement the following actions in VTXMainWindowController and add these actions to the list of actions in First Responder in MainMenu.nib
- ( IBAction ) insertVocableViaMenu: ( id ) sender
{
[ lexiconController insert: self ];
}
- ( IBAction ) deleteVocableViaMenu: ( id ) sender
{
[ lexiconController remove: self ];
}
2.	In the File menu of the program menus in MainMenu.nib, add items Insert Vocable and Delete Vocable; connect these items to appropriate First Responder actions
3.	Disable the Delete Vocable item if nothing is selected in the table and both of the new menu items if the search field is active, using the validateMenuItem: method
- ( BOOL ) validateMenuItem: ( NSMenuItem * ) theItem
{
if ( [theItem action] == @selector(insertVocableViaMenu:) )
return [toolbarSearchField stringValue] length == 0;
if ( [theItem action] == @selector(deleteVocableViaMenu:) )
return ( [[lexiconController selectedObjects] count] > 0
&& [toolbarSearchField stringValue] length == 0 );
else
return true;
}
4.	Enable resetting of scores values for listed vocables by adding the following action to VTXMainWindowController
- ( IBAction ) resetScores: ( id ) sender
{
NSMutableArray *vocables =
[ lexiconController arrangedObjects ];
NSEnumerator *vocablesEnumerator;
VTXVocable *vocable;
vocablesEnumerator = [ vocables objectEnumerator ];
while ( vocable = [ vocablesEnumerator nextObject ] )
{
[ vocable setNumGuesses: 0 ];
[ vocable setNumCorrect: 0 ];
}
[ lexiconTable reloadData ];
[ doc updateChangeCount: NSChangeDone ];
}
5.	Add a Lexicon menu to the main menu in MainMenu.nib; add a Reset Scores item to it and connect the new item to the resetScores: action by adding it to the list of actions in First Responder; disable the Reset Scores menu item if the table is empty by adding the following test to validateMenuItem: in VTXMainWindowController
if ( [ theItem action ] == @selector( resetScores: ) )
return [ [ lexiconController arrangedObjects ] count ] > 0;
6.	Implement the following action in VTXMainWindowController; add an appropriate item to the Lexicon menu and add the action through First Responder in MainMenu.nib
- ( IBAction ) swapLanguages: ( id ) sender
{
VTXVocable *vocable;
NSMutableArray *vocables =
[ lexiconController selectedObjects ];
NSEnumerator *vocablesEnumerator =
[ vocables objectEnumerator ];
while ( vocable = [ vocablesEnumerator nextObject ] )
{
[ vocable swapLanguages ];
}
[ doc updateChangeCount: NSChangeDone ];    
}
Declare and implement the swapLanguages method in class VTXVocable
- ( void ) swapLanguages
{
NSString *temp = [ NSString stringWithString: firstL ];
[ self setFirstL: secondL ];
[ self setSecondL: temp ];
}
Disable the Swap Languages menu item if no rows are selected in the table by adding the following test to validateManuItem: in VTXMainWindowController
if ( [ theItem action ] == @selector( swapLanguages: ) )
return [ [ lexiconController selectedObjects ] count ] > 0;
7.	Create a Categorize panel in VTXDocument.nib with a text field labeled Change Category of Selected Vocables To:, and a button to close the panel; include a Cancel button that will dismiss the panel without making any changes
Implement a categorizeVocables: action to open the panel as a modal sheet; data from text field in sheet will set topic data for selected vocables; implement a button to dismiss the sheet; add a Categorize item to the Lexicon menu; connect it to the action
- ( IBAction ) categorizeVocables: ( id ) sender
{
[ self openCategorizeVocablesSheet ];
}
- ( void ) openCategorizeVocablesSheet
{
[ NSApp beginSheet: categorizePanel
modalForWindow: [ self window ] modalDelegate: nil
didEndSelector: nil contextInfo: nil ];
[ NSApp runModalForWindow: categorizePanel ];
[ NSApp endSheet: categorizePanel ];
[ categorizePanel orderOut: self ];
}
- ( IBAction ) dismissCategorizePanel: ( id ) sender
{
NSArray *vocableSelection =
[ lexiconController selectedObjects ];
NSEnumerator *e = [ vocableSelection objectEnumerator ];
VTXVocable *v;
while ( v = [ e nextObject ] )
[ v setTopic: [ categoryField stringValue ] ];
[ [ self document ] updateChangeCount: NSChangeDone ];
[ NSApp stopModal ];
}
- ( IBAction ) cancelCategorizeAction: ( id ) sender
{
[ NSApp stopModal ];
}
Disable the Categorize menu item in validateMenuItem if no rows are selected
V.	Implement graphical display cells for the Score table column
1.	Add the BezierPathCell class files to the project
2.	Modify the VTXVocable class score accessor to return the value of the score ivar as a string representation of the percentage score, and to return -1 if no scores have been recorded
- ( NSString * ) score
{
float pct;
if ( numGuesses != 0 )
pct = ( float ) numCorrect / ( float ) numGuesses;
else
pct = -1.0;
// Return the score as pct, guesses, and number correct
return [ NSString stringWithFormat:
@"%4.2f %d/%d", pct, numCorrect, numGuesses ];
// Return the score only as percentage correct
// return [ NSString stringWithFormat: @"%4.2f", pct ];
}
3.	The table view can set the data cell object for the appropriate column with this line in the awakeFromNib method of VTXMainWindowController:
[ [ lexiconTable tableColumnWithIdentifier: @"score" ]
setDataCell: [ [ [ BezierPathCell alloc ] init ]
autorelease ] ];
You must explicitly create an identifier for the Score table column in VTXDocument.nib, or do it programmatically
4.	Implement Insert Item and Delete Item in the Lexicon menu; the connected actions in the window controller will simply invoke the insert: and remove: methods of the array controller; then connect these actions to the editable text fields in the inspector panel so they send the appropriate actions (have the action sent not on End Editing, but on Return only); this lets the user efficiently add items to the table by tabbing from field to field
5.	Another way to implement graphical scores display is to add a new column to the table and a new instance variable, image, of type NSImage to the VTXVocable class, along with its appropriate accessors. Add a new binding to VTXLexiconController for this key and bind the new table column to it. Replace the existing code that sets the data cell for this column in the window controller’s awakeFromNib method with the following:
[ [ lexiconTable tableColumnWithIdentifier: @"image" ]
setDataCell: [ [ [ NSImageCell alloc ] init ]
autorelease ] ];
Import a set of images to the project and use them in VTXVocable in the score method in the following way:
if ( pct > 0.8 )
[ self setImage: [ NSImage imageNamed: @"violet.png" ] ];
else if ( pct > 0.6 )
[ self setImage: [ NSImage imageNamed: @"green.png" ] ];
else if ( pct > 0.4 )
[ self setImage: [ NSImage imageNamed: @"yellow.png" ] ];
else if ( pct > 0.2 )
[ self setImage: [ NSImage imageNamed: @"red.png" ] ];
else if ( pct >= 0.0 )
[ self setImage: [ NSImage imageNamed: @"black.png" ] ];
else if ( pct == -1.0 )
[ self setImage: [ NSImage imageNamed: @"white.png" ] ];
W.	In the main window’s drawer, implement a topic listing table for the table
1.	In VTXDocument.nib, add a table view containing one (noneditable) column to the drawer content view and add a new NSArrayController instance to the instances pane; declare an outlet to the table view (topicTable) and to the array controller (topicController) in VTXMainWindowController and connect them to the instances in the nib file
2.	Subclass the new NSArrayController as VTXTopicController; create files for the new class and implement the following method:
- ( BOOL ) selectsInsertedObjects
{
return NO;
}
3.	Create VTXTopic, a subclass of NSObject, with a single NSString ivar topicString, and with initializers, and at least a getter accessor, and add it to the project
4.	Set up the bindings for the array controller: Bind the attributes of the array controller to VTXTopic and bind the new table column to the topicString key in VTXTopic
5.	Implement a method to update the content array of the new array controller with a list of unique topic strings from the list of topics in the main table view; send this message to the window controller in its awakeFromNib method; make sure that if any topic strings are empty, an entry is created to represent them so user can search for them specifically
- ( void ) updateTopicsList
{
VTXVocable *vocable;
NSString *aTopic;
NSEnumerator *e;
NSMutableArray *uniqueTopics = [ NSMutableArray array ];
[ topicController removeObjects:
[ topicController arrangedObjects ] ];
e = [ lexiconArray objectEnumerator];
// find unique topics and add them to a temporary array
while ( vocable = [ e nextObject ] )
{
if ( ![ uniqueTopics containsObject: [ vocable topic ] ]
&& !( [ [ vocable topic ] isEqualToString: @"" ] ) )
[ uniqueTopics addObject: [ vocable topic ] ];
if ( ![ uniqueTopics containsObject: @”<Empty” ]
&& ( [ [ vocable topic ] isEqualToString: @"" ] ) )
[ uniqueTopics addObject: @”<Empty” ];
}
e = [ uniqueTopics objectEnumerator ];
while ( aTopic = [ e nextObject ] )
[ topicController addObject: [ [ [ VTXTopic alloc ]
initWithString: aTopic ] autorelease ] ];
[ topicTable deselectAll: self ];
}
6.	Implement controlTextDidEndEditing in VTXMainWindowController to send the updateTopicsList message to VTXMainWindowController; connect delegate outlet of topic text field for the inspector window to VTXMainWindowController so that the topics list is updated after all table cell edits and after editing in the topic inspector
7.	Send the updateTopicsList message to VTXMainWindowController in the following methods:
•	at the end of awakeFromNib and importFromCSV in the window controller
•	in the dismissCategorizePanel method
•	in controlTextDidEndEditing:
•	in the insert: and remove: methods of VTXLexArrayController
•	insertObject: inVocablesAtIndex: in VTXMainWindowController
•	removeObjectFromVocablesAtIndex: in VTXMainWindowController
•	changeKeyPath: ofObject: toValue: in VTXMainWindowController
8.	Deselect all items in topicTable at appropriate places in code, such as when the drawer closes, and by adding the following method to VTXMainWindowController; don’t forget to connect the topicsTable delegate outlet to the window controller
- ( void ) tableView: ( NSTableView * ) aTableView
mouseDownInHeaderOfTableColumn: ( NSTableColumn * ) column
{
if ( aTableView == topicTable )
[ topicTable deselectAll: self ];
}
X.	Implement filtering-by-topic of the main table from the list in the drawer’s topic table
1.	Implement filtering-by-topic of the main table with the following method in the window controller: vocablesSelectedByTopic
- ( NSArray * ) vocablesSelectedByTopic
{
VTXVocable *v;
VTXTopic t;
NSEnumerator *te;
NSEnumerator *le;
NSMutableArray *s;
NSArray *vocables;
NSArray *topics;
topics = [ topicController selectedObjects ];
vocables = [ lexiconController arrangedObjects ];
s = [ NSMutableArray arrayWithCapacity: [vocables count] ];
le = [ vocables objectEnumerator ];
while ( v = [ le nextObject ] )
{
te = [ topics objectEnumerator ];
while ( t = [ te nextObject ] )
{
if ( [ [ v topic ] isEqualToString: [t topic string] )
[ s addObject: v ];
if ( [ [ v topic ] isEqualToString: @”” ] &&
[ t topicString isEqualToString: @”<Empty>” ] }
[ s addObject: v ];
}
}
return [ [ s copy ] autorelease ];
}
2.	Subclass NSTableView as VTXTopicTableView; add the class to the document nib and create files for the new class; override only the following method:
- ( BOOL ) acceptsFirstResponder
{
return NO;
}
3.	Change the class of the topics table to VTXTopicTableView; this table will be able to change its selection, but it will not be able to receive keyboard events. Therefore the main table will continue to function as key view even when the user is selecting items in the topic table (this is mainly to preserve proper table highlighting for rows selected when the user selects from the topics table).
Y.	Implement an HTML-based Help system
1.	Create a folder containing your HTML reference documents and add it to the project from the Resources folder, since the help files can very well be localized. Be sure to select the option to create folders recursively for any added folders when the sheet for adding the files appears. The root folder of this hierarchy should be labeled Help.
2.	Make sure there is an index.html file in the root folder of the help system, with a header block that contains something closely resembling the following code:
<title>Vocable Trainer X Help</title>
<meta name="AppleTitle" content="VocableTrainerX Help">
3.	Add the following keys and associated values to the top level of the Info.plist file:
<key>CFBundleHelpBookFolder</key>
<string>Help</string>
<key>CFBundleHelpBookName</key>
<string>VocableTrainerX Help</string>
4.	The three steps above should install the Help system; it will probably be necessary to remove the current com.apple.helpui folder from the ~/Library/Caches folder of the current user in order to display the new pages rather than a blank Help system screen in order to test it.
Z.	Implement a system of user preferences for VocableTrainerX
1.	In Interface Builder, create a new nib file and add it to the project. Name it Preferences.nib and create a Preferences panel that includes checkbox controls labeled Open New Empty Doc at Launch Time and Present Quiz in Full-screen Mode.
Create a new VTXPrefsController class that is a subclass of NSWindowController.
Create outlets for two checkboxes in VTXPrefsController; name them newDocCheckbox and quizModeCheckbox.
Create the files for VTXPrefsController and add them to the VocableTrainerX project.
2.	Next, declare prefsController as an instance variable in VTXAppController.h rather than instantiating it in Preferences.nib; the instance will be created programmatically if needed:
VTXPrefsController *prefsController;
Create a forward declaration for the new class in VTXAppController.h: import the header into the implementation of VTXAppController.
3.	Connect the Preferences… item in the application menu to an action to display the preferences window; name it showPreferences: and implement it in VTXAppController, using usual procedures for connecting a menu item in MainMenu.nib to the desired action:
- ( IBAction ) showPreferences: ( id ) sender
{
if ( !prefsController )
prefsController = [ [ VTXPrefsController alloc ] init ];
[ prefsController showWindow: self ];
}
4.	In order to display the Preferences window, VTXPrefsController must have its window outlet connected, also inherited from NSWindowController. Use the File’s Owner proxy as usual to set the class for File’s Owner to VTXPrefsController and connect its window outlet.
Implement an init method for VTXPrefsController to test the functioning of the new menu item with the following method in VTXPrefsController.m:
- ( id ) init
{
if ( self = [super initWithWindowNibName: @"Preferences"] )
{
}
return self;
}
5.	Connect the outlets in VTXPrefsController to the check boxes in the Preferences window. To test the connections, override the windowDidLoad: method:
- ( void ) windowDidLoad
{
[ newDocCheckBox setState: TRUE ];
NSLog( @”New Doc state = %d”, [ newDocCheckBox state ] );
}
6.	Set up the keys that will be used to label the settings from the Preferences panel when they are stored in the defaults database. NSString objects must be used as defaults database keys. Collect these in a separate header file for accessibility where they are needed, since the same keys will be used throughout the application. Create a new file named defaults.h in the Other Sources group of the VocableTrainerX project. Add these declarations to the new file:
#define VTXNewDocCheckBoxStateKey	@”New Doc CheckBox State”
#define VTXQuizModeCheckBoxStateKey @”Quiz Mode CheckBox State”
7.	The defaults must be registered in the database before they are used. This involves creating a dictionary object and then associating the keys in defaults.h with a set of initial values. This should be done as soon as possible after the application launches. To accomplish this, add the following class method (inherited from NSObject) to VTXAppController.m:
+ ( void ) initialize	// Note that this is a class method
{
NSMutableDictionary *defaultDict =
[ NSMutableDictionary dictionary ];
[ [ NSUserDefaults standardUserDefaults ]
registerDefaults: defaultDict ];
}
Naturally, you must now import defaults.h into VTXAppController.m.
The recommended way to register defaults is via the initialize method. A class receives this message when it is being used in an application for the first time. In contrast to the init method, an explicit [ super initialize ] message is not necessary. Recall that dictionary objects used in the defaults database can only contain objects from certain supported classes.
8.	Set up PrefsController so that operating the controls in the Preferences window allows the user to change the application-wide default values. First, import definitions of database keys from defaults.h into PrefsController.m:
#import “defaults.h”
In PrefsController.m, re-implement the windowDidLoad: method with the following code:
- ( void ) windowDidLoad
{
NSUserDefaults *defaults;
defaults = [ NSUserDefaults standardUserDefaults ];
[ newDocCheckBox setState:
[ defaults boolForKey: VTXNewDocCheckBoxStateKey ] ];
[ quizModeCheckBox setState:
[ defaults boolForKey: VTXQuizModeCheckBoxStateKey ] ];
}
Next, declare the following actions in PrefsController.h, add them to the nib file, and connect the controls in the Preferences window to their targets in the nib’s File’s Owner:
- ( IBAction ) changeNewDocCBFlag: ( id ) sender;
- ( IBAction ) changeQuizModeCBFlag: ( id ) sender;
Implement these methods as follows:
- ( IBAction ) changeNewDocCBFlag: ( id ) sender
{
[ [ NSUserDefaults standardUserDefaults ] setObject:
[ NSNumber numberWithBool: [ sender state ] ]
forKey: VTXNewDocCheckBoxStateKey ];
}
- ( IBAction ) changeQuizModeCBFlag: ( id ) sender
{
[ [ NSUserDefaults standardUserDefaults ] setObject:
[ NSNumber numberWithBool: [ sender state ] ]
forKey: VTXQuizModeCheckBoxStateKey ];
}
9.	The file named foo.mydomain.VocableTrainerX should exist in ~/Library/Preferences (the toolbar settings bring it into existence). Use the new information saved in the database to update the user interface and set the user’s desired behavior for the application. Open a new document (or not) based on the value returned by the checkbox in the Preferences window, with applicationShouldOpenUntitledFile: in VTXAppController.m: with the following code
- ( BOOL ) applicationShouldOpenUntitledFile:
( NSApplication * ) theApplication
{
NSUserDefaults *defaults =
[ NSUserDefaults standardUserDefaults ];
if ( [ defaults boolForKey: VTXNewDocCheckBoxStateKey ] )
return YES;
else
return NO;
}
Implement code for using the quiz mode setting in startQuiz: using a similar pattern.
Appendix 1. Notes for implementing the full-screen quiz window
1.	Create a VTXQuizWindow window subclass that returns YES in canBecomeKeyWindow
2.	Modify QuizWindow.nib so that the controls are in a custom view; depending on how you do this, you may have to reconnect all the outlets and actions. Include an outlet to the custom view in the nib file. Make the default quiz window blank and assign its content view to the custom view before the quiz starts (in the quiz controller’s windowDidLoad method)
3.	Add a Terminate Quiz button to the quiz window interface view; create an action for it in the quiz controller implementation. For now, just have it invoke windowShouldClose:, but it will eventually handle terminating the full-screen quiz, which will have no Close button.
4.	Implement the showFullScreenQuizWindow method in the quiz controller; its job is to capture the full screen display, allocate and initialize an instance of the custom quiz window, and set up the frame and content view so that it shows the quiz controls. In addition, you will have to manually load the nib containing the custom view so that its outlet is set by the time the quiz window comes into existence; at the end of this method, simply re-use the code that sets up the initial state of the quiz by invoking windowDidLoad. The screen capture code looks like this:
if (CGDisplayCapture(kCGDirectMainDisplay) != kCGErrorSuccess)
{
NSLog( @"Unable to capture the main display" );
return;
}
5.	All the quiz controls should work normally, with one proviso: In windowDidLoad there is a line that sets the window’s content view if the non-full-screen version is loaded; this will not work for the full-screen version, so you must make this instruction conditional on there being no instance of the full screen window; this is not tricky – the full screen instance will not be created unless the user has set the preferences to run the quiz in that mode (see below). This step is necessary because the full-screen version sets its content in the method that creates it.
6.	All that is left is to clean up after the user terminates a full-screen quiz. In the terminate: method, after the line that invokes windowShouldClose:, test for a full-screen window instance and if it exists, release the instance and unwind the capture of the main display:
if (CGDisplayRelease(kCGDirectMainDisplay) != kCGErrorSuccess)
{
NSLog( @"Unable to release the display(s)" );
// Any GUI alert should appear at shield window level
}
If a full-screen quiz window does not exist (i.e., the user has not requested full-screen mode), then just send the quiz controller’s window a close message. This will cause the delegate method, windowShouldClose:, to be invoked automatically.
In either case, the random enumerator instance should be released, and the delegate method should take care of bringing the main window back in front and invoking the main window controller’s endQuizWithFlag: method.
7.	The initialization of the full-screen, borderless quiz window looks like this:
vtxQuizWindow = [ [ VTXQuizWindow alloc ]
initWithContentRect: [ [ NSScreen mainScreen ] frame ]
styleMask: NSBorderlessWindowMask
backing: NSBackingStoreBuffered
defer: NO screen: [ NSScreen mainScreen ] ];
[ vtxQuizWindow setFrame:
[ [ NSScreen mainScreen ] frame ] display: YES ];
[ vtxQuizWindow setContentView: quizView ];
[ vtxQuizWindow setLevel:  CGShieldingWindowLevel( ) ];
[ vtxQuizWindow makeKeyAndOrderFront: self ];
Appendix 2. Further notes on quiz filtering based on success rates
1.	In Section T, a method was included that eliminated items from a typical quiz under specified conditions; however, such items should not be invariably omitted, since occasional practice is beneficial. Testing methodology can grow rather technical, but the following will illustrate a straightforward approach based simply on pseudo-random number generation. Insert another if block in the code for showNextItem, as follows:
else if ( [ [ item score ] floatValue ] >= 0.8 &&
random( ) % 1000 > 666 )
{
[ promptField setStringValue: [ item firstL ] ];
[ solutionField setStringValue: @"" ];
}
Appendix 3. Localizable strings within the VocableTrainerX source code
1.	Create a Localizable.strings file for the VocableTrainerX project. This file will contain key-value pairs for specifying string values for any localizable strings in the application. This file name is used by convention if only one file is used to list these strings. Additional files will also use the .strings extension, and all such files should be added to each localized resource (.lproj) folder in the project.
2.	In DDVLexiconModel (initWithDocument: method) initialize the field labels with:
NSLocalizedString( @"Some Language", nil )
NSLocalizedString( @"Andere Sprache", nil )
NSLocalizedString( @"Topic", nil )
The second argument could be used to supply a descriptive comment for use by localization contractors, to explain the intended purpose of each string.
3.	In DDVLexItem (init method) initialize the default values with:
NSLocalizedString( @"A Word", nil )
NSLocalizedString( @"Ein Wort", nil )
NSLocalizedString( @"None", nil )
4.	In DDVToolbarDelegateCategory, localize the labels for the toolbar items:
NSLocalizedString( @"Insert Item", nil )
NSLocalizedString( @"Delete Item", nil )
NSLocalizedString( @"Export Data", nil )
NSLocalizedString( @"Start Quiz", nil )
NSLocalizedString( @"Search Data", nil )
NSLocalizedString( @"Topic Drawer", nil )
5.	In DDVTopic, localize the default value in the init method (this string also appears in the readNSStringFromPBoard method in MMPasteboardCategory):
NSLocalizedString( @"None", nil )
6.	In DDVWindowController localize the selection status message (awakeFromNib); also in the updateSelectionStatus method:
NSLocalizedString( @"%d of %d selected", nil )
Localize the title string for the Inspector window (windowTitleForDocument…)
NSLocalizedString( @"XQuizIt Selection: ", nil )
Localize the placeholder string for empty topic table entries in updateTopicsList and lexiconSelectedByTopic methods:
NSLocalizedString( @"<Empty>", nil )
Localize the menu item strings for undo manager operations (indexed accessor methods)
NSLocalizedString( @"Insert Item", nil )
NSLocalizedString( @"Edit", nil )
NSLocalizedString( @"Delete Item", nil )
7.	In MMPrintView, localize the string for enumerating printed pages:
NSLocalizedString( @"Page", nil )
Appendix 4. Debugging notes related to quiz-presentation code
1.	When the quiz is NOT presented in full-screen mode, several adjustments must be made in managing the main document window, the inspector window, and the quiz window. In the startQuiz: method in VTXMainWindowController, make the following code changes:
if ( [ defaults boolForKey: VTXQuizModeCheckBoxStateKey ] )
[ quizController showFullScreenQuizWindow ];
else
{
if ( [ inspector isVisible ] )
[ inspector orderOut: self ];
[ [ self window ] orderOut: self ];
quizSession = [ NSApp beginModalSessionForWindow:
[ quizController window ] ];
}
[ [ self window ] orderOut: self ];
[ NSApp runModalSession: quizSession ];
This hides the inspector window during the quiz, which is run in a modal session so that ordinary VocableTrainerX commands will be disabled. The NSModalSession instance variable quizSession is declared in VTXMainWindowController.
2.	Of course, the modal quiz session must be terminated properly when the user closes the quiz window. In endQuizWithFlag:, add the following block of code:
if ( [ NSApp modalWindow ] )
[ NSApp endModalSession: quizSession ];
This ends the modal session (well, duh!); the windowShouldClose: method of the quiz window controller already takes care of bringing the main window back in front.
Appendix 5. Notes for addition of language-swapping feature in quiz window
1.	In VTXQuizController, add some logic for simple random swapping of languages during the quiz. In detail, convert the code in showNextItem to produce a random number (this will need to be stored in an instance variable of the class); use this value to swap the prompted-for language depending on whether the random value is even or odd. That is, if the value is odd, prompt with the second-language value and test the response for the first-language value, and do the opposite if the value is even. The solve: method will also need to be changed to test for the correct half of the lexical item and to show the correct solution in the feedback field.
2.	Set up a new checkbox in the preferences window so the user can switch between systematic and random language prompts, and add a key for the new preference to defaults.h. Obtain the default value for this preference in the quiz controller’s windowDidLoad method so that it is only loaded once for each quiz. Update the checkboxes for the Preferences window in that window’s windowDidLoad method using the current set of values in the defaults database.