1#import <AppKit/AppKit.h>
2
3#import "AddressView/ADPersonView.h"
4#import "Controller.h"
5#import "DragDropMatrix.h"
6
7@interface ADAddressBook (AddressManagerAdditions)
8- (ADPerson*) personWithFirstName: (NSString*) first
9			 lastName: (NSString*) last;
10@end
11
12@implementation ADAddressBook (AddressManagerAdditions)
13- (ADPerson*) personWithFirstName: (NSString*) first
14			 lastName: (NSString*) last
15{
16  NSEnumerator *e; ADPerson *p;
17  e = [[self people] objectEnumerator];
18  while((p = [e nextObject]))
19    if([[p valueForProperty: ADFirstNameProperty]
20	 isEqualToString: first] &&
21       [[p valueForProperty: ADLastNameProperty]
22	 isEqualToString: last])
23      return p;
24  return nil;
25}
26@end
27
28@interface Controller (Private)
29- (void) browserAction: (id) sender;
30@end
31
32@implementation Controller
33- (void) applicationDidFinishLaunching: (NSNotification*) note
34{
35  NSUserDefaults *def;
36  NSString *uid;
37
38  [NSApp registerServicesMenuSendTypes: [NSArray arrayWithObjects:
39						   NSStringPboardType,
40						 nil]
41	 returnTypes: nil];
42
43  servicesMenu = [[[NSApp mainMenu]
44		    itemWithTitle: @"Services"]
45		   submenu];
46  [NSApp setServicesMenu: servicesMenu];
47
48  def = [NSUserDefaults standardUserDefaults];
49  uid = [def stringForKey: @"SelectedGroup"];
50  if(uid && ![uid isEqualToString: @"None"])
51    [self selectGroup: (ADGroup*) [_book recordForUniqueId: uid]];
52  else
53    [self selectGroup: nil];
54
55  uid = [def stringForKey: @"SelectedPerson"];
56  if(uid && ![uid isEqualToString: @"None"])
57    [self selectPerson: (ADPerson*)[_book recordForUniqueId: uid]];
58  else
59    [groupsBrowser selectRow: 0 inColumn: 1];
60
61  [[NSNotificationCenter defaultCenter]
62    addObserver: self
63    selector: @selector(handleDatabaseChanged:)
64    name: ADDatabaseChangedNotification
65    object: nil];
66  [[NSNotificationCenter defaultCenter]
67    addObserver: self
68    selector: @selector(handleDatabaseChangedExternally:)
69    name: ADDatabaseChangedExternallyNotification
70    object: nil];
71  [[NSNotificationCenter defaultCenter]
72    addObserver:self
73    selector: @selector(handleNameChanged:)
74    name: ADPersonNameChangedNotification
75    object: nil];
76}
77
78- (void) awakeFromNib
79{
80  NSString *filename;
81
82  _fm = [NSFileManager defaultManager];
83  _book = [ADAddressBook sharedAddressBook];
84  _selfChanging = NO;
85  _selectedByDrop = NO;
86
87  if(!_book)
88    {
89      NSRunAlertPanel(_(@"No Address Book"),
90		      _(@"[ADAddressBook sharedAddressBook] returned nil.\n"
91			@"Configuration broken?"),
92		      _(@"OK"), nil, nil, nil);
93      exit(-1);
94    }
95
96  [[groupsBrowser window] setFrameAutosaveName: @"Addresses"];
97  [[groupsBrowser window] setFrameUsingName: @"Addresses"];
98  [prefsPanel setFrameAutosaveName: @"Preferences"];
99  [prefsPanel setFrameUsingName: @"Preferences"];
100
101  [groupsBrowser setAllowsEmptySelection: NO];
102  [groupsBrowser setAllowsMultipleSelection: YES];
103  [groupsBrowser setMaxVisibleColumns: 2];
104  [groupsBrowser setDelegate: self];
105  [groupsBrowser setTitle: _(@"Group") ofColumn: 0];
106  [groupsBrowser setTitle: _(@"Name") ofColumn: 1];
107  [groupsBrowser setMatrixClass: [DragDropMatrix class]];
108  [groupsBrowser setTarget: self];
109  [groupsBrowser setAction: @selector(browserAction:)];
110
111  clipView = [[NSClipView alloc] initWithFrame: [scrollView frame]];
112  [clipView setAutoresizesSubviews: YES];
113  personView = [[ADPersonView alloc] initWithFrame: NSZeroRect];
114  [clipView setDocumentView: personView];
115  [personView setFillsSuperview: YES];
116  [personView setForceImage: YES];
117
118  [scrollView setContentView: clipView];
119  [scrollView setHasVerticalScroller: YES];
120  [scrollView setHasHorizontalScroller: YES];
121  [scrollView setBorderType: NSBezelBorder];
122
123  [personView setDelegate: self];
124
125  filename = [[NSBundle mainBundle] pathForResource: @"ISOCountryCodes"
126				    ofType: @"dict"];
127  _countryCodeDict = [[[NSString stringWithContentsOfFile: filename]
128			propertyList] retain];
129  NSAssert(_countryCodeDict, @"ISOCountryCodes.dict could not be loaded.");
130
131  [self createCache];
132  if([_peopleCache count])
133    [self selectPerson: [_peopleCache objectAtIndex: 0]];
134
135  [self initPrefsPanel];
136}
137
138- (void) initPrefsPanel
139{
140  NSUserDefaults *ud;
141  NSEnumerator *e; NSString *key;
142
143  ud = [NSUserDefaults standardUserDefaults];
144  if(![ud objectForKey: @"Autosave"] ||
145     ![[ud objectForKey: @"Autosave"] boolValue])
146    [prefsAutosaveButton setState: NSOffState];
147  else
148    [prefsAutosaveButton setState: NSOnState];
149
150  [prefsAddressLayoutPopup removeAllItems];
151  e = [[[_countryCodeDict allKeys]
152	 sortedArrayUsingSelector: @selector(compare:)]
153	objectEnumerator];
154  while((key = [e nextObject]))
155    {
156      [prefsAddressLayoutPopup
157	addItemWithTitle: [_countryCodeDict objectForKey: key]];
158      [[prefsAddressLayoutPopup
159	 itemWithTitle: [_countryCodeDict objectForKey: key]]
160	setRepresentedObject: key];
161    }
162  [prefsAddressLayoutPopup addItemWithTitle: _(@"Everything")];
163  [[prefsAddressLayoutPopup itemWithTitle: _(@"Everything")]
164    setRepresentedObject: @"Default"];
165  [prefsAddressLayoutPopup sizeToFit];
166
167  if([ud objectForKey: @"DefaultISOCountryCode"])
168    {
169      NSString *def;
170      NSInteger index;
171
172      def = [ud objectForKey: @"DefaultISOCountryCode"];
173      index = [prefsAddressLayoutPopup indexOfItemWithRepresentedObject: def];
174      if(index != NSNotFound)
175	[prefsAddressLayoutPopup selectItemAtIndex: index];
176      [[personView class] setDefaultISOCountryCode: def];
177    }
178
179  if([[ADPerson class] screenNameFormat] == ADScreenNameFirstNameFirst)
180    [prefsScreenNameLayoutMatrix selectCellWithTag: 1];
181  else
182    [prefsScreenNameLayoutMatrix selectCellWithTag: 0];
183}
184
185- (void) createCache
186{
187  SEL sortingSelector;
188
189  sortingSelector =
190    GSSelectorFromNameAndTypes("compareByScreenName:", "i12@0:4@8");
191
192  [_peopleCache release];
193  if(_currentGroup)
194    _peopleCache = [_currentGroup members];
195  else
196    _peopleCache = [_book people];
197  _peopleCache =
198    [[_peopleCache sortedArrayUsingSelector: sortingSelector]
199      retain];
200}
201
202- (NSArray*) groupNames
203{
204  NSArray *groups;
205  NSMutableArray *retval;
206  int i;
207
208  groups = [_book groups];
209  retval = [NSMutableArray arrayWithCapacity: [groups count]];
210
211  for(i=0; i<[groups count]; i++)
212    {
213      NSString *name;
214
215      name = [[groups objectAtIndex: i]
216	       valueForProperty: ADGroupNameProperty];
217      if(!name) NSLog(@"Group at %d has no name!\n", i);
218      else [retval addObject: name];
219    }
220  return retval;
221}
222
223- (void) selectGroup: (ADGroup*) group
224{
225  int num, i;
226
227  if([personView isEditable])
228    [self finishEditingPerson];
229
230  if(!group)
231    {
232      [[groupsBrowser matrixInColumn: 0] deselectAllCells];
233      [groupsBrowser selectRow: 0 inColumn: 0];
234      [_currentGroup autorelease];
235      _currentGroup = nil;
236      [self createCache];
237
238      [groupsBrowser reloadColumn: 1];
239
240      if([_peopleCache count])
241	[self selectPerson: [_peopleCache objectAtIndex: 0]];
242      else
243	[self selectPerson: nil];
244
245      return;
246    }
247
248  num = [[groupsBrowser matrixInColumn: 0] numberOfRows];
249  for(i=0; i<num; i++)
250    {
251      ADRecord *r;
252
253      r = [[groupsBrowser loadedCellAtRow: i column: 0] representedObject];
254      if([[r uniqueId] isEqualToString: [group uniqueId]])
255	{
256	  [[groupsBrowser matrixInColumn: 0] deselectAllCells];
257	  [groupsBrowser selectRow: i inColumn: 0];
258
259	  [_currentGroup autorelease];
260	  _currentGroup = [group retain];
261	  [self createCache];
262
263	  [groupsBrowser reloadColumn: 1];
264
265	  if([_peopleCache count])
266	    [self selectPerson: [_peopleCache objectAtIndex: 0]];
267	  else
268	    [self selectPerson: nil];
269	  return;
270	}
271    }
272
273  NSLog(@"Group %@ not found in column 0!\n", [group uniqueId]);
274}
275
276
277- (void) selectPerson: (ADPerson*) person
278{
279  int i;
280
281  if([personView isEditable])
282    [self finishEditingPerson];
283
284  if(!person)
285    {
286      [personView setPerson: nil];
287      return;
288    }
289
290  for(i=0; i<[_peopleCache count]; i++)
291    if([[[_peopleCache objectAtIndex: i] uniqueId]
292	 isEqualToString: [person uniqueId]])
293      break;
294
295  if(i==[_peopleCache count]) // Not found? Select "all" and try again
296    {
297      NSArray *all;
298      SEL sortingSelector;
299
300      sortingSelector =
301	GSSelectorFromNameAndTypes("compareByScreenName:", "i12@0:4@8");
302
303
304      all = [[_book people] sortedArrayUsingSelector: sortingSelector];
305      for(i=0; i<[all count]; i++)
306	{
307	  if([[[all objectAtIndex: i] uniqueId]
308	       isEqualToString: [person uniqueId]])
309	    {
310	      [self selectGroup: nil];
311	      break;
312	    }
313	}
314    }
315
316  if(i==[_peopleCache count]) // Still not found? WEIRD.
317    {
318      NSLog(@"Person %@ not found!\n", [person uniqueId]);
319      return;
320    }
321
322  // HACK to avoid NSBrowser to keep its old selection; we want only
323  // the new one.
324  [groupsBrowser setAllowsMultipleSelection: NO];
325  [groupsBrowser selectRow: i inColumn: 1];
326  [groupsBrowser setAllowsMultipleSelection: YES];
327
328  [personView setPerson: [_peopleCache objectAtIndex: i]];
329  [clipView scrollToPoint: NSZeroPoint];
330
331  if([person readOnly])
332    {
333      [editButton setEnabled: NO];
334      [editItem setEnabled: NO];
335    }
336  else
337    {
338      [editButton setEnabled: YES];
339      [editItem setEnabled: YES];
340    }
341}
342
343- (void) deletePersonAndSelectNext: (ADPerson*) person
344{
345  int row = [groupsBrowser selectedRowInColumn: 1];
346
347  if(_currentGroup)
348    [_currentGroup removeMember: person];
349  else
350    [_book removeRecord: person];
351
352  [self createCache];
353  [groupsBrowser reloadColumn: 1];
354
355  if(![_peopleCache count])
356    [personView setPerson: nil];
357  else if(row >= [_peopleCache count])
358    [self selectPerson: [_peopleCache objectAtIndex: [_peopleCache count]-1]];
359  else
360    [self selectPerson: [_peopleCache objectAtIndex: row]];
361}
362
363- (void) beginEditingPerson: (ADPerson*) person
364{
365  if([person readOnly])
366    {
367      NSRunAlertPanel(_(@"Read-Only Person"),
368		      [NSString stringWithFormat:
369			_(@"'%@' cannot be edited because\n"
370			  @"the record is marked as read-only."), [person screenName]],
371		      _(@"OK"), nil, nil, nil);
372      [editButton setState: NSOffState];
373      [editItem setTitle: _(@"Edit person")];
374      return;
375    }
376  [self selectPerson: person];
377  [personView setEditable: YES];
378  [editItem setTitle: _(@"End editing")];
379  [editButton setState: NSOnState];
380  [clipView scrollToPoint: NSZeroPoint];
381};
382
383- (void) finishEditingPerson
384{
385  ADPerson *p;
386
387  p = [personView person];
388  if(!p || ![personView isEditable]) return;
389
390  if(![p valueForProperty: ADLastNameProperty] ||
391     ![p valueForProperty: ADFirstNameProperty])
392    {
393      if(NSRunAlertPanel(_(@"Discard Person?"),
394			 _(@"The person you have edited has no first or last\n"
395			   @"names. Would you like to discard this person?"),
396			 _(@"Yes"), _(@"No"), nil, nil))
397	{
398	    [personView setEditable: NO];
399	    [self deletePersonAndSelectNext: p];
400	    p = nil;
401	}
402    }
403  [personView setEditable: NO];
404
405  [editItem setTitle: _(@"Edit Person")];
406  [editButton setState: NSOffState];
407
408  if(p)
409    {
410        [groupsBrowser reloadColumn: 1];
411	[self selectPerson: p];
412    }
413}
414
415- (NSArray*) selectedPersons
416{
417  NSMutableArray *arr;
418  NSEnumerator *e;
419  NSCell *c;
420
421  e = [[groupsBrowser selectedCells] objectEnumerator];
422  arr = [NSMutableArray arrayWithCapacity: [[groupsBrowser selectedCells]
423					     count]];
424  while((c = [e nextObject]))
425    if([c representedObject])
426      [arr addObject: [c representedObject]];
427
428  return [NSArray arrayWithArray: arr];
429}
430
431/*
432  Actions
433*/
434
435- (void) doEditPerson: (id) sender
436{
437  ADPerson *p;
438
439  p = [personView person];
440  if(!p) return; // nothing to do
441  if([personView isEditable])
442    [self finishEditingPerson];
443  else
444    [self beginEditingPerson: p];
445  [clipView scrollToPoint: NSZeroPoint];
446}
447
448- (void) doTogglePersonEditable: (id) sender
449{
450  ADPerson *p;
451
452  p = [personView person];
453  if(!p && [sender state] == NSOnState)
454    [self doCreatePerson: sender];
455  else if([sender state] == NSOnState)
456    [self beginEditingPerson: p];
457  else
458    [self finishEditingPerson];
459  [clipView scrollToPoint: NSZeroPoint];
460}
461
462- (void) doCreatePerson: (id) sender
463{
464  BOOL ok;
465  ADPerson *p;
466
467  p = [[ADPerson alloc] init];
468
469  ok = [_book addRecord: p];
470  if(!ok)
471    {
472      NSRunAlertPanel(_(@"Couldn't create person"),
473		      _(@"A new person could not be created."),
474		      _(@"OK"), nil, nil, nil);
475      return;
476    }
477
478  if(_currentGroup)
479    {
480      ok = [_currentGroup addMember: p];
481      if(!ok)
482	{
483	  NSRunAlertPanel(_(@"Couldn't add person"),
484			  _(@"The newly created person could not be\n"
485			    @"added to this group."),
486			  _(@"OK"), nil, nil, nil);
487	  return;
488	}
489    }
490
491  p = (ADPerson*)[_book recordForUniqueId: [p uniqueId]];
492
493  [self createCache];
494  [groupsBrowser reloadColumn: 1];
495  [self beginEditingPerson: p];
496  [personView beginEditingInFirstCell];
497}
498
499- (IBAction) doDeletePerson: (id) sender
500{
501  NSArray *a;
502  NSEnumerator *e;
503  ADPerson *p;
504
505  a = [self selectedPersons];
506  if(![a count]) return; // nothing to do.
507
508  // deleting from "All"? Ask.
509  if(!_currentGroup)
510    {
511      NSString *msg, *cpt;
512      if([a count] == 1)
513	{
514	  msg = [NSString stringWithFormat: _(@"Do you really want to delete %@ "
515					      @"from \"All\" and all groups?"),
516			  [[a objectAtIndex: 0] screenName]];
517	  cpt = _(@"Delete Person?");
518	}
519      else
520	{
521	  msg = [NSString stringWithFormat: _(@"Do you really want to delete "
522					      @"the %d selected persons "
523					      @"from \"All\" and all groups?"),
524			  [a count]];
525	  cpt = _(@"Delete Persons?");
526	}
527      if(!NSRunAlertPanel(cpt, msg,
528			  _(@"Yes"), _(@"No"), nil, nil))
529	{
530	  NSLog(@"Not deleting.\n");
531	  return;
532	}
533    }
534
535  e = [a objectEnumerator];
536  while((p = [e nextObject]))
537    [self deletePersonAndSelectNext: p];
538}
539
540- (void) doImportPerson: (id) sender
541{
542  int retval;
543  id obj;
544  int loaded = 0;
545  NSString *fname;
546  id<ADInputConverting> conv;
547  ADConverterManager *man;
548  NSOpenPanel *p;
549
550  man = [ADConverterManager sharedManager];
551
552  p = [NSOpenPanel openPanel];
553
554  [p setDirectory: [[NSUserDefaults standardUserDefaults]
555		     objectForKey: @"ImportDirectory"]];
556
557  [p setCanChooseDirectories: NO];
558  [p setAllowsMultipleSelection: NO];
559  [p setTitle: _(@"Import...")];
560  retval = [p runModalForTypes: [man inputConvertableFileTypes]];
561  if(!retval)
562    return;
563
564  [[NSUserDefaults standardUserDefaults]
565    setObject: [p directory] forKey: @"ImportDirectory"];
566
567  fname = [[p filenames] objectAtIndex: 0];
568  conv = [man inputConverterWithFile: fname];
569  NSAssert(conv, @"No converter for this file!");
570
571  while((obj = [conv nextRecord]))
572    {
573      NSEnumerator *e = [_peopleCache objectEnumerator];
574      id other;
575      int retval;
576
577      loaded++;
578
579      retval = 0; // insert anyway
580      while((other = [e nextObject]))
581	{
582	  if([[other screenName] isEqualToString: [obj screenName]])
583	    {
584	      NSString *fmt;
585	      fmt =
586		[NSString stringWithFormat:
587			    _(@"Trying to import person named '%@',\n"
588			      @"which already exists in the database."),
589			  [obj screenName]];
590	      retval = NSRunAlertPanel(_(@"Existing person?"),
591				       fmt, _(@"Replace"), _(@"Insert anyway"),
592				       _(@"Don't insert"), nil);
593	      break;
594	    }
595	}
596      if(retval == 1) // replace
597	{
598	  [_book removeRecord: other];
599	  [_book addRecord: obj];
600	}
601      else if(retval == 0) // insert anyway
602	[_book addRecord: obj];
603      else if(retval == -1) // don't insert; continue reading
604	continue;
605    }
606  [groupsBrowser reloadColumn: 1];
607}
608
609- (void) doExportPerson: (id) sender
610{
611  ADConverterManager *man;
612  NSSavePanel *panel;
613  NSString *fname;
614  int retval;
615  NSArray *a;
616  NSEnumerator *e; ADPerson *person;
617  id<ADOutputConverting> conv;
618
619  if([personView isEditable])
620    [self finishEditingPerson];
621
622  a = [self selectedPersons];
623  if(![a count]) return;
624
625  man = [ADConverterManager sharedManager];
626  panel = [NSSavePanel savePanel];
627
628  [panel setDirectory: [[NSUserDefaults standardUserDefaults]
629			 objectForKey: @"ExportDirectory"]];
630  [panel setRequiredFileType: @"vcf"];
631  if([a count] > 1)
632    [panel
633      setTitle: [NSString stringWithFormat: _(@"Export %d records to..."),
634			  [a count]]];
635  else
636    [panel
637      setTitle: [NSString stringWithFormat: _(@"Export '%@' to..."),
638			  [[a objectAtIndex: 0] screenName]]];
639
640
641  retval = [panel runModal];
642  if(!retval)
643    return;
644
645  [[NSUserDefaults standardUserDefaults]
646    setObject: [panel directory]
647    forKey: @"ExportDirectory"];
648
649  fname = [panel filename];
650  conv = [man outputConverterForType: [[fname pathExtension] lowercaseString]];
651  if(!conv)
652    {
653      NSString *msg =
654	[NSString stringWithFormat: _(@"Cannot export files of type %@"),
655		  [fname pathExtension]];
656      NSRunAlertPanel(_(@"Invalid File Type"), msg, _(@"OK"), nil, nil, nil);
657      return;
658    }
659  else if([a count]>1 && ![conv canStoreMultipleRecords])
660    {
661      NSString *msg =
662	[NSString stringWithFormat: _(@"Can only store a single person\n"
663				      @"in files of type %@"),
664		  [fname pathExtension]];
665      NSRunAlertPanel(_(@"Invalid File Type"), msg, _(@"OK"), nil, nil, nil);
666      return;
667    }
668
669  e = [a objectEnumerator];
670  while((person = [e nextObject]))
671    [conv storeRecord: person];
672
673  retval = [[conv string] writeToFile: fname atomically: NO];
674  if(!retval)
675    {
676      NSString *msg =
677	[NSString stringWithFormat: _(@"Could not write file %@.\n"
678				      @"Permissions error?"),
679		  fname];
680      NSRunAlertPanel(_(@"Write Failed"), msg, _(@"OK"), nil, nil, nil);
681    }
682}
683
684- (void) doSetMe: (id) sender
685{
686  if(![personView person]) return;
687  [_book setMe: [personView person]];
688  [personView setNeedsDisplay: YES];
689  [groupsBrowser reloadColumn: 1];
690  [self selectPerson: [personView person]];
691}
692
693- (void) doShowMe: (id) sender
694{
695  if(![_book me]) return;
696  [self selectPerson: [_book me]];
697}
698
699- (void) doSelectAllPersons: (id) sender
700{
701  [groupsBrowser selectAll: self];
702  if([[groupsBrowser selectedCells] count] == 1)
703    [personView setPerson: [[groupsBrowser selectedCellInColumn: 1]
704			     representedObject]];
705  else [personView setPerson: nil];
706}
707
708- (void) doToggleShared: (id) sender
709{
710  NSEnumerator *e;
711  BOOL share;
712  ADPerson *person;
713
714  share = YES; // default yes, but if one selected person is shared,
715	       // set to no
716  e = [[self selectedPersons] objectEnumerator];
717  while((person = [e nextObject]))
718    if([person shared])
719      {
720	share = NO;
721	break;
722      }
723
724  e = [[self selectedPersons] objectEnumerator];
725  while((person = [e nextObject]))
726    [person setShared: share];
727
728  if(share)
729    {
730      if([[self selectedPersons] count] > 1)
731	[shareItem setTitle: _(@"Do not share these people")];
732      else
733	[shareItem setTitle: _(@"Do not share this person")];
734    }
735  else
736    {
737      if([[self selectedPersons] count] > 1)
738	[shareItem setTitle: _(@"Share these people")];
739      else
740	[shareItem setTitle: _(@"Share this person")];
741    }
742
743  [personView setNeedsDisplay: YES];
744}
745
746- (void) doDuplicatePerson: (id) sender
747{
748  ADPerson *newPerson, *oldPerson;
749  BOOL ok;
750
751  oldPerson = [personView person];
752  if(!oldPerson) return;
753
754  newPerson = [oldPerson copy];
755  [newPerson removeValueForProperty: ADFirstNameProperty];
756  [newPerson removeValueForProperty: ADLastNameProperty];
757
758  ok = [_book addRecord: newPerson];
759  if(!ok)
760    {
761      NSRunAlertPanel(_(@"Couldn't create person"),
762		      _(@"A new person could not be created."),
763		      _(@"OK"), nil, nil, nil);
764      return;
765    }
766  if(_currentGroup)
767    {
768      ok = [_currentGroup addMember: newPerson];
769      if(!ok)
770	{
771	  NSRunAlertPanel(_(@"Couldn't add person"),
772			  _(@"The newly created person could not be\n"
773			    @"added to this group."),
774			  _(@"OK"), nil, nil, nil);
775	  return;
776	}
777    }
778
779  [self createCache];
780  [groupsBrowser reloadColumn: 1];
781  [self beginEditingPerson: newPerson];
782  [personView beginEditingInFirstCell];
783}
784
785- (void) doMergePersons: (id) sender
786{
787  // FIXME: Unimplemented!
788}
789
790- (void) doCreateGroup: (id)sender
791{
792  ADGroup *group;
793
794  group = [[[ADGroup alloc] init] autorelease];
795  [group setValue: _(@"New Group") forProperty: ADGroupNameProperty];
796  if(![_book addRecord: group])
797    NSRunAlertPanel(_(@"Error"), _(@"Could not create group!"),
798		    _(@"OK"), nil, nil, nil);
799  [groupsBrowser reloadColumn: 0];
800
801  [self selectGroup: group];
802}
803
804- (void) doDeleteGroup: (id) sender
805{
806  ADGroup *group;
807  NSString *name;
808  NSString *msg;
809  int retval;
810  int row = [groupsBrowser selectedRowInColumn: 0];
811
812  if(row == 0) return; // Can't delete this
813
814  group = [[_book groups] objectAtIndex: row-1];
815  name = [group valueForProperty: ADGroupNameProperty];
816
817  msg =
818    [NSString stringWithFormat: _(@"Do you really want to delete "
819				  @"the group '%@'?"),
820	      name];
821  retval = NSRunAlertPanel(_(@"Delete Group?"), msg,
822			       _(@"Yes"), _(@"No"), nil, nil);
823  if(!retval) return;
824
825  if(![_book removeRecord: group])
826    {
827      msg =
828	[NSString stringWithFormat: _(@"The group '%@` could not be deleted."),
829		  name];
830      NSRunAlertPanel(_(@"Error"), msg, _(@"OK"), nil, nil, nil);
831    }
832
833  lastCell = nil;
834  [groupsBrowser reloadColumn: 0];
835  [groupsBrowser selectRow: row-1 inColumn: 0]; // OK -- we caught 0 above
836  [self browserAction: groupsBrowser];
837  [self createCache];
838  if([_peopleCache count])
839    [self selectPerson: [_peopleCache objectAtIndex: 0]];
840}
841
842- (void) doSaveDatabase: (id) sender
843{
844  if([personView isEditable])
845    [self finishEditingPerson];
846  if([_book hasUnsavedChanges])
847    {
848      _selfChanging = YES;
849
850      if(![_book save])
851	NSRunAlertPanel(_(@"Couldn't save"),
852			_(@"The database could not be saved!"),
853			_(@"OK"), nil, nil, nil);
854      else
855	[[personView window] setTitle: _(@"Addresses")];
856
857      _selfChanging = NO;
858    }
859}
860
861- (void) doShowPrefsPanel: (id) sender
862{
863  [prefsPanel makeKeyAndOrderFront: self];
864}
865
866- (void) prefsToggleAutosave: (id) sender
867{
868  if([sender state] == NSOnState)
869    [[NSUserDefaults standardUserDefaults]
870      setObject: @"YES" forKey: @"Autosave"];
871  else
872    [[NSUserDefaults standardUserDefaults]
873      setObject: @"NO" forKey: @"Autosave"];
874}
875
876- (void) prefsChangeAddressLayout: (id) sender
877{
878  NSString *code, *title; NSEnumerator *e;
879
880  title = [[sender selectedItem] title];
881  e = [_countryCodeDict keyEnumerator];
882  while((code = [e nextObject]))
883    if([[_countryCodeDict objectForKey: code] isEqualToString: title])
884      break;
885  if(!code && [title isEqualToString: _(@"Everything")])
886    code = @"Default";
887  if(!code) return;
888
889  [[NSUserDefaults standardUserDefaults]
890    setObject: code forKey: @"DefaultISOCountryCode"];
891  [[personView class] setDefaultISOCountryCode: code];
892  [personView layout];
893}
894
895- (void) prefsChangeScreenNameLayout: (id) sender
896{
897  int tag;
898  ADPerson *p;
899
900  tag = [sender selectedTag];
901  [[ADPerson class] setScreenNameFormat: tag];
902  // FIXME: Doesn't change in remote address books
903
904  p = [personView person];
905  [self createCache];
906  [groupsBrowser reloadColumn: 1];
907  if(p) [self selectPerson: p];
908}
909
910/*
911 *  Browser delegate methods
912 */
913
914- (int) browser: (NSBrowser*) sender
915numberOfRowsInColumn: (int) column
916{
917  NSArray *groupnames = [self groupNames];
918
919  if(column == 0)
920    return [groupnames count]+1;
921  else
922    {
923      NSCell *cell;
924      NSString *oldName = @"";
925      NSString *newName = @"";
926      ADGroup *group = nil;
927
928      cell = [sender selectedCellInColumn: 0];
929
930      if([sender selectedRowInColumn: 0] != 0)
931	{
932	  group =
933	    [[_book groups] objectAtIndex: [sender selectedRowInColumn: 0]-1];
934	  oldName = [group valueForProperty: ADGroupNameProperty];
935	  newName = [cell stringValue];
936	}
937
938      // stop editing; rename if necessary
939      [lastCell setEditable: NO];
940      if(cell == lastCell && ![oldName isEqualToString: newName])
941	{
942	  if([newName isEqualToString: _(@"All")])
943	    {
944	      [cell setStringValue: oldName];
945	      NSRunAlertPanel(_(@"Disallowed"),
946			      _(@"You cannot rename a group to \"All\",\n"
947				@"since that name is reserved by the system."),
948			      _(@"OK"), nil, nil, nil);
949	    }
950	  else if([groupnames containsObject: newName])
951	    {
952	      [cell setStringValue: oldName];
953	      NSRunAlertPanel(_(@"Disallowed"),
954			      [NSString stringWithFormat:
955					  _(@"You cannot rename this group "
956					    @"to \"%@\",\n"
957					    @"since a group of that name "
958					    @"already exists."), newName],
959			      _(@"OK"), nil, nil, nil);
960	    }
961	  else
962	    [group setValue: newName forProperty: ADGroupNameProperty];
963	  [lastCell setEditable: NO];
964	}
965
966      if(![newName isEqualToString: _(@"All")] &&
967	 ![oldName isEqualToString: _(@"All")] &&
968	 ![[cell stringValue] isEqualToString: _(@"All")])
969	{
970	  if([cell isEditable] || _selectedByDrop)
971	    [cell setEditable: NO];
972	  else
973	    [cell setEditable: YES];
974
975	  _selectedByDrop = NO;
976
977	  [lastCell release];
978	  lastCell = [cell retain];
979	}
980
981      return [_peopleCache count];
982    }
983
984  return 0;
985}
986
987- (void) browser: (NSBrowser*) sender
988 willDisplayCell: (id) cell
989	   atRow: (int) row
990	  column: (int) column
991{
992  [cell setFont: [NSFont systemFontOfSize: [NSFont systemFontSize]]];
993  if(column == 0)
994    {
995      if(row == 0)
996	[cell setStringValue: _(@"All")];
997      else
998	{
999	  [cell setStringValue: [[self groupNames] objectAtIndex: row-1]];
1000	  [cell setRepresentedObject: [[_book groups] objectAtIndex: row-1]];
1001	  [cell setEditable: NO];
1002	}
1003    }
1004  else
1005    {
1006      ADPerson *p = [_peopleCache objectAtIndex: row];
1007      if(p == [_book me])
1008	[cell setStringValue: [[p screenName]
1009				stringByAppendingString: _(@" (Me)")]];
1010      else
1011	[cell setStringValue: [p screenName]];
1012      [cell setRepresentedObject: p];
1013      [cell setLeaf: YES];
1014    }
1015}
1016
1017- (void) browserAction: (id) sender
1018{
1019  if([personView isEditable])
1020    {
1021      [personView setPerson: nil];
1022      [personView setEditable: NO];
1023      [editButton setState: NSOffState];
1024    }
1025
1026  if([sender selectedColumn] == 0)
1027    {
1028      int row;
1029      ADGroup *group = nil;
1030      [_currentGroup release];
1031      _currentGroup = nil;
1032
1033      row = [sender selectedRowInColumn: 0];
1034      if(row != 0)
1035	group = [[_book groups] objectAtIndex: row-1];
1036      if(![[group uniqueId] isEqualToString: [_currentGroup uniqueId]])
1037	{
1038	  _currentGroup = [group retain];
1039	  [self createCache];
1040	  [sender reloadColumn: 1];
1041	  if([_peopleCache count])
1042	    [self selectPerson: [_peopleCache objectAtIndex: 0]];
1043	  else
1044	    [self selectPerson: nil];
1045	}
1046    }
1047  else
1048    {
1049      NSEnumerator *e; ADPerson *p; BOOL shared;
1050
1051      if([[sender selectedCells] count] == 1)
1052	[personView setPerson: [[sender selectedCellInColumn: 1]
1053				 representedObject]];
1054      else [personView setPerson: nil];
1055
1056      shared = NO;
1057      e = [[self selectedPersons] objectEnumerator];
1058      while((p = [e nextObject]))
1059	if([p shared])
1060	  {
1061	    shared = YES;
1062	    break;
1063	  }
1064
1065      if(shared)
1066	{
1067	  if([[self selectedPersons] count] > 1)
1068	    [shareItem setTitle: _(@"Do not share these people")];
1069	  else
1070	    [shareItem setTitle: _(@"Do not share this person")];
1071	}
1072      else
1073	{
1074	  if([[self selectedPersons] count] > 1)
1075	    [shareItem setTitle: _(@"Share these people")];
1076	  else
1077	    [shareItem setTitle: _(@"Share this person")];
1078	}
1079    }
1080}
1081
1082- (void) handleDatabaseChanged: (NSNotification*) note
1083{
1084  if([_book hasUnsavedChanges])
1085    {
1086      if([prefsAutosaveButton state] == NSOnState)
1087	{
1088	  _selfChanging = YES;
1089	  [_book save];
1090	  _selfChanging = NO;
1091	  [[personView window] setDocumentEdited: NO];
1092	  [[personView window] setTitle: _(@"Addresses")];
1093	}
1094      else
1095	{
1096	  [[personView window] setDocumentEdited: YES];
1097	  [[personView window] setTitle: _(@"Addresses*")];
1098	}
1099    }
1100  else
1101    {
1102      [[personView window] setDocumentEdited: NO];
1103      [[personView window] setTitle: _(@"Addresses")];
1104    }
1105  [self createCache];
1106}
1107
1108- (void) handleDatabaseChangedExternally: (NSNotification*) note
1109{
1110  NSString *guid = nil, *uid = nil;
1111
1112  if(_selfChanging) return;
1113
1114  if(_currentGroup)
1115    guid = [[_currentGroup uniqueId] retain];
1116  if([personView person])
1117    uid = [[[personView person] uniqueId] retain];
1118
1119  [self createCache];
1120  [groupsBrowser reloadColumn: 0];
1121  [groupsBrowser reloadColumn: 1];
1122
1123  [self selectGroup: (ADGroup*)[_book recordForUniqueId: guid]];
1124  if(uid)
1125    {
1126      ADPerson *p = (ADPerson*)[_book recordForUniqueId: uid];
1127      if(p) [self selectPerson: p];
1128    }
1129}
1130
1131- (void) handleNameChanged: (NSNotification*) note
1132{
1133  NSDictionary *userInfo; ADPerson *p;
1134  NSString *first, *last, *prop, *val, *scrName;
1135  ADScreenNameFormat fmt;
1136
1137  if([note object] != [personView person])
1138    return;
1139
1140  userInfo = [note userInfo];
1141  p = [note object];
1142
1143  prop = [userInfo objectForKey: @"Property"];
1144  val = [userInfo objectForKey: @"Value"];
1145  if([prop isEqualToString: ADFirstNameProperty])
1146    {
1147      first = val;
1148      last = [p valueForProperty: ADLastNameProperty];
1149    }
1150  else
1151    {
1152      first = [p valueForProperty: ADFirstNameProperty];
1153      last = val;
1154    }
1155
1156  if([first isEmptyString]) first = nil;
1157  if([last isEmptyString]) last = nil;
1158
1159  fmt = [[p class] screenNameFormat];
1160
1161  if(!last && !first) scrName = _(@"New Person");
1162  else if(!first) scrName = last;
1163  else if(!last) scrName = first;
1164  else if(fmt == ADScreenNameFirstNameFirst)
1165    scrName = [NSString stringWithFormat: @"%@ %@", first, last];
1166  else
1167    scrName = [NSString stringWithFormat: @"%@, %@", last, first];
1168
1169  [[groupsBrowser selectedCellInColumn: 1] setStringValue: scrName];
1170  [groupsBrowser setNeedsDisplay: YES];
1171}
1172
1173- (BOOL) application: (NSApplication*) app
1174	    openFile: (NSString*) filename
1175{
1176  id conv;
1177  ADRecord *r, *r1;
1178
1179  conv = [[ADConverterManager sharedManager] inputConverterWithFile: filename];
1180  if(!conv) return NO;
1181
1182  r1 = nil;
1183  while((r = [conv nextRecord]))
1184    {
1185      if(!r1) r1 = r;
1186      [_book addRecord: r];
1187    }
1188  if(!r1) return NO;
1189
1190  [self createCache];
1191  [groupsBrowser reloadColumn: 0];
1192  [groupsBrowser reloadColumn: 1];
1193  if([r1 isKindOfClass: [ADPerson class]])
1194    {
1195      [self selectGroup: _currentGroup];
1196      [self selectPerson: (ADPerson*)r1];
1197    }
1198  else
1199    {
1200      [self selectGroup: (ADGroup*)r1];
1201      if(![_peopleCache count]) [self selectPerson: nil];
1202      else [self selectPerson: [_peopleCache objectAtIndex: 0]];
1203    }
1204
1205  return YES;
1206}
1207
1208- (NSApplicationTerminateReply) applicationShouldTerminate: (NSApplication*) app
1209{
1210  NSUserDefaults *def;
1211
1212  if([personView isEditable])
1213    [self finishEditingPerson];
1214
1215  // store current group and person in defaults
1216  def = [NSUserDefaults standardUserDefaults];
1217  if(_currentGroup)
1218    [def setObject: [_currentGroup uniqueId]
1219	 forKey: @"SelectedGroup"];
1220  else
1221    [def setObject: @"None" forKey: @"SelectedGroup"];
1222
1223  if([personView person])
1224    [def setObject: [[personView person] uniqueId]
1225	 forKey: @"SelectedPerson"];
1226  else
1227    [def setObject: @"None" forKey: @"SelectedPerson"];
1228
1229
1230  if([_book hasUnsavedChanges])
1231    {
1232      int retval =
1233	NSRunAlertPanel(_(@"Save Changes?"),
1234			_(@"You have made changes to the database.\n"
1235			  @"Should these changes be saved?"),
1236			_(@"Save and Quit"), _(@"Quit without saving"),
1237			_(@"Don't quit"), nil);
1238      switch(retval)
1239	{
1240	case 1:
1241	  if(![_book save])
1242	    {
1243	      NSRunAlertPanel(_(@"Couldn't save"),
1244			      _(@"The database could not be saved!"),
1245			      _(@"OK"), nil, nil, nil);
1246	      return NSTerminateCancel;
1247	    }
1248	  return NSTerminateNow;
1249	case 0:
1250	  return NSTerminateNow;
1251	default:
1252	  return NSTerminateCancel;
1253	}
1254    }
1255  else
1256    return NSTerminateNow;
1257}
1258
1259- (BOOL) validateMenuItem: (NSMenuItem*) anItem
1260{
1261  int count;
1262
1263  count = [[self selectedPersons] count];
1264
1265  if(anItem == editItem || anItem == duplicatePersonItem ||
1266     anItem == thisIsMeItem)
1267    {
1268      if(count != 1) return NO;
1269      return YES;
1270    }
1271
1272  if(anItem == shareItem)
1273    {
1274      if(count < 1) return NO;
1275      return YES;
1276    }
1277
1278  if(anItem == mergePersonsItem)
1279    {
1280      if(count <= 1) return NO;
1281      return YES;
1282    }
1283
1284  return YES;
1285}
1286
1287- (BOOL) personView: (ADPersonView*) aView
1288   shouldAcceptDrop: (id<NSDraggingInfo>) info
1289{
1290  return YES;
1291}
1292
1293- (BOOL) personView: (ADPersonView*) aView
1294receivedDroppedPersons: (NSArray*) persons
1295{
1296  int i;
1297
1298  if(![persons count]) return NO;
1299
1300  for(i=0; i<[persons count]; i++)
1301    {
1302      ADPerson *p, *o;
1303
1304      p = [persons objectAtIndex: i];
1305      o = [_book personWithFirstName: [p valueForProperty: ADFirstNameProperty]
1306		 lastName: [p valueForProperty: ADLastNameProperty]];
1307      if(o)
1308	{
1309	  NSString *fmt;
1310	  int retval;
1311
1312	  fmt =
1313	    [NSString stringWithFormat:
1314			_(@"Trying to import person named '%@',\n"
1315			  @"which already exists in the database."),
1316		      [p screenName]];
1317	  retval = NSRunAlertPanel(_(@"Existing person?"),
1318				   fmt, _(@"Replace"), _(@"Insert anyway"),
1319				   _(@"Don't insert"), nil);
1320	  if(retval == 1) // replace
1321	    {
1322	      [_book removeRecord: o];
1323	      [_book addRecord: p];
1324	    }
1325	  else if(retval == 0) // insert anyway
1326	    [_book addRecord: p];
1327	  else if(retval == -1) // don't insert; continue reading
1328	    return NO;
1329	}
1330      else
1331	if(![_book addRecord: p]) return NO;
1332
1333      if(_currentGroup)
1334	[_currentGroup addMember: p];
1335    }
1336
1337  [groupsBrowser reloadColumn: 1];
1338  [self selectPerson: [persons objectAtIndex: 0]];
1339
1340  return YES;
1341}
1342
1343- (BOOL) personView: (ADPersonView*) aView
1344     willDragPerson: (ADPerson*) person
1345{
1346  return YES;
1347}
1348
1349- (BOOL) personView: (ADPersonView*) aView
1350   willDragProperty: (NSString*) property
1351{
1352  return YES;
1353}
1354
1355- (NSDragOperation) dragDropMatrix: (DragDropMatrix*) matrix
1356	shouldAcceptDropFromSender: (id<NSDraggingInfo>) sender
1357			    onCell: (NSCell*) cell
1358{
1359  ADGroup *g;
1360
1361  g = [cell representedObject];
1362  if(g && ![g isKindOfClass: [ADGroup class]])
1363    return NSDragOperationNone;
1364
1365  if([[[sender draggingPasteboard] types]
1366       containsObject: ADPeoplePboardType])
1367    {
1368      NSDictionary *d; NSEnumerator *e;
1369      BOOL _pureLocal, _didSomeWork;
1370
1371      if(![cell representedObject] ||
1372	 ![[cell representedObject] isKindOfClass: [ADGroup class]] ||
1373	 [[[cell representedObject] uniqueId]
1374	   isEqualToString: [_currentGroup uniqueId]])
1375	goto useVCard;
1376
1377      _pureLocal = YES;
1378      _didSomeWork = NO;
1379      e = [[[sender draggingPasteboard]
1380	     propertyListForType: ADPeoplePboardType] objectEnumerator];
1381      while((d = [e nextObject]))
1382	{
1383	  int pid; NSString *uid; NSArray *adDescr;
1384	  ADPerson *p;
1385
1386	  if(![d objectForKey: @"UID"] ||
1387	     ![d objectForKey: @"AB"] ||
1388	     ![d objectForKey: @"PID"])
1389	    continue;
1390
1391	  pid = [[d objectForKey: @"PID"] intValue];
1392	  uid = [d objectForKey: @"UID"];
1393	  adDescr = [d objectForKey: @"AD"];
1394
1395	  p = (ADPerson*)[_book recordForUniqueId: uid];
1396
1397	  if(pid != [[NSProcessInfo processInfo] processIdentifier])
1398	    _pureLocal = NO;
1399	  else if(!p || ![p isKindOfClass: [ADPerson class]])
1400	    continue;
1401
1402	  if(g)
1403	    {
1404	      ADPerson *p2; NSEnumerator *e;
1405	      BOOL isIn;
1406
1407	      isIn = NO;
1408	      e = [[g members] objectEnumerator];
1409	      while((p2 = [e nextObject]))
1410		if([[p2 uniqueId] isEqualToString: [p uniqueId]])
1411		  {
1412		    isIn = YES;
1413		    break;
1414		  }
1415
1416	      if(isIn) continue;
1417	    }
1418
1419	  _didSomeWork = YES;
1420	}
1421
1422      if(_didSomeWork)
1423	{
1424	  if(_pureLocal)
1425	    return NSDragOperationLink;
1426	  return NSDragOperationCopy;
1427	}
1428    }
1429
1430 useVCard:
1431  if([[[sender draggingPasteboard] types]
1432       containsObject: @"NSVCardPboardType"])
1433    return NSDragOperationCopy;
1434
1435  return NSDragOperationNone;
1436}
1437
1438- (BOOL) dragDropMatrix: (DragDropMatrix*) matrix
1439didAcceptDropFromSender: (id<NSDraggingInfo>) sender
1440		 onCell: (NSCell*) cell
1441{
1442  NSPasteboard *pb;
1443  ADGroup *g;
1444
1445  g = [cell representedObject];
1446  if(g && ![g isKindOfClass: [ADGroup class]])
1447    return NO;
1448
1449  pb = [sender draggingPasteboard];
1450
1451  if([[pb types] containsObject: ADPeoplePboardType])
1452    {
1453      NSDictionary *d; NSEnumerator *e;
1454      BOOL _didSomeWork;
1455
1456      if(!g) goto useVCard;
1457
1458      _didSomeWork = NO;
1459      e = [[pb propertyListForType: ADPeoplePboardType] objectEnumerator];
1460      while((d = [e nextObject]))
1461	{
1462	  int pid; NSString *uid; NSArray *adDescr;
1463	  ADPerson *p;
1464
1465	  if(![d objectForKey: @"UID"] ||
1466	     ![d objectForKey: @"AB"] ||
1467	     ![d objectForKey: @"PID"])
1468	    continue;
1469
1470	  pid = [[d objectForKey: @"PID"] intValue];
1471	  uid = [d objectForKey: @"UID"];
1472	  adDescr = [d objectForKey: @"AD"];
1473
1474	  if(pid != [[NSProcessInfo processInfo] processIdentifier])
1475	    continue;
1476
1477	  p = (ADPerson*)[_book recordForUniqueId: uid];
1478	  if(!p || ![p isKindOfClass: [ADPerson class]])
1479	    continue;
1480
1481	  if([g addMember: p])
1482	    _didSomeWork = YES;
1483	}
1484      if(_didSomeWork)
1485	return YES;
1486    }
1487
1488 useVCard:
1489  if([[pb types] containsObject: @"NSVCardPboardType"])
1490    {
1491      ADPerson *p, *o;
1492      NSData *vcard;
1493
1494      vcard = [pb dataForType: @"NSVCardPboardType"];
1495      p = [[[ADPerson alloc] initWithVCardRepresentation: vcard] autorelease];
1496      if(!p)
1497	return NO;
1498
1499      o = [_book personWithFirstName: [p valueForProperty: ADFirstNameProperty]
1500		 lastName: [p valueForProperty: ADLastNameProperty]];
1501      if(o)
1502	{
1503	  NSString *fmt;
1504	  int retval;
1505
1506	  fmt =
1507	    [NSString stringWithFormat:
1508			_(@"Trying to import person named '%@',\n"
1509			  @"which already exists in the database."),
1510		      [p screenName]];
1511	  retval = NSRunAlertPanel(_(@"Existing person?"),
1512				   fmt, _(@"Replace"), _(@"Insert anyway"),
1513				   _(@"Don't insert"), nil);
1514	  if(retval == 1) // replace
1515	    {
1516	      [_book removeRecord: o];
1517	      [_book addRecord: p];
1518	    }
1519	  else if(retval == 0) // insert anyway
1520	    [_book addRecord: p];
1521	  else if(retval == -1) // don't insert; continue reading
1522	    return NO;
1523	}
1524      else
1525	if(![_book addRecord: p]) return NO;
1526
1527      p = (ADPerson*)[_book recordForUniqueId: [p uniqueId]];
1528      if(!p || ![p isKindOfClass: [ADPerson class]]) return NO;
1529
1530      if(g)
1531	[g addMember: p];
1532
1533      [groupsBrowser reloadColumn: 1];
1534      if(g)
1535	{
1536	  [self selectGroup: g];
1537	  _selectedByDrop = YES;
1538	}
1539      [self selectPerson: p];
1540
1541      return YES;
1542    }
1543
1544  return NO;
1545}
1546@end
1547
1548