-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathGeniusDocument.m
More file actions
1030 lines (854 loc) · 34.6 KB
/
GeniusDocument.m
File metadata and controls
1030 lines (854 loc) · 34.6 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/*
Genius
Copyright (C) 2003-2006 John R Chang
Copyright (C) 2007-2008 Chris Miner
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.
http://www.gnu.org/licenses/gpl.txt
*/
#import "GeniusDocument.h"
#import "GeniusDocument_DebugLogging.h"
#import "GeniusDocumentFile.h"
#import "IconTextFieldCell.h"
#import "GeniusToolbar.h"
#import "GeniusItem.h"
#import "GeniusAssociationEnumerator.h"
#import "MyQuizController.h"
#import "GeniusPreferencesController.h"
#import "GeniusPair.h"
#import "GeniusAssociation.h"
#import "IsPairImportantTransformer.h"
#import "ColorFromPairImportanceTransformer.h"
#import "GSTableView.h"
@interface GeniusDocument (VeryPrivate)
- (NSArray *) _enabledAssociationsForPairs:(NSArray *)pairs;
- (void) _updateStatusText;
- (void) _updateLevelIndicator;
@end
// Standard NSDocument subclass for controlling display and editing of a Genius file.
@implementation GeniusDocument
static NSArray *columnBindings;
//! returns list of keypaths used to get at values of a GeniusPair.
+ (NSArray *) columnBindings
{
return columnBindings;
}
//! Sets up logging and registers value transformers.
+ (void) initialize
{
[super initialize];
// install our read only value tranformers referenced in GeniusDocument.nib.
[NSValueTransformer setValueTransformer:[[[IsPairImportantTransformer alloc] init] autorelease] forName:@"IsPairImportantTransformer"];
[NSValueTransformer setValueTransformer:[[[ColorFromPairImportanceTransformer alloc] init] autorelease] forName:@"ColorFromPairImportanceTransformer"];
columnBindings = [[NSArray alloc] initWithObjects:@"itemA.stringValue", @"itemB.stringValue", @"customGroupString", @"customTypeString", @"associationAB.scoreNumber", @"associationBA.scoreNumber", @"notesString", nil];
// install swizzle based logging
#if DEBUG
[self installLogging];
#endif
}
//! Basic NSDocument init method.
/*!
Initializes some transformers and registers them with NSValueTransformer.
*/
- (id)init
{
self = [super init];
if (self) {
// Init array for genius pairs.
[self setPairs:[NSMutableArray array]];
// Expect not to be loading a 1.0 format file
_shouldShowImportWarningOnSave = NO;
// 50 - 50 value for the learn review setting.
probabilityCenter = [[NSNumber alloc] initWithFloat:50.0F];
// default card side titles
_columnHeadersDict = [[NSMutableDictionary dictionaryWithObjectsAndKeys:@"Question", @"columnA", @"Answer", @"columnB", nil] retain];
// default visible columns.
_visibleColumnIdentifiers = [[NSMutableArray alloc] initWithObjects:@"disabled", @"columnA", @"columnB", @"scoreAB", nil];
_customTypeStringCache = [[NSMutableSet alloc] init];
// setup change tracking of ourself
[self addObserver:self];
// initialize table layout font size and row height monitor changes to preference
[self setListTextSizeMode:[[NSUserDefaults standardUserDefaults] integerForKey:GeniusPreferencesListTextSizeModeKey]];
[[NSUserDefaults standardUserDefaults] addObserver:self forKeyPath:@"ListTextSizeMode" options:NSKeyValueObservingOptionNew context:nil];
}
return self;
}
//! Releases various ivars and deallocates memory.
- (void) dealloc
{
[_pairs release];
[_visibleColumnIdentifiers release];
[_columnHeadersDict release];
[_searchField release];
[_customTypeStringCache release];
[probabilityCenter release];
[_sortedCustomTypeStrings release];
[super dealloc];
}
//! Standard NSWindowController override.
- (NSString *)windowNibName
{
// Override returning the nib file name of the document
// If you need to use a subclass of NSWindowController or if your document supports multiple NSWindowControllers, you should remove this method and override -makeWindowControllers instead.
return @"GeniusDocument";
}
//! Initializes UI based on the existing document model info.
- (void)windowControllerDidLoadNib:(NSWindowController *) aController
{
[super windowControllerDidLoadNib:aController];
// Add any code here that needs to be executed once the windowController has loaded the document's window.
// Slip the help text in on top of the table view so it doesn't flicker.
NSRect newFrame = [tableView convertRect:[helpTextOverlay frame] fromView:[helpTextOverlay superview]];
[helpTextOverlay retain];
[helpTextOverlay removeFromSuperview];
[helpTextOverlay setFrame:newFrame];
[tableView addSubview:helpTextOverlay];
[helpTextOverlay release];
// set up tool bar and enable tabbing from search field to table view.
[self setupToolbarForWindow:[aController window]];
[_searchField setNextKeyView:tableView];
[self reloadInterfaceFromModel];
}
//! inserts the item at index in pairs array, taking care to start observing it.
- (void) insertObject:(GeniusPair*) pair inPairsAtIndex:(int)index
{
NSUndoManager *undoManager = [self undoManager];
[[undoManager prepareWithInvocationTarget:self] removeObjectFromPairsAtIndex:index];
[pair addObserver:self];
[_pairs insertObject:pair atIndex:index];
}
//! removes the item at index from pairs array, taking care to stop observing it first.
- (void) removeObjectFromPairsAtIndex:(int) index
{
NSUndoManager *undoManager = [self undoManager];
GeniusPair *pair = [_pairs objectAtIndex:index];
[[undoManager prepareWithInvocationTarget:self] insertObject:pair inPairsAtIndex:index];
[pair removeObserver:self];
[_pairs removeObjectAtIndex:index];
}
//! _pairs getter.
- (NSArray*) pairs
{
return _pairs;
}
//! _pairs setter. observes contents of @a values
- (void) setPairs: (NSMutableArray*) values
{
[_pairs makeObjectsPerformSelector:@selector(removeObserver:) withObject:self];
[values makeObjectsPerformSelector:@selector(addObserver:) withObject:self];
[values retain];
[_pairs release];
_pairs = values;
}
//! _searchField getter.
- (NSSearchField *) searchField
{
return _searchField;
}
//! Dumps the GeniusDocument#_customTypeStringCache and rebuilds it from _pairs.
- (void) _reloadCustomTypeCacheSet
{
[_customTypeStringCache removeAllObjects];
NSEnumerator * pairEnumerator = [_pairs objectEnumerator];
GeniusPair * pair;
while ((pair = [pairEnumerator nextObject]))
{
NSString * customTypeString = [pair customTypeString];
if (customTypeString && [customTypeString isEqualToString:@""] == NO)
[_customTypeStringCache addObject:customTypeString];
}
[_sortedCustomTypeStrings release];
_sortedCustomTypeStrings = [[[_customTypeStringCache allObjects] sortedArrayUsingSelector:@selector(localizedCaseInsensitiveCompare:)] retain];
}
//! Updates fontSize and rowHeight to reflect the current small medium or large List Text preference
- (void) setListTextSizeMode: (int) mode
{
[self willChangeValueForKey:@"fontSize"];
[self willChangeValueForKey:@"rowHeight"];
switch (mode)
{
case 0:
fontSize = [NSFont smallSystemFontSize];
break;
case 1:
fontSize = [NSFont systemFontSize];
break;
case 2:
fontSize = 16.0f;
break;
default:
fontSize = [NSFont systemFontSize];
}
NSLayoutManager *layoutManager = [[[NSLayoutManager alloc] init] autorelease];
// the magic 4 pixels here accomodates the customGroup NSComboBoxCell which is otherwise too big.
rowHeight = 4.0f + [layoutManager defaultLineHeightForFont:[NSFont systemFontOfSize:fontSize]];
[self didChangeValueForKey:@"rowHeight"];
[self didChangeValueForKey:@"fontSize"];
}
//! Updates UI to reflect the document.
- (void) reloadInterfaceFromModel
{
// Sync with _visibleColumnIdentifiers
[tableView setVisibleColumns:_visibleColumnIdentifiers];
if ([_pairs count])
{
[tableView reloadData];
[self _reloadCustomTypeCacheSet];
}
[self _updateStatusText];
[self _updateLevelIndicator];
}
@end
//! The super secret stuff that not even we should be using.
/*!
@category GeniusDocument(VeryPrivate)
I don't know what makes these worthy of being in the VeryPrivate category.
*/
@implementation GeniusDocument(VeryPrivate)
//! Convenience method to check which GeniusPair GeniusAssociation scores are Displayed.
/*!
Hiding a score column excludes its related GeniusAssociation from the quiz.
@todo Wouldn't this be better controlled during Quiz setup?
@todo Is this really that clear as UI control?
*/
- (NSArray *) _enabledAssociationsForPairs:(NSArray *)pairs
{
BOOL useAB = !([tableView columnWithIdentifier:@"scoreAB"] < 0);
BOOL useBA = !([tableView columnWithIdentifier:@"scoreBA"] < 0);
return [GeniusPair associationsForPairs:pairs useAB:useAB useBA:useBA];
}
//! Updates the selection summary text at bottom of window.
- (void) _updateStatusText
{
NSString * status = nil;
int rowCount = [_pairs count];
if (rowCount > 0)
{
NSString * queryString = [arrayController filterString];
if ([queryString length] > 0)
{
NSString * format = NSLocalizedString(@"n of m shown", nil);
status = [NSString stringWithFormat:format, [[arrayController arrangedObjects] count], rowCount];
}
else
{
int numberOfSelectedRows = [tableView numberOfSelectedRows];
if (numberOfSelectedRows > 0)
{
NSString * format = NSLocalizedString(@"n of m selected", nil);
status = [NSString stringWithFormat:format, numberOfSelectedRows, rowCount];
}
else if (rowCount == 1)
{
status = NSLocalizedString(@"1 item", nil);
}
else
{
NSString * format = NSLocalizedString(@"n items", nil);
status = [NSString stringWithFormat:format, rowCount];
}
}
}
[statusField setObjectValue:status];
}
//! Updates the progress bar at lower right of Genius window to reflect current success with a Genius Document.
- (void) _updateLevelIndicator
{
NSArray * associations = [self _enabledAssociationsForPairs:_pairs];
int associationCount = [associations count];
if (associationCount == 0)
{
[levelIndicator setDoubleValue:0.0];
[levelField setStringValue:@""];
return;
}
int learnedAssociationCount = 0;
NSEnumerator * associationEnumerator = [associations objectEnumerator];
GeniusAssociation * association;
while ((association = [associationEnumerator nextObject]))
if ([association scoreNumber] != nil)
learnedAssociationCount++;
float percentLearned = (float)learnedAssociationCount/(float)associationCount;
[levelIndicator setDoubleValue:(percentLearned * 100.0)];
[levelField setDoubleValue:(percentLearned * 100.0)];
}
@end
//! Collection of methods loosely related to coordinating model and view changes.
/*! @category GeniusDocument(UndoRedoSupport) */
@implementation GeniusDocument(UndoRedoSupport)
//! support for undo of property changes.
- (void) setValue:(id)value forKeyPath:(NSString*)keyPath inObject:(id)object
{
[object setValue:value forKeyPath:keyPath];
}
//! registers observer for relevant keys.
- (void) addObserver: (id) observer
{
[self addObserver:observer forKeyPath:@"probabilityCenter" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
[_columnHeadersDict addObserver:observer forKeyPath:@"columnA" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
[_columnHeadersDict addObserver:observer forKeyPath:@"columnB" options:(NSKeyValueObservingOptionNew | NSKeyValueObservingOptionOld) context:nil];
}
//! un-registers up observer for relevant keys.
- (void) removeObserver: (id) observer
{
[self removeObserver:self forKeyPath:@"probabilityCenter"];
[_columnHeadersDict removeObserver:self forKeyPath:@"columnA"];
[_columnHeadersDict removeObserver:self forKeyPath:@"columnB"];
}
//! Catches changes to many objects in the model graph and updates cached values as needed.
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context
{
if ([keyPath isEqualToString:@"ListTextSizeMode"])
{
[self setListTextSizeMode:[[change objectForKey:NSKeyValueChangeNewKey] intValue]];
}
else
{
NSUndoManager *undoManager = [self undoManager];
id oldValue = [change objectForKey:NSKeyValueChangeOldKey];
if (oldValue == [NSNull null]) {
oldValue = nil;
}
[[undoManager prepareWithInvocationTarget:self] setValue:oldValue forKeyPath:keyPath inObject:object];
if ([keyPath isEqualToString:@"customTypeString"])
[self _reloadCustomTypeCacheSet];
[self _updateStatusText];
[self _updateLevelIndicator];
}
}
@end
//! Support for the NSTableView.
/*! GeniusDocument(NSTableDataSource) */
@implementation GeniusDocument(NSTableDataSource)
//! Copy paste support, writes out selected items to @a pboard as tab delimited text.
- (BOOL)tableView:(NSTableView *)tableView writeRows:(NSArray *)rows toPasteboard:(NSPasteboard*)pboard
{
[pboard declareTypes:[NSArray arrayWithObjects:NSTabularTextPboardType, nil] owner:self];
// Convert row numbers to items
_pairsDuringDrag = [NSMutableArray array];
NSEnumerator * rowNumberEnumerator = [rows objectEnumerator];
NSNumber * rowNumber;
while ((rowNumber = [rowNumberEnumerator nextObject]))
{
GeniusItem * item = [[arrayController arrangedObjects] objectAtIndex:[rowNumber intValue]];
[(NSMutableArray *)_pairsDuringDrag addObject:item];
}
NSString * outputString = [GeniusPair tabularTextFromPairs:_pairsDuringDrag order:[GeniusDocument columnBindings]];
[pboard setString:outputString forType:NSTabularTextPboardType];
[pboard setString:outputString forType:NSStringPboardType];
return YES;
}
//! Validates drop target.
- (NSDragOperation)tableView:(NSTableView *)aTableView validateDrop:(id <NSDraggingInfo>)info proposedRow:(int)row proposedDropOperation:(NSTableViewDropOperation)operation
{
if ([info draggingSource] == aTableView) // intra-document
{
if (operation == NSTableViewDropOn)
return NSDragOperationNone;
return NSDragOperationMove;
}
else // inter-document, inter-application
{
[aTableView setDropRow:-1 dropOperation:NSTableViewDropOn]; // entire table view
return NSDragOperationCopy;
}
}
//! Accept drop previously validated.
/*! Unpacks GeniusItem instances from the paste board. Expects tablular text. */
- (BOOL)tableView:(NSTableView *)aTableView acceptDrop:(id <NSDraggingInfo>)info row:(int)row dropOperation:(NSTableViewDropOperation)operation
{
if ([info draggingSource] == aTableView) // intra-document
{
NSArray * copyOfItemsDuringDrag = [[NSArray alloc] initWithArray:_pairsDuringDrag copyItems:YES];
NSIndexSet * indexes = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(row, [copyOfItemsDuringDrag count])];
[arrayController insertObjects:copyOfItemsDuringDrag atArrangedObjectIndexes:indexes];
if ([info draggingSource] == aTableView)
[arrayController removeObjects:_pairsDuringDrag];
[copyOfItemsDuringDrag release];
_pairsDuringDrag = nil;
}
else // inter-document, inter-application
{
NSPasteboard * pboard = [info draggingPasteboard];
NSString * string = [pboard stringForType:NSStringPboardType];
if (string == nil)
return NO;
string = [string stringByTrimmingCharactersInSet:[NSCharacterSet whitespaceAndNewlineCharacterSet]];
NSArray * pairs = [GeniusPair pairsFromTabularText:string order:[GeniusDocument columnBindings]];
[arrayController setFilterString:@""];
[arrayController addObjects:pairs];
}
return YES;
}
@end
/*!
@category GeniusDocument(IBActions)
@abstract Collections of methods accessed directly from the GUI.
*/
@implementation GeniusDocument(IBActions)
//! Turns on/off display of the group column.
- (IBAction)toggleGroupColumn:(id)sender
{
[tableView toggleColumnWithIdentifier:@"customGroup"];
}
//! Turns on/off display of the type column.
- (IBAction)toggleTypeColumn:(id)sender
{
[tableView toggleColumnWithIdentifier:@"customType"];
}
//! Turns on/off display of the standard style score column.
- (IBAction)toggleABScoreColumn:(id)sender
{
[tableView toggleColumnWithIdentifier:@"scoreAB"];
}
//! Turns on/off display of the jepardy style score column.
- (IBAction)toggleBAScoreColumn:(id)sender
{
[tableView toggleColumnWithIdentifier:@"scoreBA"];
}
//! shows panel for setting field labels
- (IBAction) showDeckPreferences:(id)sender
{
[NSApp beginSheet:deckPreferences modalForWindow:[self windowForSheet] modalDelegate:nil didEndSelector:nil contextInfo:nil];
}
//! Puts away deck preferences sheet.
- (IBAction) endDeckPreferences: (id) sender
{
[deckPreferences orderOut:self];
[NSApp endSheet:deckPreferences returnCode:1];
}
//! Toggles open state of info drawer at default window edge.
- (IBAction)showInfo:(id)sender
{
[infoDrawer toggle:sender];
}
//! Toggles open state of notes drawer at bottom of window.
- (IBAction)showNotes:(id)sender
{
if ([notesDrawer state] == NSDrawerOpenState)
[notesDrawer close];
else
[notesDrawer openOnEdge:NSMinYEdge];
}
//! FirstResponder wrapper for arrayController insert:.
- (IBAction) add:(id)sender
{
// In case user is typing in table view.
[[tableView window] endEditingFor:nil];
// Isolate this action it its own undo group.
NSUndoManager *undoManager = [self undoManager];
if([undoManager groupingLevel] > 0)
{
[undoManager endUndoGrouping];
[undoManager beginUndoGrouping];
}
unsigned int count = [[arrayController arrangedObjects] count];
[arrayController insertObject:[[arrayController newObject] autorelease]
atArrangedObjectIndex:count];
[tableView editColumn: 1 row:count withEvent:nil select:YES];
[undoManager setActionName:@"Insert Pair"];
}
//! FirstResponder wrapper for arrayController remove:.
- (IBAction) delete:(id)sender
{
// In case user is typing in table view.
[[tableView window] endEditingFor:nil];
// Isolate this action it its own undo group.
NSUndoManager *undoManager = [self undoManager];
if([undoManager groupingLevel] > 0)
{
[undoManager endUndoGrouping];
[undoManager beginUndoGrouping];
}
[arrayController remove:sender];
[undoManager setActionName:@"Delete Pair"];
}
//! Duplicates the selected items and inserts them in the document.
- (IBAction) duplicate:(id)sender
{
NSArray * selectedObjects = [arrayController selectedObjects];
NSIndexSet * selectionIndexes = [arrayController selectionIndexes];
int lastIndex = [selectionIndexes lastIndex];
NSIndexSet * indexSet = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(lastIndex+1, [selectionIndexes count])];
NSArray * newObjects = [[NSArray alloc] initWithArray:selectedObjects copyItems:YES];
[arrayController insertObjects:newObjects atArrangedObjectIndexes:indexSet];
[arrayController setSelectedObjects:newObjects];
[newObjects release];
}
//! Initiates modal sheet to check if the user really wants to reset selected items.
- (IBAction)resetScore:(id)sender
{
NSArray * selectedObjects = [arrayController selectedObjects];
if ([selectedObjects count] == 0)
return;
NSString * title = NSLocalizedString(@"Are you sure you want to reset the items?", nil);
NSString * message = NSLocalizedString(@"Your performance history will be cleared for the selected items.", nil);
NSString * resetTitle = NSLocalizedString(@"Reset", nil);
NSString * cancelTitle = NSLocalizedString(@"Cancel", nil);
NSAlert * alert = [NSAlert alertWithMessageText:title defaultButton:resetTitle alternateButton:cancelTitle otherButton:nil informativeTextWithFormat:message];
NSButton * defaultButton = [[alert buttons] objectAtIndex:0];
[defaultButton setKeyEquivalent:@"\r"];
[alert beginSheetModalForWindow:[self windowForSheet] modalDelegate:self didEndSelector:@selector(_resetAlertDidEnd:returnCode:contextInfo:) contextInfo:selectedObjects];
}
//! Handles the results of the modal sheet initiated in @a resetScore:.
/*!
In the event the user confirmed the reset action, then the performance statistics for all GeniusPair items
in the this document are deleted.
@todo Check if it makes that much sense to nuke the performance data?
*/
- (void)_resetAlertDidEnd:(NSAlert *)alert returnCode:(int)returnCode contextInfo:(void *)contextInfo
{
if (returnCode == 0)
return;
NSArray * associations = [GeniusPair associationsForPairs:(NSArray *)contextInfo useAB:YES useBA:YES];
[associations makeObjectsPerformSelector:@selector(reset)];
}
//! Sets the importance of the selected items from -1 to 10.
- (IBAction)setItemImportance:(id)sender
{
NSMenuItem * menuItem = (NSMenuItem *)sender;
int importance = [menuItem tag];
NSArray * selectedObjects = [arrayController selectedObjects];
NSEnumerator * objectEnumerator = [selectedObjects objectEnumerator];
GeniusPair * pair;
while ((pair = [objectEnumerator nextObject]))
[pair setImportance:importance];
}
//! Move cursor to search field
- (IBAction) selectSearchField: (id) sender
{
[[_searchField window] makeFirstResponder:_searchField];
}
//! Action invoked from the little find NSTextField
- (IBAction)search:(id)sender
{
[arrayController setFilterString:[sender stringValue]];
[self tableViewSelectionDidChange:nil];
}
//! Actually starts the currenlty configured quiz
/*!
In the event that the users choices have resulted in no items being available, the user is
presented with an alert panel to that effect and the quiz is not begun. In the end the status
text and level indicator are updated
*/
- (void) beginQuiz:(GeniusAssociationEnumerator *)enumerator
{
NSUndoManager *undoManager = [self undoManager];
if([undoManager groupingLevel] > 0)
{
[undoManager endUndoGrouping];
[undoManager beginUndoGrouping];
}
[enumerator performChooseAssociations]; // Pre-perform for progress indication
if ([enumerator remainingCount] == 0)
{
NSString * title = NSLocalizedString(@"There is nothing to study.", nil);
NSString * message = NSLocalizedString(@"Make sure the items you want to study are enabled, or add more items.", nil);
NSString * okTitle = NSLocalizedString(@"OK", nil);
NSAlert * alert = [NSAlert alertWithMessageText:title defaultButton:okTitle alternateButton:nil otherButton:nil informativeTextWithFormat:message];
NSButton * defaultButton = [[alert buttons] objectAtIndex:0];
[defaultButton setKeyEquivalent:@"\r"];
[alert runModal];
}
else {
MyQuizController *quizController = [[MyQuizController alloc] init];
[quizController runQuiz:enumerator];
[quizController release];
}
[self _updateStatusText];
[self _updateLevelIndicator];
[[self undoManager] setActionName:@"Run Quiz"];
}
//! Set up quiz mode using enabled and based probablity.
/*!
Uses the learn review slider value as input to probability based selection process. In the event
that the learn revew slider is set to review, then only previously learned items will appear in the quiz.
Tries to setup the review for 13 items.
*/
- (IBAction)quizAutoPick:(id)sender
{
[[tableView window] endEditingFor:nil];
NSArray * associations = [self _enabledAssociationsForPairs:_pairs];
GeniusAssociationEnumerator * enumerator = [[GeniusAssociationEnumerator alloc] initWithAssociations:associations];
[enumerator setCount:13];
/*
0% should be m=0.0 (learn only)
50% should be m=1.0
100% should be minimum=0 (review only)
*/
float weight = [probabilityCenter doubleValue];
if (weight == 100.0)
[enumerator setMinimumScore:0]; // Review only
float m_value = (weight / 100.0) * 2.0;
[enumerator setProbabilityCenter:m_value];
[self beginQuiz:enumerator];
}
//! Set up quiz mode using enabled and previously learned GeniusPair items.
/*!
Sets the minimum required score on the GeniusAssociationEnumerator to 0 which
precludes pairs with uninitialized score values. @see GeniusAssociation#score
*/
- (IBAction)quizReview:(id)sender
{
[[tableView window] endEditingFor:nil];
NSArray * associations = [self _enabledAssociationsForPairs:_pairs];
GeniusAssociationEnumerator * enumerator = [[GeniusAssociationEnumerator alloc] initWithAssociations:associations];
[enumerator setCount:13];
[enumerator setMinimumScore:0];
[self beginQuiz:enumerator];
}
//! Set up quiz mode using the user selected GeniusPair items.
/*! Excludes disabled items */
- (IBAction)quizSelection:(id)sender
{
[[tableView window] endEditingFor:nil];
NSArray * selectedPairs = [arrayController selectedObjects];
if ([selectedPairs count] == 0)
selectedPairs = _pairs;
NSArray * associations = [self _enabledAssociationsForPairs:selectedPairs];
GeniusAssociationEnumerator * enumerator = [[GeniusAssociationEnumerator alloc] initWithAssociations:associations];
[self beginQuiz:enumerator];
}
@end
//! Enabled / Disable menu items.
/*! @category GeniusDocument(NSMenuValidation) */
@implementation GeniusDocument(NSMenuValidation)
//! Enable / Disable menu items.
/*!
Handles updates of table colum toggles, auto pick quiz mode, setting of importance, duplication, and reseting.
*/
- (BOOL)validateMenuItem:(id <NSMenuItem>)menuItem
{
SEL action = [menuItem action];
if (action == @selector(toggleABScoreColumn:) || action == @selector(toggleBAScoreColumn:))
{
NSString * titleA = [_columnHeadersDict valueForKey:@"columnA"];
NSString * titleB = [_columnHeadersDict valueForKey:@"columnB"];
if (action == @selector(toggleABScoreColumn:))
{
NSString * format = NSLocalizedString(@"Score (A->B)", nil);
NSString * title = [NSString stringWithFormat:format, titleA, titleB];
title = [@" " stringByAppendingString:title];
[menuItem setTitle:title];
}
else if (action == @selector(toggleBAScoreColumn:))
{
NSString * format = NSLocalizedString(@"Score (A<-B)", nil);
NSString * title = [NSString stringWithFormat:format, titleA, titleB];
title = [@" " stringByAppendingString:title];
[menuItem setTitle:title];
}
}
NSString * chosenColumnIdentifier = nil;
if (action == @selector(toggleGroupColumn:))
chosenColumnIdentifier = @"customGroup";
else if (action == @selector(toggleTypeColumn:))
chosenColumnIdentifier = @"customType";
else if (action == @selector(toggleABScoreColumn:))
chosenColumnIdentifier = @"scoreAB";
else if (action == @selector(toggleBAScoreColumn:))
chosenColumnIdentifier = @"scoreBA";
if (chosenColumnIdentifier)
{
NSArray * visibleColumnIdentifiers = [tableView visibleColumnIdentifiers];
int state = ([visibleColumnIdentifiers containsObject:chosenColumnIdentifier] ? NSOnState : NSOffState);
[menuItem setState:state];
// Make sure at least one score column is always enabled.
if ([chosenColumnIdentifier isEqualToString:@"scoreAB"] &&
[visibleColumnIdentifiers containsObject:@"scoreBA"] == NO)
return NO;
if ([chosenColumnIdentifier isEqualToString:@"scoreBA"] &&
[visibleColumnIdentifiers containsObject:@"scoreAB"] == NO)
return NO;
}
if (action == @selector(quizAutoPick:) || action == @selector(quizReview:))
{
if ([[arrayController arrangedObjects] count] == 0)
return NO;
}
NSArray * selectedObjects = [arrayController selectedObjects];
int selectedCount = [selectedObjects count];
if (action == @selector(duplicate:) || action == @selector(resetScore:) || action == @selector(setItemImportance:)
|| action == @selector(quizSelection:))
{
if (selectedCount == 0)
return NO;
}
if (action == @selector(setItemImportance:))
{
int expectedImportance = [menuItem tag];
int i, actualCount = 0;
for (i=0; i<selectedCount; i++)
{
GeniusPair * pair = [selectedObjects objectAtIndex:i];
if ([pair importance] == expectedImportance)
actualCount++;
}
if (actualCount == 0)
[menuItem setState:NSOffState];
else if (actualCount == selectedCount)
[menuItem setState:NSOnState];
else
[menuItem setState:NSMixedState];
return YES;
}
return [super validateMenuItem:(id)menuItem];
}
@end
@implementation GeniusDocument(NSTableViewDelegate)
- (void)tableView:(NSTableView *)aTableView willDisplayCell:(id)aCell forTableColumn:(NSTableColumn *)aTableColumn row:(int)rowIndex
{
NSString * identifier = [aTableColumn identifier];
if ([identifier isEqualToString:@"scoreAB"] || [identifier isEqualToString:@"scoreBA"])
{
GeniusPair * pair = [[arrayController arrangedObjects] objectAtIndex:rowIndex];
int score;
if ([identifier isEqualToString:@"scoreAB"])
score = [[pair associationAB] score];
else
score = [[pair associationBA] score];
NSImage * image = nil;
if (score == -1)
image = [NSImage imageNamed:@"status-red"];
else if (score < 5)
image = [NSImage imageNamed:@"status-yellow"];
else
image = [NSImage imageNamed:@"status-green"];
[aCell setImage:image];
}
}
- (void)tableViewSelectionDidChange:(NSNotification *)aNotification
{
[self _updateStatusText];
[[infoDrawer contentView] setNeedsDisplay:YES];
}
@end
//! Subclass to handle item filtering
@implementation GeniusArrayController
//! Returns an object initialized from data in the provided @a decoder
- (id)initWithCoder:(NSCoder *)decoder
{
self = [super initWithCoder:decoder];
return self;
}
//! Releases _filterString and frees memory.
- (void) dealloc
{
[_filterString release];
[super dealloc];
}
//! _filterString getter
- (NSString *) filterString
{
return _filterString;
}
//! _filterString setter.
- (void) setFilterString:(NSString *)string
{
[string retain];
[_filterString release];
_filterString = string;
[self rearrangeObjects];
}
//! Returns a given array, appropriately sorted and filtered.
- (NSArray *)arrangeObjects:(NSArray *)objects
{
if ([_filterString length] > 0)
{
NSArray * keyPaths = [GeniusDocument columnBindings];
NSMutableArray * filteredObjects = [NSMutableArray array];
NSEnumerator * pairEnumerator = [objects objectEnumerator];
GeniusPair * pair;
while ((pair = [pairEnumerator nextObject]))
{
//! @todo surely there is a better way to do this. Perhaps use SearchKit?
NSString * tabularText = [pair tabularTextByOrder:keyPaths];
NSRange range = [tabularText rangeOfString:_filterString options:NSCaseInsensitiveSearch];
if (range.location != NSNotFound)
[filteredObjects addObject:pair];
}
return [super arrangeObjects:filteredObjects];
}
else
{
return [super arrangeObjects:objects];
}
}
@end
//! NSComboBox Supporting Methods.
/*! @category GeniusDocument(NSComboBoxDataSource) */
@implementation GeniusDocument(NSComboBoxDataSource)
//! returns index of @a aString from _customTypeStringCache.
- (unsigned int)comboBox:(NSComboBox *)aComboBox indexOfItemWithStringValue:(NSString *)aString
{
return [_sortedCustomTypeStrings indexOfObject:aString];
}
//! returns object at @a index from _customTypeStringCache.
- (id)comboBox:(NSComboBox *)aComboBox objectValueForItemAtIndex:(int)index
{
return [_sortedCustomTypeStrings objectAtIndex:index];
}
//! returns number of items in _customTypeStringCache.
- (int)numberOfItemsInComboBox:(NSComboBox *)aComboBox
{
return [_sortedCustomTypeStrings count];
}
@end
//! NSComboBoxCell supporting methods.
/*! @category GeniusDocument(NSComboBoxCellDataSource) */
@implementation GeniusDocument(NSComboBoxCellDataSource)
//! returns value from _customTypeStringCache.
- (id)comboBoxCell:(NSComboBoxCell *)aComboBoxCell objectValueForItemAtIndex:(int)index
{
return [_sortedCustomTypeStrings objectAtIndex:index];
}
//! count of _customTypeStringCache.
- (int)numberOfItemsInComboBoxCell:(NSComboBoxCell *)aComboBoxCell
{
return [_sortedCustomTypeStrings count];
}
//! Returns index of @a string in _customTypeStringCache.
- (unsigned int)comboBoxCell:(NSComboBoxCell *)aComboBoxCell indexOfItemWithStringValue:(NSString *)string
{
string = [aComboBoxCell stringValue]; // string comes in as (null) for some reason
return [_sortedCustomTypeStrings indexOfObject:string];
}
//! Field completion for the custom type popup.
- (NSString *)comboBoxCell:(NSComboBoxCell *)aComboBoxCell completedString:(NSString*)uncompletedString
{
NSEnumerator * stringEnumerator = [_sortedCustomTypeStrings objectEnumerator];
NSString * string;
while ((string = [stringEnumerator nextObject]))
if ([[string lowercaseString] hasPrefix:[uncompletedString lowercaseString]])
return string;
return nil;
}