/* StackModel.m - card stack class for Popup.app Copyright (C) 2003, 2004 Bjorn Gohla, Rob Burns This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 675 Mass Ave, Cambridge, MA 02111, USA. */ #include #include "EditorWindowController.h" #include "QuizController.h" #include "SpellingQuizController.h" #include "NSValue+Extensions.h" #include "Constants.h" @implementation StackModel - (void) dealloc { TEST_RELEASE(cards); RELEASE(scheduler); RELEASE(notes); RELEASE(frontTitle); RELEASE(backTitle); RELEASE(filteredCards); [super dealloc]; } - (id) init { if( (self = [super init]) ) { cards = [[NSMutableArray alloc] init]; scheduler = [[Scheduler alloc] init]; [scheduler setCardStack: self]; notes = [[NSString alloc] init]; frontTitle = [[NSString alloc] initWithString: @"Front"]; backTitle = [[NSString alloc] initWithString: @"Back"]; filteredCards = [[NSMutableArray alloc] init]; return self; } return nil; } - (StackModel *) initWithDictionary: (NSDictionary *) dict; { NSEnumerator *wEnum = nil; NSDictionary *cDict = nil; if( (self = [self init]) ) { if( [dict objectForKey: @"Notes"] != nil ) [self setNotes: [dict objectForKey: @"Notes"]]; if( [[dict objectForKey: @"Languages"] objectForKey: @"FirstValue"] != nil ) [self setFrontTitle: [[dict objectForKey: @"Languages"] objectForKey: @"FirstValue"]]; if( [[dict objectForKey: @"Languages"] objectForKey: @"SecondValue"] != nil ) [self setBackTitle: [[dict objectForKey: @"Languages"] objectForKey: @"SecondValue"]]; if( [dict objectForKey: @"State"] != nil ) [scheduler setState: [dict objectForKey: @"State"]]; wEnum = [[dict objectForKey: @"Words"] objectEnumerator]; while( (cDict = [wEnum nextObject]) ) { [cards addObject: [CardModel cardWithDictionary: cDict]]; } return self; } return nil; } - (NSDictionary *) stackFileRep { NSMutableDictionary *dict = [NSMutableDictionary dictionaryWithCapacity: 1]; NSMutableArray *cArray = [NSMutableArray arrayWithCapacity: 1]; NSEnumerator *cEnum = [cards objectEnumerator]; CardModel *card = nil; [dict setObject: [NSDictionary dictionaryWithObjectsAndKeys: frontTitle, @"FirstValue", backTitle, @"SecondValue", nil] forKey: @"Languages"]; [dict setObject: [scheduler schedule] forKey: @"Schedule"]; [dict setObject: [scheduler stateFileRep] forKey: @"State"]; [dict setObject: notes forKey: @"Notes"]; while( (card = [cEnum nextObject]) ) { [cArray addObject: [card cardFileRep]]; } [dict setObject: cArray forKey: @"Words"]; return dict; } // **************************** // CardStack properties methods // **************************** - (NSArray *) cards { return cards; } - (Scheduler *) scheduler { return scheduler; } - (NSArray *) filteredCards { return filteredCards; } - (NSString *) notes { return notes; } - (void) setNotes: (NSString *)aNote { RELEASE(notes); notes = [[NSString alloc] initWithString: aNote]; } - (NSString *) frontTitle { return frontTitle; } - (void) setFrontTitle: (NSString *)aTitle { RELEASE(frontTitle); frontTitle = [[NSString alloc] initWithString: aTitle]; } - (NSString *) backTitle; { return backTitle; } - (void) setBackTitle: (NSString *)aTitle { RELEASE(backTitle); backTitle = [[NSString alloc] initWithString: aTitle]; } - (NSString *) description { NSMutableString *desc = [[NSMutableString alloc] init]; int i; [desc appendFormat: @"Front Title - %@, Back Title - %@\n", frontTitle, backTitle]; [desc appendFormat: @"Schedule - %@\n", [[[self scheduler] schedule] description]]; [desc appendFormat: @"State - %@\n", [[[self scheduler] state] description]]; [desc appendFormat: @"Notes - %@\n", notes]; for( i=0; i < [cards count]; i++ ) { [desc appendFormat: @"%@", [[cards objectAtIndex: i] description]]; } [desc appendFormat: @"Filtered Cards - %@\n", [filteredCards description]]; return [NSString stringWithString: desc]; } // Quiz related methods // ******************** - (NSArray *) drawAlternatives:(int) number excluding: (NSString *)exclude { NSMutableArray *myCandidates; int i,x; myCandidates = AUTORELEASE([NSMutableArray arrayWithCapacity: [filteredCards count]]); [myCandidates retain]; for(i=0;i<[filteredCards count];i++) { x = [[filteredCards objectAtIndex: i] intValue]; [myCandidates addObject: [[cards objectAtIndex: x] back]]; } [myCandidates removeObject: exclude]; while(number<[myCandidates count]) { [myCandidates removeObjectAtIndex: [NSValue nextIntWithMax: [myCandidates count]]-1]; }; return myCandidates; } - (CardModel *) drawCardUsingAchievement: (NSArray *)achievementList method: (int)method { int i, x; NSMutableArray *tempArray; NSMutableArray *answers; CardModel *result; int reps = [[[NSUserDefaults standardUserDefaults] objectForKey: @"Repetitions"] intValue]; tempArray = [NSMutableArray arrayWithCapacity: 1]; // the next line is there because nextIntWithMax would never return // zero. consequently the word at index zero would never get selected. // there is surely a better way than to just put a spacer in the array, // but according to my testing this works. [tempArray addObject: @"x"]; if( method == SHOWONCE_QUIZ_METHOD ) { for(i=0;i<[achievementList count];i++) { if([[achievementList objectAtIndex: i] intValue] == NOTUSED) { [tempArray addObject: [NSNumber numberWithUnsignedInt: i]]; } } } else { for(i=0;i<[achievementList count];i++) { if([[achievementList objectAtIndex: i] intValue] < reps) { [tempArray addObject: [NSNumber numberWithUnsignedInt: i]]; } } } i = [NSValue nextIntWithMax: [tempArray count]-1]; i = [[tempArray objectAtIndex: i] intValue]; x = [[filteredCards objectAtIndex: i] intValue]; // we don't return a pointer to the actual cards in the stack, but create a new card // based on that information and return it. that way the stack doesn't get polluted // with 'answers' information. and it just seems safer. don't want the quizes to modify // the stack inadvertantly. result = [[CardModel alloc] init]; [result setIndex: x]; [result setFront: [[cards objectAtIndex: x] front]]; [result setBack: [[cards objectAtIndex: x] back]]; // get 'all' the possible correct answers. It // might not actually get all of them, but it should get all of them // in most cases. We also reuse the i counter variable. answers = [NSMutableArray arrayWithCapacity: 1]; [answers addObject: [[cards objectAtIndex: [result index]] front]]; [answers addObject: [[cards objectAtIndex: [result index]] back]]; for(i = 0; i<[filteredCards count]; i++) { x = [[filteredCards objectAtIndex: i] intValue]; if(![answers containsObject: [[cards objectAtIndex: x] front]] || ![answers containsObject: [[cards objectAtIndex: x] back]]) { if([answers containsObject: [[cards objectAtIndex: x] front]]) { [answers addObject: [[cards objectAtIndex: x] back]]; } else if([answers containsObject: [[cards objectAtIndex: x] back]]) { [answers addObject: [[cards objectAtIndex: x] front]]; } } } [result setAnswers: answers]; AUTORELEASE(result); return result; } - (void) flipLangs { // NSString *tempOne, *tempTwo; // int i; // // tempOne = [[NSString alloc]init]; // tempTwo = [[NSString alloc]init]; // // for(i=0;i<[WORDS count];i++) // { // tempOne = [NSString stringWithString: // [[WORDS objectAtIndex: i] objectForKey: @"FirstValue"]]; // tempTwo = [NSString stringWithString: // [[WORDS objectAtIndex: i] objectForKey: @"SecondValue"]]; // // [[WORDS objectAtIndex: i] setObject: tempTwo forKey: @"FirstValue"]; // [[WORDS objectAtIndex: i] setObject: tempOne forKey: @"SecondValue"]; // } // // [achievementList release]; // achievementList = [NSMutableArray arrayWithCapacity: [WORDS count]]; // [achievementList retain]; // // for(i=0;i<[WORDS count];i++) // { // [achievementList addObject: // [NSNumber numberWithUnsignedInt: 0]]; // } } // ************** // Action methods // ************** - (void) addCard: (CardModel *) aCard atIndex: (int)index; { if(index >= 0 && aCard != nil) { [cards insertObject: aCard atIndex: index]; [scheduler incrementCardsInEntry: [scheduler entryForCard: aCard]]; if( [aCard slot] > -1 ) [scheduler incrementCardsInEntry: [[scheduler state] objectAtIndex: 0]]; } else if(aCard != nil) { [cards addObject: aCard]; [scheduler incrementCardsInEntry: [scheduler entryForCard: aCard]]; if( [aCard slot] > -1 ) [scheduler incrementCardsInEntry: [[scheduler state] objectAtIndex: 0]]; } } - (void) deleteCardAtIndex: (int) index { CardModel *aCard = [cards objectAtIndex: index]; [scheduler decrementCardsInEntry: [scheduler entryForCard: aCard]]; if( [aCard slot] > -1 ) [scheduler decrementCardsInEntry: [[scheduler state] objectAtIndex: 0]]; [cards removeObjectAtIndex: index]; } // this method is not strict enough. If there is more than one // card in the stack with the same word and meaning, it may remove the wrong one. FIXME - (void) deleteCard: (CardModel *)aCard { int i; for(i=0;i<[cards count];i++) { if([[aCard front] isEqualToString: [[cards objectAtIndex: i] front]] && [[aCard back] isEqualToString: [[cards objectAtIndex: i] back]]) { [self deleteCardAtIndex: i]; } } [self clearFilter]; } // generic method for use by copy:, and the drag and drop code - (void) writeRows: (NSArray *)rows toPasteboard: (NSPasteboard *)pboard { NSMutableString *dataString = [NSMutableString stringWithCapacity: 1]; NSMutableArray *dataArray = [NSMutableArray arrayWithCapacity: 1]; NSEnumerator *e = [rows objectEnumerator]; NSNumber *row; int index = 0; while( (row = [e nextObject]) ) { index = [[filteredCards objectAtIndex: [row intValue]] intValue]; [dataArray addObject: [(CardModel *)[cards objectAtIndex: index] words]]; [dataString appendString: [[cards objectAtIndex: index] front]]; [dataString appendString: @"\t"]; [dataString appendString: [[cards objectAtIndex: index] back]]; [dataString appendString: @"\n"]; } [pboard declareTypes: [NSArray arrayWithObjects: NSTabularTextPboardType, NSStringPboardType, PopupCardsPboardType, nil] owner: self]; [pboard setString: dataString forType: NSStringPboardType]; [pboard setString: dataString forType: NSTabularTextPboardType]; [pboard setPropertyList: [NSDictionary dictionaryWithObjectsAndKeys: dataArray, @"Cards", nil] forType: PopupCardsPboardType]; } // generic method for use by paste:, and the drag and drop code - (void) insertData: (NSArray *)data atRow: (int)row inTableView: (NSTableView *)tableView { NSMutableDictionary *scheduleEntry; NSDictionary *entry; CardModel *aCard; NSEnumerator *e; int end, start; start = end = row; scheduleEntry = [[[self scheduler] state] objectAtIndex: 0]; e = [data objectEnumerator]; while( (entry = [e nextObject]) ) { aCard = [[CardModel alloc]init]; [aCard setFront: [entry objectForKey: @"Front"]]; [aCard setBack: [entry objectForKey: @"Back"]]; [self addCard: aCard atIndex: end]; RELEASE(aCard); [[self scheduler] incrementCardsInEntry: scheduleEntry]; end++; } if( [[tableView delegate] displayingAllCards] ) { [self clearFilter]; [self sortFilteredCardsBySlot]; [tableView reloadData]; } #ifdef __APPLE__ int i; [tableView deselectAll: self]; for( i = start; i < end; i++ ) [tableView selectRow: i byExtendingSelection: YES]; #else [tableView selectRowIndexes: [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(start,end)] byExtendingSelection: NO]; #endif [[[[tableView window] windowController] document] updateChangeCount: NSChangeDone]; } - (void) clearFilter { int i; NSAutoreleasePool *arp = [[NSAutoreleasePool alloc] init]; [filteredCards removeAllObjects]; for( i = 0; i < [cards count]; i++ ) { [filteredCards addObject: [NSNumber numberWithInt: i]]; } RELEASE(arp); } // should rename this method to something more descriptive - (void) filterForItem: (NSMutableDictionary *) anItem; { int i, x; NSDate *time; [filteredCards removeAllObjects]; if( [anItem objectForKey: @"Times"] != nil || [[anItem objectForKey: @"Slot"] intValue] == 0) // its not an individual quiz { x = [[anItem objectForKey: @"Slot"] intValue]; for( i = 0; i < [cards count]; i++ ) { if( [[cards objectAtIndex: i] slot] == x ) { [filteredCards addObject: [NSNumber numberWithInt: i]]; } } [scheduler setNumberOfCards: [filteredCards count] inEntry: anItem]; } else if( [anItem objectForKey: @"Time"] != nil) // it is an individual quiz { x = [[anItem objectForKey: @"Slot"] intValue]; time = [anItem objectForKey: @"Time"]; for( i = 0; i < [cards count]; i++ ) { if( [[cards objectAtIndex: i] time] != nil && [[cards objectAtIndex: i] slot] == x && [[[cards objectAtIndex: i] time] isEqualToDate: (NSDate *)time] ) { [filteredCards addObject: [NSNumber numberWithInt: i]]; } } [scheduler setNumberOfCards: [filteredCards count] inEntry: anItem]; } else if([[anItem objectForKey: @"Slot"] intValue] == -1) { [self clearFilter]; [scheduler setNumberOfCards: [filteredCards count] inEntry: anItem]; } else { NSLog(@"we recieved bunk data, what to do. currently leaving the filter empty"); } } - (void) sortFilteredCardsBySlot { int k = 0; // index in filteredCardsArray int i = 0; // loop counter // there are two variables here because its not always necessary // to look through the entire array. this should be quicker. although // its not working at the moment. see commented out parts. FIXME int slot = 0; for( i = 0; i < [filteredCards count]; i++ ) { slot = [[cards objectAtIndex: [[filteredCards objectAtIndex: k] intValue]] slot]; // oops lost the comment if( slot > -1 ) { [filteredCards addObject: [NSNumber numberWithInt: [[filteredCards objectAtIndex: k] intValue]]]; [filteredCards removeObjectAtIndex: k]; // i++; } else k++; } } // ********************************** // NSTableDataSource Protocol Methods // ********************************** - (int) numberOfRowsInTableView: (NSTableView *) aTableView { return [filteredCards count]; } - (id) tableView: (NSTableView *) aTableView objectValueForTableColumn: (NSTableColumn *) aTableColumn row: (int) rowIndex { NSString *theValue; int x; NSParameterAssert(rowIndex >= 0); x = [[filteredCards objectAtIndex: rowIndex] intValue]; if( [[aTableColumn identifier] isEqualToString: @"Front"] ) { theValue = [[cards objectAtIndex: x] front]; return theValue; } if( [[aTableColumn identifier] isEqualToString: @"Back"] ) { theValue = [[cards objectAtIndex: x] back]; return theValue; } else { return nil; } } - (void) tableView: (NSTableView *) aTableView setObjectValue: (id) anObject forTableColumn: (NSTableColumn *) aTableColumn row: (int) rowIndex { int x; NSParameterAssert(rowIndex >= 0); x = [[filteredCards objectAtIndex: rowIndex] intValue]; [[[[aTableView window] windowController] document] updateChangeCount: NSChangeDone]; if( [[aTableColumn identifier] isEqualToString: @"Front"] ) { [[cards objectAtIndex: x] setFront: anObject]; } else if( [[aTableColumn identifier] isEqualToString: @"Back"] ) { [[cards objectAtIndex: x] setBack: anObject]; } } // **************************************************** // NSTableDataSource Protocol Methods for drag and drop // **************************************************** - (BOOL) tableView: (NSTableView *) tableView writeRows: (NSArray *) rows toPasteboard: (NSPasteboard *) pboard { [self writeRows: rows toPasteboard: pboard]; return YES; } - (NSDragOperation) tableView: (NSTableView *) tableView validateDrop: (id) info proposedRow: (int) row proposedDropOperation: (NSTableViewDropOperation) operation { // need to force the drop point into the range of unscheduled cards // if we are in AllCards view (setDrop;row:dropOperation). and do // something about dropping onto the table in non-AllCards view. // The latter should be allowed, but there needs to be something to let the // user know that his cards aren't going to be going into that // schedule slot (this last bit probably gets implemented in the next // method). FIXME if( ![tableView isEqual: [info draggingSource]] ) { return NSDragOperationCopy; } else return NSDragOperationNone; } // this method has it basically right but needs to be more user friendly - (BOOL) tableView: (NSTableView *) tableView acceptDrop: (id) info row: (int) row dropOperation: (NSTableViewDropOperation) operation { NSArray *theData; theData = [[[info draggingPasteboard] propertyListForType: PopupCardsPboardType] objectForKey: @"Cards"]; if( [theData count] > 0 ) { [self insertData: theData atRow: 0 inTableView: tableView]; return YES; } return NO; } @end