PageRenderTime 251ms CodeModel.GetById 16ms app.highlight 223ms RepoModel.GetById 1ms app.codeStats 1ms

/core/externals/update-engine/externals/gdata-objectivec-client/Examples/ContactsSample/ContactsSampleWindowController.m

http://macfuse.googlecode.com/
Objective C | 1853 lines | 1195 code | 439 blank | 219 comment | 220 complexity | 69353c07ca10f45952e4b611a715ea22 MD5 | raw file

Large files files are truncated, but you can click here to view the full file

   1/* Copyright (c) 2008 Google Inc.
   2 *
   3 * Licensed under the Apache License, Version 2.0 (the "License");
   4 * you may not use this file except in compliance with the License.
   5 * You may obtain a copy of the License at
   6 *
   7 *     http://www.apache.org/licenses/LICENSE-2.0
   8 *
   9 * Unless required by applicable law or agreed to in writing, software
  10 * distributed under the License is distributed on an "AS IS" BASIS,
  11 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12 * See the License for the specific language governing permissions and
  13 * limitations under the License.
  14 */
  15
  16//
  17//  ContactsSampleWindowController.m
  18//
  19
  20#import "ContactsSampleWindowController.h"
  21
  22#import "EditEntryWindowController.h"
  23
  24#import "GData/GDataContacts.h"
  25
  26static const unichar kBallotX = 0x2717; // fancy X mark to indicate deleted items
  27
  28// use a category on the Contact entry so we can refer to the display
  29// name string in a sort descriptor
  30@interface GDataEntryContact (ContactsSampleAdditions)
  31- (NSString *)entryDisplayName;
  32@end
  33
  34@interface GDataEntryContactGroup (ContactsSampleAdditions)
  35- (NSString *)entryDisplayName;
  36@end
  37
  38@interface ContactsSampleWindowController (PrivateMethods)
  39- (void)updateUI;
  40- (void)updateImageForContact:(GDataEntryContact *)contact;
  41
  42- (void)fetchAllGroupsAndContacts;
  43- (void)fetchAllContacts;
  44
  45- (void)addAnItem;
  46- (void)editSelectedItem;
  47- (void)deleteSelectedItem;
  48- (void)makeSelectedItemPrimary;
  49
  50- (void)setContactImage;
  51- (void)deleteContactImage;
  52- (void)setSelectedContactPhotoAtPath:(NSString *)photoPath;
  53
  54- (void)addAContact;
  55- (void)addAGroup;
  56- (void)deleteSelectedContactOrGroup;
  57- (void)deleteAllContactsOrGroups;
  58
  59- (GDataServiceGoogleContact *)contactService;
  60- (GDataEntryContact *)selectedContact;
  61- (GDataEntryContactGroup *)selectedGroup;
  62- (GDataObject *)selectedItem;
  63
  64- (int)selectedSegment;
  65- (NSArray *)itemsForSelectedSegment;
  66
  67- (NSString *)displayNameForItem:(id)item;
  68
  69- (GDataFeedContact *)contactFeed;
  70- (void)setContactFeed:(GDataFeedContact *)feed;
  71- (NSError *)contactFetchError;
  72- (void)setContactFetchError:(NSError *)error;  
  73- (GDataServiceTicket *)contactFetchTicket;
  74- (void)setContactFetchTicket:(GDataServiceTicket *)ticket;
  75- (NSString *)contactImageETag;
  76- (void)setContactImageETag:(NSString *)str;
  77
  78- (GDataFeedContactGroup *)groupFeed;
  79- (void)setGroupFeed:(GDataFeedContactGroup *)feed;
  80- (NSError *)groupFetchError;
  81- (void)setGroupFetchError:(NSError *)error;  
  82- (GDataServiceTicket *)groupFetchTicket;
  83- (void)setGroupFetchTicket:(GDataServiceTicket *)ticket;
  84@end
  85
  86enum {
  87  kContactsSegment = 0,
  88  kGroupsSegment = 1
  89};
  90
  91enum {
  92  kOrganizationSegment = 0,
  93  kEmailSegment = 1,
  94  kPhoneSegment = 2,
  95  kPostalSegment = 3,
  96  kIMSegment = 4,
  97  kGroupSegment = 5,
  98  kExtendedPropsSegment = 6
  99};
 100
 101@implementation ContactsSampleWindowController
 102
 103static ContactsSampleWindowController* gContactsSampleWindowController = nil;
 104
 105
 106+ (ContactsSampleWindowController *)sharedContactsSampleWindowController {
 107  
 108  if (!gContactsSampleWindowController) {
 109    gContactsSampleWindowController = [[[self class] alloc] init];
 110  }  
 111  return gContactsSampleWindowController;
 112}
 113
 114
 115- (id)init {
 116  return [self initWithWindowNibName:@"ContactsSampleWindow"];
 117}
 118
 119- (void)windowDidLoad {
 120}
 121
 122- (void)awakeFromNib {
 123  // Set the result text fields to have a distinctive color and mono-spaced font
 124  // to aid in understanding of each contact query operation.
 125  [mFeedResultTextField setTextColor:[NSColor darkGrayColor]];
 126  [mEntryResultTextField setTextColor:[NSColor darkGrayColor]];
 127  
 128  NSFont *resultTextFont = [NSFont fontWithName:@"Monaco" size:9];
 129  [mFeedResultTextField setFont:resultTextFont];
 130  [mEntryResultTextField setFont:resultTextFont];
 131  
 132  [self updateUI];
 133}
 134
 135- (void)dealloc {
 136  [mContactFeed release];
 137  [mContactFetchError release];
 138  [mContactFetchTicket release];
 139  [mContactImageETag release];
 140  
 141  [mGroupFeed release];
 142  [mGroupFetchError release];
 143  [mGroupFetchTicket release];
 144  
 145  [super dealloc];
 146}
 147
 148#pragma mark -
 149
 150- (BOOL)isDisplayingContacts {
 151  BOOL flag = ([mFeedSegmentedControl selectedSegment] == kContactsSegment); 
 152  return flag;
 153}
 154
 155- (void)updateSegmentedControlLabels {
 156  BOOL isDisplayingContacts = [self isDisplayingContacts];
 157  
 158  // put the number of each type of segment in the label for the item
 159  
 160  // feed segments
 161  
 162  int numContacts = [[mContactFeed entries] count];
 163  NSString *contactsLabel = [NSString stringWithFormat:@"Contacts - %d", numContacts];
 164  [mFeedSegmentedControl setLabel:contactsLabel forSegment:kContactsSegment];
 165  
 166  int numGroups = [[mGroupFeed entries] count];
 167  NSString *groupsLabel = [NSString stringWithFormat:@"Groups - %d", numGroups];
 168  [mFeedSegmentedControl setLabel:groupsLabel forSegment:kGroupsSegment];
 169  
 170  // entry segments
 171  //
 172  // when contacts are displayed, all segments are enabled; when groups
 173  // are displayed, only extended properties is enabled
 174  
 175  int numOrg = [[[self selectedContact] organizations] count];
 176  NSString *orgLabel = [NSString stringWithFormat:@"Org - %d", numOrg];
 177  [mEntrySegmentedControl setLabel:orgLabel forSegment:kOrganizationSegment];
 178  [mEntrySegmentedControl setEnabled:isDisplayingContacts forSegment:kOrganizationSegment];
 179  
 180  int numEmail = [[[self selectedContact] emailAddresses] count];
 181  NSString *mailLabel = [NSString stringWithFormat:@"E-mail - %d", numEmail];
 182  [mEntrySegmentedControl setLabel:mailLabel forSegment:kEmailSegment];
 183  [mEntrySegmentedControl setEnabled:isDisplayingContacts forSegment:kEmailSegment];
 184  
 185  int numIM = [[[self selectedContact] IMAddresses] count];
 186  NSString *IMlabel = [NSString stringWithFormat:@"IM - %d", numIM];
 187  [mEntrySegmentedControl setLabel:IMlabel forSegment:kIMSegment];
 188  [mEntrySegmentedControl setEnabled:isDisplayingContacts forSegment:kIMSegment];
 189  
 190  int numPhone = [[[self selectedContact] phoneNumbers] count];
 191  NSString *phoneLabel = [NSString stringWithFormat:@"Phone - %d", numPhone];
 192  [mEntrySegmentedControl setLabel:phoneLabel forSegment:kPhoneSegment];
 193  [mEntrySegmentedControl setEnabled:isDisplayingContacts forSegment:kPhoneSegment];
 194  
 195  int numPostal = [[[self selectedContact] structuredPostalAddresses] count];
 196  NSString *postalLabel = [NSString stringWithFormat:@"Postal - %d", numPostal];
 197  [mEntrySegmentedControl setLabel:postalLabel forSegment:kPostalSegment];
 198  [mEntrySegmentedControl setEnabled:isDisplayingContacts forSegment:kPostalSegment];
 199  
 200  int numGroupInfos = [[[self selectedContact] groupMembershipInfos] count];
 201  NSString *groupLabel = [NSString stringWithFormat:@"Group - %d", numGroupInfos];
 202  [mEntrySegmentedControl setLabel:groupLabel forSegment:kGroupSegment];
 203  [mEntrySegmentedControl setEnabled:isDisplayingContacts forSegment:kGroupSegment];
 204  
 205  int numExtProps;
 206  if ([self isDisplayingContacts]) {
 207    numExtProps = [[[self selectedContact] extendedProperties] count];
 208  } else {
 209    numExtProps = [[[self selectedGroup] extendedProperties] count];
 210  }
 211  NSString *extPropsLabel = [NSString stringWithFormat:@"ExtProp - %d", numExtProps];
 212  [mEntrySegmentedControl setLabel:extPropsLabel forSegment:kExtendedPropsSegment];
 213}
 214
 215- (void)updateUI {
 216  BOOL isDisplayingContacts = [self isDisplayingContacts];
 217  
 218  // contact list display
 219  [mFeedTable reloadData]; 
 220  
 221  if ((isDisplayingContacts && mContactFetchTicket != nil)
 222      || (!isDisplayingContacts && mGroupFetchTicket != nil)) {
 223    [mFeedProgressIndicator startAnimation:self];  
 224  } else {
 225    [mFeedProgressIndicator stopAnimation:self];  
 226  }
 227  
 228  GDataEntryContact *selectedContact = [self selectedContact];
 229  GDataEntryContactGroup *selectedGroup = [self selectedGroup];
 230  
 231  if (isDisplayingContacts) {
 232    
 233    // show contact fetch or group fetch result error or the selected contact 
 234    NSString *resultStr = @"";
 235    if (mContactFetchError) {
 236      resultStr = [mContactFetchError description];
 237    } else if (mGroupFetchError) {
 238      resultStr = [mGroupFetchError description];
 239    } else {
 240      if (selectedContact) {
 241        resultStr = [selectedContact description];
 242      }
 243    }
 244    [mFeedResultTextField setString:resultStr];
 245    
 246  } else {
 247    
 248    // show group fetch result error or the selected group 
 249    NSString *resultStr = @"";
 250    if (mGroupFetchError) {
 251      resultStr = [mGroupFetchError description];
 252    } else {
 253      if (selectedGroup) {
 254        resultStr = [selectedGroup description];
 255      }
 256    }
 257    [mFeedResultTextField setString:resultStr];
 258  }
 259  
 260  [self updateImageForContact:selectedContact];
 261  
 262  // the bottom table displays items (orgs, e-mail, postal, IM, or phone
 263  // numbers, groups, extended props) for the selected contact, according to the 
 264  // selected segment for the type of the item
 265  
 266  [mEntryTable reloadData]; 
 267  
 268  // display selected item's description
 269  NSString *entryDesc = @"";
 270  GDataObject *selectedItem = [self selectedItem];
 271  if (selectedItem) {
 272    entryDesc = [selectedItem description];
 273  }
 274  [mEntryResultTextField setString:entryDesc];
 275  
 276  // show the number of items for each segment
 277  [self updateSegmentedControlLabels];
 278  
 279  // enable/disable cancel button depending on if a ticket is outstanding
 280  BOOL isTicketPending = (mContactFetchTicket != nil || mGroupFetchTicket != nil);
 281  [mFeedCancelButton setEnabled:isTicketPending];
 282  
 283  // enable/disable other buttons
 284  BOOL isContactOrGroupSelected = (selectedContact != nil || selectedGroup != nil);
 285  BOOL isItemSelected = ([self selectedItem] != nil);
 286  BOOL canItemBePrimary = [[self selectedItem] respondsToSelector:@selector(isPrimary)];
 287  BOOL isEntrySegmentEnabled = [mEntrySegmentedControl isEnabledForSegment:
 288                                      [mEntrySegmentedControl selectedSegment]];
 289  
 290  [mAddEntryButton setEnabled:(isEntrySegmentEnabled && isContactOrGroupSelected)];
 291  [mDeleteEntryButton setEnabled:isItemSelected];
 292  [mEditEntryButton setEnabled:isItemSelected];
 293  
 294  [mMakePrimaryEntryButton setEnabled:(isItemSelected && canItemBePrimary)];
 295  
 296  [mEntrySegmentedControl setEnabled:isContactOrGroupSelected];
 297  [mDeleteContactButton setEnabled:isContactOrGroupSelected];
 298  
 299  BOOL isFeedAvailable = (mContactFeed != nil || mGroupFeed != nil);
 300  BOOL isAddInfoEntered = ([[mAddTitleField stringValue] length] > 0);
 301  
 302  [mAddContactButton setEnabled:(isAddInfoEntered && isFeedAvailable)];
 303  
 304  BOOL canEditSelectedContactImage = ([selectedContact photoLink] != nil);
 305  [mSetContactImageButton setEnabled:canEditSelectedContactImage];
 306  
 307  BOOL doesContactHaveImage = ([[selectedContact photoLink] ETag] != nil);
 308  [mDeleteContactImageButton setEnabled:(doesContactHaveImage && 
 309                                         canEditSelectedContactImage)];
 310  
 311  [mAddTitleField setEnabled:isFeedAvailable];
 312  [mAddEmailField setEnabled:(isFeedAvailable && isDisplayingContacts)];
 313  [mAddContactButton setEnabled:isFeedAvailable];
 314  
 315  BOOL canDeleteAllEntries = 
 316    (isDisplayingContacts 
 317      && [[mContactFeed entries] count] > 0 
 318      && [mContactFeed batchLink] != nil)
 319    || (!isDisplayingContacts 
 320      && [[mGroupFeed entries] count] > 0
 321      && [mGroupFeed batchLink] != nil);
 322  
 323  [mDeleteAllButton setEnabled:canDeleteAllEntries];
 324  
 325  if (isDisplayingContacts) {
 326    [[mAddTitleField cell] setPlaceholderString:@"Add Name"]; 
 327    [[mAddEmailField cell] setPlaceholderString:@"Add E-mail"]; 
 328  } else {
 329    [[mAddTitleField cell] setPlaceholderString:@"Add Group"]; 
 330    [[mAddEmailField cell] setPlaceholderString:@""]; 
 331  }
 332}
 333
 334// get or clear the image for this specified contact
 335- (void)updateImageForContact:(GDataEntryContact *)contact {
 336
 337  if (!contact) {
 338    // clear the image
 339    [mContactImageView setImage:nil];
 340    [self setContactImageETag:nil];
 341
 342  } else {
 343    // Google Contacts guarantees that the photo link ETag changes whenever
 344    // the photo for the contact changes
 345    //
 346    // if the new photo link ETag is different from the previous one,
 347    // clear the image and fetch the new image
 348    
 349    GDataLink *photoLink = [contact photoLink];
 350
 351    NSString *imageETag = [photoLink ETag];
 352    if (imageETag == nil || ![mContactImageETag isEqual:imageETag]) {
 353
 354      // save the image ETag for the contact we're fetching so later we can
 355      // use it to determine if the image on the server has changed
 356      [self setContactImageETag:imageETag];
 357
 358      [mContactImageView setImage:nil];
 359
 360      if (imageETag != nil) {
 361
 362        // get an NSURLRequest object with an auth token
 363        NSURL *imageURL = [photoLink URL];
 364        GDataServiceGoogleContact *service = [self contactService];
 365
 366        // requestForURL:ETag:httpMethod: sets the user agent header of the
 367        // request and, when using ClientLogin, adds the authorization header
 368        NSMutableURLRequest *request = [service requestForURL:imageURL
 369                                                         ETag:nil
 370                                                   httpMethod:nil];
 371
 372        [request setValue:@"image/*" forHTTPHeaderField:@"Accept"];
 373
 374        GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request];
 375        [fetcher setAuthorizer:[service authorizer]];
 376        [fetcher beginFetchWithDelegate:self
 377                      didFinishSelector:@selector(imageFetcher:finishedWithData:error:)];
 378      }
 379    }
 380  }
 381}
 382
 383- (void)imageFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error {
 384  if (error == nil) {
 385    // got the data; display it in the image view.  Because this is sample
 386    // code, we won't be rigorous about verifying that the selected contact hasn't
 387    // changed between when the fetch began and now.
 388    NSImage *image = [[[NSImage alloc] initWithData:data] autorelease];
 389
 390    [mContactImageView setImage:image];
 391  } else {
 392    NSLog(@"imageFetcher:%@ failedWithError:%@", fetcher,  error);
 393  }
 394}
 395
 396#pragma mark IBActions
 397
 398- (IBAction)getFeedClicked:(id)sender {
 399  
 400  NSCharacterSet *whitespace = [NSCharacterSet whitespaceAndNewlineCharacterSet];
 401  
 402  NSString *username = [mUsernameField stringValue];
 403  username = [username stringByTrimmingCharactersInSet:whitespace];
 404  
 405  if ([username rangeOfString:@"@"].location == NSNotFound) {
 406    // if no domain was supplied, add @gmail.com
 407    username = [username stringByAppendingString:@"@gmail.com"];
 408  }
 409  
 410  [mUsernameField setStringValue:username];
 411  
 412  [self fetchAllGroupsAndContacts];
 413}
 414
 415- (IBAction)feedSegmentClicked:(id)sender {
 416  [self updateUI];  
 417}
 418
 419- (IBAction)cancelFeedFetchClicked:(id)sender {
 420  [mContactFetchTicket cancelTicket];
 421  [self setContactFetchTicket:nil];
 422  
 423  [mGroupFetchTicket cancelTicket];
 424  [self setGroupFetchTicket:nil];
 425  
 426  [self updateUI];
 427}
 428
 429- (IBAction)setContactImageClicked:(id)sender {
 430  [self setContactImage]; 
 431}
 432
 433- (IBAction)deleteContactImageClicked:(id)sender {
 434  [self deleteContactImage]; 
 435}
 436
 437- (IBAction)addContactClicked:(id)sender {
 438  if ([self isDisplayingContacts]) {
 439    [self addAContact];
 440  } else {
 441    [self addAGroup]; 
 442  }
 443}
 444
 445- (IBAction)deleteContactClicked:(id)sender {
 446  [self deleteSelectedContactOrGroup];
 447}
 448
 449- (IBAction)deleteAllClicked:(id)sender {
 450  [self deleteAllContactsOrGroups];
 451}
 452
 453
 454- (IBAction)addEntryClicked:(id)sender {
 455  [self addAnItem];
 456}
 457
 458- (IBAction)editEntryClicked:(id)sender {
 459  [self editSelectedItem];
 460}
 461
 462- (IBAction)deleteEntryClicked:(id)sender {
 463  [self deleteSelectedItem];
 464}
 465
 466- (IBAction)makeEntryPrimaryClicked:(id)sender {
 467  [self makeSelectedItemPrimary];
 468}
 469
 470- (IBAction)entrySegmentClicked:(id)sender {
 471  [self updateUI];  
 472}
 473
 474- (IBAction)sortContactsClicked:(id)sender {
 475  [self updateUI]; 
 476}
 477
 478- (IBAction)loggingCheckboxClicked:(id)sender {
 479  [GTMHTTPFetcher setLoggingEnabled:[sender state]]; 
 480}
 481
 482#pragma mark -
 483
 484// get a contact service object with the current username/password
 485//
 486// A "service" object handles networking tasks.  Service objects
 487// contain user authentication information as well as networking
 488// state information (such as cookies and the "last modified" date for
 489// fetched data.)
 490
 491- (GDataServiceGoogleContact *)contactService {
 492  
 493  static GDataServiceGoogleContact* service = nil;
 494  
 495  if (!service) {
 496    service = [[GDataServiceGoogleContact alloc] init];
 497
 498    [service setShouldCacheResponseData:YES];
 499    [service setServiceShouldFollowNextLinks:YES];
 500  }
 501
 502  // update the username/password each time the service is requested
 503  NSString *username = [mUsernameField stringValue];
 504  NSString *password = [mPasswordField stringValue];
 505  
 506  [service setUserCredentialsWithUsername:username
 507                                 password:password];
 508  
 509  return service;
 510}
 511
 512- (NSArray *)sortedEntries:(NSArray *)entries {
 513  
 514  if ([mSortFeedCheckbox state] == NSOnState) {
 515    
 516    NSSortDescriptor *sortDesc;
 517    SEL sel = @selector(caseInsensitiveCompare:);
 518    
 519    sortDesc = [[[NSSortDescriptor alloc] initWithKey:@"entryDisplayName" 
 520                                            ascending:YES
 521                                             selector:sel] autorelease];
 522    
 523    NSArray *sortDescriptors = [NSArray arrayWithObject:sortDesc];
 524    entries = [entries sortedArrayUsingDescriptors:sortDescriptors];
 525  }
 526  return entries;
 527}
 528
 529// returns all contacts from the feed, sorted if the checkbox is checked
 530- (NSArray *)sortedContactEntries {
 531  NSArray *entries = [mContactFeed entries];
 532  return [self sortedEntries:entries];
 533}
 534
 535// returns all groups from the feed, sorted if the checkbox is checked
 536- (NSArray *)sortedGroupEntries {
 537  NSArray *entries = [mGroupFeed entries];
 538  return [self sortedEntries:entries];
 539}
 540
 541
 542// get the contact selected in the top list, or nil if none or if groups
 543// are being viewed
 544- (GDataEntryContact *)selectedContact {
 545  
 546  if ([self isDisplayingContacts]) {
 547    
 548    NSArray *contacts = [self sortedContactEntries];
 549    int rowIndex = [mFeedTable selectedRow];
 550    if ([contacts count] > rowIndex) {
 551      
 552      GDataEntryContact *contact = [contacts objectAtIndex:rowIndex];
 553      return contact;
 554    }
 555  }
 556  return nil;
 557}
 558
 559// get the contact or group selected in the top list, or nil if none 
 560- (GDataEntryContactGroup *)selectedGroup {
 561  
 562  if (![self isDisplayingContacts]) {
 563    
 564    NSArray *groups = [self sortedGroupEntries];
 565    int rowIndex = [mFeedTable selectedRow];
 566    if ([groups count] > rowIndex) {
 567      
 568      GDataEntryContactGroup *group = [groups objectAtIndex:rowIndex];
 569      return group;
 570    }
 571  }
 572  return nil;
 573}
 574
 575- (id)selectedContactOrGroup {
 576  GDataEntryContact *selectedContact = [self selectedContact];
 577  if (selectedContact) return selectedContact;
 578  
 579  GDataEntryContactGroup *selectedGroup = [self selectedGroup];
 580  return selectedGroup;
 581}
 582
 583// get the item selected in the bottom list, or nil if none
 584//
 585// the item could be org, phone, e-mail, IM, postal, group, or extended props
 586- (GDataObject *)selectedItem {
 587  
 588  NSArray *entries = [self itemsForSelectedSegment];
 589  
 590  int rowIndex = [mEntryTable selectedRow];
 591  if ([entries count] > rowIndex) {
 592    
 593    GDataObject *entry = [entries objectAtIndex:rowIndex];
 594    return entry;
 595  }
 596  return nil;  
 597}
 598
 599// get the key needed to retrieve the list of items from a contact
 600- (NSString *)keyForSelectedSegment {
 601  
 602  switch ([mEntrySegmentedControl selectedSegment]) {
 603    case kOrganizationSegment:  return @"organizations";
 604    case kEmailSegment:         return @"emailAddresses";
 605    case kPhoneSegment:         return @"phoneNumbers";
 606    case kPostalSegment:        return @"structuredPostalAddresses";
 607    case kIMSegment:            return @"IMAddresses";
 608    case kGroupSegment:         return @"groupMembershipInfos";
 609    case kExtendedPropsSegment: return @"extendedProperties";
 610  }  
 611  return nil;
 612}
 613
 614// get the selector needed to make an item primary
 615- (SEL)makePrimarySelectorForSelectedSegment {
 616  
 617  switch ([mEntrySegmentedControl selectedSegment]) {
 618    case kOrganizationSegment: return @selector(setPrimaryOrganization:);
 619    case kEmailSegment:        return @selector(setPrimaryEmailAddress:);
 620    case kIMSegment:           return @selector(setPrimaryIMAddress:);
 621    case kPhoneSegment:        return @selector(setPrimaryPhoneNumber:);
 622    case kPostalSegment:       return @selector(setPrimaryStructuredPostalAddress:);
 623  }  
 624  return nil;
 625}
 626
 627// get the list of items from the selected contact, according to the segmented
 628// control's selection
 629- (NSArray *)itemsForSelectedSegment {
 630  
 631  NSString *path = [self keyForSelectedSegment];
 632  SEL sel = NSSelectorFromString(path);
 633  
 634  id selectedEntry = [self selectedContactOrGroup];
 635  
 636  // some segment selectors don't apply to group entries
 637  NSArray *array = nil;
 638  
 639  if ([selectedEntry respondsToSelector:sel]) {
 640    array = [selectedEntry performSelector:sel];
 641  }
 642  
 643  return array;
 644}
 645
 646- (Class)itemClassForSelectedSegment {
 647  
 648  switch ([mEntrySegmentedControl selectedSegment]) {
 649    case kOrganizationSegment:  return [GDataOrganization class];
 650    case kEmailSegment:         return [GDataEmail class];
 651    case kPhoneSegment:         return [GDataPhoneNumber class];
 652    case kPostalSegment:        return [GDataStructuredPostalAddress class];
 653    case kIMSegment:            return [GDataIM class];
 654    case kGroupSegment:         return [GDataGroupMembershipInfo class];
 655    case kExtendedPropsSegment: return [GDataExtendedProperty class];
 656  }
 657  return nil;
 658}
 659
 660#pragma mark Fetch all groups
 661
 662- (NSURL *)groupFeedURL {
 663  
 664  NSString *propName = [mPropertyNameField stringValue];
 665  
 666  NSURL *feedURL;
 667  if ([propName caseInsensitiveCompare:@"full"] == NSOrderedSame
 668      || [propName length] == 0) {
 669    
 670    // full feed includes all clients' extended properties
 671    feedURL = [GDataServiceGoogleContact groupFeedURLForUserID:kGDataServiceDefaultUser];
 672    
 673  } else if ([propName caseInsensitiveCompare:@"thin"] == NSOrderedSame) {
 674    
 675    // thin feed excludes extended properties
 676    feedURL = [GDataServiceGoogleContact contactURLForFeedName:kGDataGoogleContactGroupsFeedName
 677                                                        userID:kGDataServiceDefaultUser
 678                                                    projection:kGDataGoogleContactThinProjection];
 679    
 680  } else {
 681    
 682    feedURL = [GDataServiceGoogleContact contactGroupFeedURLForPropertyName:propName];
 683  }
 684  return feedURL;
 685}
 686
 687// begin retrieving the list of the user's contacts
 688- (void)fetchAllGroupsAndContacts {
 689  
 690  [self setGroupFeed:nil];
 691  [self setGroupFetchError:nil];
 692  [self setGroupFetchTicket:nil];
 693  
 694  // we will fetch contacts next
 695  [self setContactFeed:nil];
 696
 697  GDataServiceGoogleContact *service = [self contactService];
 698  GDataServiceTicket *ticket;
 699  
 700  BOOL showDeleted = ([mShowDeletedCheckbox state] == NSOnState);
 701  
 702  // request a whole buncha groups; our service object is set to
 703  // follow next links as well in case there are more than 2000
 704  const int kBuncha = 2000;
 705  
 706  NSURL *feedURL = [self groupFeedURL];
 707  
 708  GDataQueryContact *query = [GDataQueryContact contactQueryWithFeedURL:feedURL];
 709  [query setShouldShowDeleted:showDeleted];
 710  [query setMaxResults:kBuncha];
 711  
 712  ticket = [service fetchFeedWithQuery:query
 713                              delegate:self
 714                     didFinishSelector:@selector(groupsFetchTicket:finishedWithFeed:error:)];
 715  
 716  [self setGroupFetchTicket:ticket];
 717  
 718  [self updateUI];
 719}
 720
 721// groups fetched callback
 722- (void)groupsFetchTicket:(GDataServiceTicket *)ticket
 723         finishedWithFeed:(GDataFeedContactGroup *)feed
 724                    error:(NSError *)error {
 725
 726  [self setGroupFeed:feed];
 727  [self setGroupFetchError:error];
 728  [self setGroupFetchTicket:nil];
 729
 730  if (error == nil) {
 731    // we have the groups; now get the contacts
 732    [self fetchAllContacts];
 733  } else {
 734    // error fetching groups
 735    [self updateUI];
 736  }
 737}
 738
 739#pragma mark Fetch all contacts
 740
 741- (NSURL *)contactFeedURL {
 742  
 743  NSString *propName = [mPropertyNameField stringValue];
 744  
 745  NSURL *feedURL;
 746  if ([propName caseInsensitiveCompare:@"full"] == NSOrderedSame
 747      || [propName length] == 0) {
 748    
 749    // full feed includes all clients' extended properties
 750    feedURL = [GDataServiceGoogleContact contactFeedURLForUserID:kGDataServiceDefaultUser];
 751    
 752  } else if ([propName caseInsensitiveCompare:@"thin"] == NSOrderedSame) {
 753    
 754    // thin feed excludes all extended properties
 755    feedURL = [GDataServiceGoogleContact contactFeedURLForUserID:kGDataServiceDefaultUser
 756                                                      projection:kGDataGoogleContactThinProjection];
 757  } else {
 758    
 759    feedURL = [GDataServiceGoogleContact contactFeedURLForPropertyName:propName];
 760  }
 761  return feedURL;
 762}
 763
 764// begin retrieving the list of the user's contacts
 765- (void)fetchAllContacts {
 766
 767  [self setContactFeed:nil];
 768  [self setContactFetchError:nil];
 769  [self setContactFetchTicket:nil];
 770
 771  GDataServiceGoogleContact *service = [self contactService];
 772  GDataServiceTicket *ticket;
 773
 774  BOOL shouldShowDeleted = ([mShowDeletedCheckbox state] == NSOnState);
 775  BOOL shouldQueryMyContacts = ([mMyContactsCheckbox state] == NSOnState);
 776
 777  // request a whole buncha contacts; our service object is set to
 778  // follow next links as well in case there are more than 2000
 779  const int kBuncha = 2000;
 780
 781  NSURL *feedURL = [self contactFeedURL];
 782
 783  GDataQueryContact *query = [GDataQueryContact contactQueryWithFeedURL:feedURL];
 784  [query setShouldShowDeleted:shouldShowDeleted];
 785  [query setMaxResults:kBuncha];
 786
 787  if (shouldQueryMyContacts) {
 788
 789    GDataFeedContactGroup *groupFeed = [self groupFeed];
 790    GDataEntryContactGroup *myContactsGroup
 791      = [groupFeed entryForSystemGroupID:kGDataSystemGroupIDMyContacts];
 792
 793    NSString *myContactsGroupID = [myContactsGroup identifier];
 794
 795    [query setGroupIdentifier:myContactsGroupID];
 796  }
 797
 798  ticket = [service fetchFeedWithQuery:query
 799                              delegate:self
 800                     didFinishSelector:@selector(contactsFetchTicket:finishedWithFeed:error:)];
 801
 802  [self setContactFetchTicket:ticket];
 803
 804  [self updateUI];
 805}
 806
 807// contacts fetched callback
 808- (void)contactsFetchTicket:(GDataServiceTicket *)ticket
 809           finishedWithFeed:(GDataFeedContact *)feed
 810                      error:(NSError *)error {
 811
 812  [self setContactFeed:feed];
 813  [self setContactFetchError:error];
 814  [self setContactFetchTicket:nil];
 815
 816  [self updateUI];
 817}
 818
 819#pragma mark Set contact image
 820
 821- (void)setContactImage {
 822
 823  // ask the user to choose an image file
 824  NSOpenPanel *openPanel = [NSOpenPanel openPanel];
 825  [openPanel setPrompt:@"Set"];
 826  [openPanel setAllowedFileTypes:[NSImage imageFileTypes]];
 827  [openPanel beginSheetModalForWindow:[self window]
 828                    completionHandler:^(NSInteger result) {
 829    if (result == NSOKButton) {
 830      // user chose a photo and clicked OK
 831      //
 832      // start uploading (deferred to the main thread since we currently have
 833      // a sheet displayed)
 834      [self performSelectorOnMainThread:@selector(setSelectedContactPhotoAtPath:)
 835                             withObject:[[openPanel URL] path]
 836                          waitUntilDone:NO];
 837    }
 838  }];
 839}
 840
 841- (void)setSelectedContactPhotoAtPath:(NSString *)path {
 842  
 843  NSString *errorMsg = nil;
 844  
 845  // make a new entry for the file
 846  NSString *mimeType = [GDataUtilities MIMETypeForFileAtPath:path
 847                                             defaultMIMEType:@"image/jpeg"];
 848  if (!mimeType) {
 849    errorMsg = [NSString stringWithFormat:@"need MIME type for file %@", path];
 850  } else {
 851    
 852    NSString *fullName = [[NSFileManager defaultManager] displayNameAtPath:path];
 853
 854    GDataEntryContact *newEntry = [GDataEntryContact contactEntryWithFullNameString:fullName];
 855
 856    NSData *uploadData = [NSData dataWithContentsOfFile:path];
 857    if (!uploadData) {
 858      errorMsg = [NSString stringWithFormat:@"cannot read file %@", path];
 859    } else {
 860      GDataLink *photoLink = [[self selectedContact] photoLink];
 861
 862      [newEntry setShouldUploadDataOnly:YES];
 863      [newEntry setUploadData:uploadData];
 864      [newEntry setUploadMIMEType:mimeType];
 865      [newEntry setUploadSlug:[path lastPathComponent]];
 866
 867      // provide the ETag of the photo we are replacing, if any
 868      [newEntry setETag:[photoLink ETag]];
 869
 870      NSURL *putURL = [photoLink URL];
 871      
 872      // make service tickets call back into our upload progress selector
 873      GDataServiceGoogleContact *service = [self contactService];
 874      
 875      SEL progressSel = @selector(ticket:hasDeliveredByteCount:ofTotalByteCount:);
 876      [service setServiceUploadProgressSelector:progressSel];
 877      
 878      // insert the entry into the contacts feed
 879      [service fetchEntryByUpdatingEntry:newEntry
 880                             forEntryURL:putURL
 881                                delegate:self
 882                       didFinishSelector:@selector(uploadPhotoTicket:finishedWithEntry:error:)];
 883
 884      // we don't want future tickets to always use the upload progress selector
 885      [service setServiceUploadProgressSelector:nil];
 886    }
 887  }
 888  
 889  if (errorMsg) {
 890    NSBeginAlertSheet(@"Upload Error", nil, nil, nil,
 891                      [self window], nil, nil,
 892                      nil, nil, @"%@", errorMsg);
 893  }
 894  
 895  [self updateUI];
 896}
 897
 898// progress callback
 899- (void)ticket:(GDataServiceTicket *)ticket
 900hasDeliveredByteCount:(unsigned long long)numberOfBytesRead 
 901   ofTotalByteCount:(unsigned long long)dataLength {
 902  
 903  [mSetContactImageProgressIndicator setMinValue:0.0];
 904  [mSetContactImageProgressIndicator setMaxValue:(double)dataLength];
 905  [mSetContactImageProgressIndicator setDoubleValue:(double)numberOfBytesRead];
 906}
 907
 908// finished callback
 909- (void)uploadPhotoTicket:(GDataServiceTicket *)ticket
 910        finishedWithEntry:(GDataEntryBase *)entry
 911                    error:(NSError *)error {
 912
 913  [mSetContactImageProgressIndicator setDoubleValue:0.0];
 914  [self updateUI];
 915
 916  if (error == nil) {
 917    // refetch the current contact list
 918    [self fetchAllGroupsAndContacts];
 919
 920    // tell the user that the add worked
 921    NSBeginAlertSheet(@"Uploaded photo", nil, nil, nil,
 922                      [self window], nil, nil,
 923                      nil, nil, @"Photo uploaded");
 924  } else {
 925    // error uploading photo
 926    NSBeginAlertSheet(@"Upload failed", nil, nil, nil,
 927                      [self window], nil, nil,
 928                      nil, nil, @"Photo upload failed: %@", error);
 929  }
 930}
 931
 932#pragma mark Delete contact image
 933
 934- (void)deleteContactImage {
 935  
 936  // display the confirmation dialog
 937  GDataEntryContact *contact = [self selectedContact];
 938  if (contact) {
 939    
 940    // make the user confirm that the selected contact should be deleted
 941    NSBeginAlertSheet(@"Delete Image", @"Delete", @"Cancel", nil,
 942                      [self window], self, 
 943                      @selector(contactPhotoDeleteSheetDidEnd:returnCode:contextInfo:),
 944                      nil, nil, @"Delete photo for contact \"%@\"?",
 945                      [contact entryDisplayName]);
 946  }
 947}
 948
 949
 950- (void)contactPhotoDeleteSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
 951  
 952  if (returnCode == NSAlertDefaultReturn) {
 953    
 954    // delete the contact's photo
 955    
 956    GDataServiceGoogleContact *service = [self contactService];
 957    
 958    GDataLink *photoLink = [[self selectedContact] photoLink];
 959
 960    NSURL *editURL = [photoLink URL];
 961    NSString *etag = [photoLink ETag];
 962    
 963    [service deleteResourceURL:editURL
 964                          ETag:etag
 965                      delegate:self
 966             didFinishSelector:@selector(deletePhotoTicket:finishedWithNil:error:)];
 967  }
 968}
 969
 970// delete photo callback
 971- (void)deletePhotoTicket:(GDataServiceTicket *)ticket
 972          finishedWithNil:(GDataObject *)obj
 973                    error:(NSError *)error {
 974  if (error == nil) {
 975    // photo deleted
 976    NSBeginAlertSheet(@"Deleted photo", nil, nil, nil,
 977                      [self window], nil, nil,
 978                      nil, nil, @"Photo deleted");
 979
 980    // refetch the current contact list
 981    [self fetchAllGroupsAndContacts];
 982    [self updateUI];
 983  } else {
 984    // failed
 985    NSBeginAlertSheet(@"Delete photo failed", nil, nil, nil,
 986                      [self window], nil, nil,
 987                      nil, nil, @"Photo delete failed: %@", error);
 988  }
 989}
 990
 991#pragma mark Add a Contact
 992
 993- (void)addAContact {
 994  
 995  NSString *title = [mAddTitleField stringValue];
 996  NSString *email = [mAddEmailField stringValue];
 997  
 998  if ([title length] > 0) {
 999    
1000    GDataEntryContact *newContact;
1001    newContact = [GDataEntryContact contactEntryWithFullNameString:title];
1002    
1003    if ([email length] > 0) {
1004      // all items must have a rel or a label, but not both
1005      GDataEmail *emailObj = [GDataEmail emailWithLabel:nil
1006                                                address:email];
1007      [emailObj setRel:kGDataContactOther];
1008      [emailObj setIsPrimary:YES];
1009
1010      [newContact addEmailAddress:emailObj];
1011    }
1012
1013    if ([mMyContactsCheckbox state] == NSOnState) {
1014      // add this to the MyContacts group too
1015      GDataFeedContactGroup *groupFeed = [self groupFeed];
1016      GDataEntryContactGroup *myContactsGroup
1017        = [groupFeed entryForSystemGroupID:kGDataSystemGroupIDMyContacts];
1018
1019      NSString *myContactsGroupID = [myContactsGroup identifier];
1020
1021      GDataGroupMembershipInfo *groupInfo
1022        = [GDataGroupMembershipInfo groupMembershipInfoWithHref:myContactsGroupID];
1023
1024      [newContact addGroupMembershipInfo:groupInfo];
1025    }
1026
1027    GDataServiceGoogleContact *service = [self contactService];
1028   
1029    NSURL *postURL = [[mContactFeed postLink] URL];
1030    
1031    [service fetchEntryByInsertingEntry:newContact
1032                             forFeedURL:postURL
1033                               delegate:self
1034                      didFinishSelector:@selector(addContactTicket:addedEntry:error:)];
1035  }
1036}
1037
1038// add contact callback
1039- (void)addContactTicket:(GDataServiceTicket *)ticket
1040              addedEntry:(GDataEntryContact *)object
1041                   error:(NSError *)error {
1042  if (error == nil) {
1043    // tell the user that the add worked
1044    NSBeginAlertSheet(@"Added contact", nil, nil, nil,
1045                      [self window], nil, nil,
1046                      nil, nil, @"Contact added");
1047
1048    [mAddTitleField setStringValue:@""];
1049    [mAddEmailField setStringValue:@""];
1050
1051    // refetch the current contacts
1052    [self fetchAllGroupsAndContacts];
1053    [self updateUI];
1054  } else {
1055    // failure to add contact
1056    NSBeginAlertSheet(@"Add failed", nil, nil, nil,
1057                      [self window], nil, nil,
1058                      nil, nil, @"Contact add failed: %@", error);
1059  }
1060}
1061
1062#pragma mark Add a Group
1063
1064- (void)addAGroup {
1065  
1066  NSString *title = [mAddTitleField stringValue];
1067  
1068  if ([title length] > 0) {
1069    
1070    GDataEntryContactGroup *newGroup;
1071    newGroup = [GDataEntryContactGroup contactGroupEntryWithTitle:title];
1072    
1073    GDataServiceGoogleContact *service = [self contactService];
1074    
1075    NSURL *postURL = [[mGroupFeed postLink] URL];
1076    
1077    [service fetchEntryByInsertingEntry:newGroup
1078                             forFeedURL:postURL
1079                               delegate:self
1080                      didFinishSelector:@selector(addGroupTicket:addedEntry:error:)];
1081  }
1082}
1083
1084// add group callback
1085- (void)addGroupTicket:(GDataServiceTicket *)ticket
1086            addedEntry:(GDataEntryContactGroup *)object
1087                 error:(NSError *)error {
1088  if (error == nil) {
1089    // tell the user that the add worked
1090    NSBeginAlertSheet(@"Added group", nil, nil, nil,
1091                      [self window], nil, nil,
1092                      nil, nil, @"Group added");
1093
1094    [mAddTitleField setStringValue:@""];
1095    [mAddEmailField setStringValue:@""];
1096
1097    // refetch the current groups
1098    [self fetchAllGroupsAndContacts];
1099    [self updateUI];
1100  } else {
1101    // failure to add group
1102    NSBeginAlertSheet(@"Add failed", nil, nil, nil,
1103                      [self window], nil, nil,
1104                      nil, nil, @"Group add failed: %@", error);
1105  }
1106}
1107
1108#pragma mark Delete a Contact or Group
1109
1110- (void)deleteSelectedContactOrGroup {
1111  
1112  // display the confirmation dialog
1113  id entry = [self selectedContactOrGroup];
1114  if (entry) {
1115    
1116    // make the user confirm that the selected entry should be deleted
1117    NSBeginAlertSheet(@"Delete", @"Delete", @"Cancel", nil,
1118                      [self window], self, 
1119                      @selector(entryDeleteSheetDidEnd:returnCode:contextInfo:),
1120                      nil, nil, @"Delete \"%@\"?",
1121                      [entry entryDisplayName]);
1122  }
1123}
1124
1125// delete dialog callback
1126- (void)entryDeleteSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
1127  
1128  if (returnCode == NSAlertDefaultReturn) {
1129    
1130    // delete the entry
1131    
1132    GDataServiceGoogleContact *service = [self contactService];
1133    
1134    id entry = [self selectedContactOrGroup];
1135    
1136    [service deleteEntry:entry
1137                delegate:self
1138       didFinishSelector:@selector(deleteEntryTicket:finishedWithNil:error:)];
1139  }
1140}
1141
1142// delete entry callback
1143- (void)deleteEntryTicket:(GDataServiceTicket *)ticket
1144          finishedWithNil:(id)object
1145                    error:(NSError *)error {
1146
1147  if (error == nil) {
1148    NSBeginAlertSheet(@"Deleted", nil, nil, nil,
1149                      [self window], nil, nil,
1150                      nil, nil, @"Entry deleted");
1151    [self fetchAllGroupsAndContacts];
1152    [self updateUI];
1153  } else {
1154    NSBeginAlertSheet(@"Delete failed", nil, nil, nil,
1155                      [self window], nil, nil,
1156                      nil, nil, @"Entry delete failed: %@", error);
1157  }
1158}
1159
1160#pragma mark Batch Delete All Contacts or Groups
1161
1162- (void)deleteAllContactsOrGroups {
1163  
1164  // make the user confirm that all entries should be deleted
1165  GDataFeedBase *feed;
1166  
1167  if ([self isDisplayingContacts]) {
1168    feed = mContactFeed;
1169  } else {
1170    feed = mGroupFeed;
1171  }
1172  
1173  NSBeginAlertSheet(@"Delete All", @"Delete", @"Cancel", nil,
1174                    [self window], self, 
1175                    @selector(deleteAllSheetDidEnd:returnCode:contextInfo:),
1176                    nil, nil, @"Delete %u %@?",
1177                    (unsigned int) [[feed entries] count],
1178                    [self isDisplayingContacts] ? @"contacts" : @"groups");
1179}
1180
1181NSString* const kBatchTicketsProperty = @"BatchTickets";
1182NSString* const kBatchResultsProperty = @"BatchResults";
1183
1184// delete dialog callback
1185- (void)deleteAllSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
1186  
1187  if (returnCode == NSAlertDefaultReturn) {
1188    
1189    // delete the feed entries
1190    GDataFeedBase *feed;
1191    if ([self isDisplayingContacts]) {
1192      feed = mContactFeed;
1193    } else {
1194      feed = mGroupFeed;
1195    }
1196    
1197    NSArray *entries = [feed entries];
1198    
1199    NSURL *batchURL = [[feed batchLink] URL];
1200    if (batchURL == nil) {
1201      // the button shouldn't be enabled when we can't batch delete, so we
1202      // shouldn't get here
1203      NSBeep();
1204      
1205    } else {
1206      
1207      // the contacts feed supports batch size up to 100 entries
1208      const int kMaxBatchSize = 100;
1209      
1210      // allocate arrays that will be used by the callback when each
1211      // ticket finishes
1212      NSMutableArray *batchTickets = [NSMutableArray array];
1213      NSMutableArray *batchResults = [NSMutableArray array];
1214      
1215      unsigned int totalEntries = [entries count];
1216      
1217      for (unsigned int idx = 0; idx < totalEntries; idx++) {
1218        
1219        GDataEntryBase *entry = [entries objectAtIndex:idx];
1220        
1221        // add a batch ID to this entry
1222        static int staticID = 0;
1223        NSString *batchID = [NSString stringWithFormat:@"batchID_%u", ++staticID];
1224        [entry setBatchIDWithString:batchID];
1225
1226        // we don't need to add the batch operation to the entries since
1227        // we're putting it in the feed to apply to all entries
1228        
1229        // we could force an error on an item by nuking the entry's identifier, 
1230        // like
1231        //   if (idx == 1) { [entry setIdentifier:nil]; }
1232        
1233        // send a batch when we've seen every entry, or when the batch size
1234        // has reached 100
1235        if (((idx + 1) % kMaxBatchSize) == 0
1236            || (idx + 1) == totalEntries) {
1237          
1238          // make a batch feed object: add entries to the feed, and since
1239          // we are doing the same operation for all entries in the feed, 
1240          // add the operation to the feed
1241          
1242          GDataFeedContact *batchFeed = [GDataFeedContact contactFeed];
1243
1244          unsigned int rangeStart = idx - (idx % kMaxBatchSize);
1245          NSRange batchEntryRange = NSMakeRange(rangeStart, 
1246                                                idx - rangeStart + 1);
1247          NSArray *entrySubset = [entries subarrayWithRange:batchEntryRange];
1248          [batchFeed setEntriesWithEntries:entrySubset];
1249          
1250          GDataBatchOperation *op;
1251          op = [GDataBatchOperation batchOperationWithType:kGDataBatchOperationDelete];
1252          [batchFeed setBatchOperation:op];    
1253          
1254          // now do the usual steps for authenticating for this service, and issue
1255          // the fetch
1256          
1257          GDataServiceGoogleContact *service = [self contactService];
1258          GDataServiceTicket *ticket;
1259          
1260          ticket = [service fetchFeedWithBatchFeed:batchFeed
1261                                   forBatchFeedURL:batchURL
1262                                          delegate:self
1263                                 didFinishSelector:@selector(batchDeleteTicket:finishedWithFeed:error:)];
1264          
1265          [batchTickets addObject:ticket];
1266          
1267          // set the arrays used by the callback into the ticket properties
1268          [ticket setProperty:batchTickets forKey:kBatchTicketsProperty];
1269          [ticket setProperty:batchResults forKey:kBatchResultsProperty];
1270        }
1271      }
1272    }
1273  }
1274}
1275
1276// batch delete callback
1277- (void)batchDeleteTicket:(GDataServiceTicket *)ticket
1278         finishedWithFeed:(GDataFeedBase *)feed
1279                    error:(NSError *)error {
1280
1281  NSMutableArray *batchTickets = [ticket propertyForKey:kBatchTicketsProperty];
1282
1283  if (error == nil) {
1284    NSMutableArray *batchResults = [ticket propertyForKey:kBatchResultsProperty];
1285
1286    [batchResults addObjectsFromArray:[feed entries]];
1287    [batchTickets removeObject:ticket];
1288
1289    if ([batchTickets count] > 0) {
1290      // more tickets are outstanding; let them complete
1291      return;
1292    }
1293
1294    // step through all the entries in the response feed,
1295    // and build a string reporting each result
1296
1297    // show the http status to start (should be 200)
1298    NSString *template = @"http status:%d\n\n";
1299    NSMutableString *reportStr = [NSMutableString stringWithFormat:template,
1300                                  [ticket statusCode]];
1301
1302    for (int idx = 0; idx < [batchResults count]; idx++) {
1303
1304      GDataEntryBase *entry = [batchResults objectAtIndex:idx];
1305      GDataBatchID *batchID = [entry batchID];
1306
1307      // report the batch ID and status for each item
1308      [reportStr appendFormat:@"%@\n", [batchID stringValue]];
1309
1310      GDataBatchInterrupted *interrupted = [entry batchInterrupted];
1311      if (interrupted) {
1312        [reportStr appendFormat:@"%@\n", [interrupted description]];
1313      }
1314
1315      GDataBatchStatus *status = [entry batchStatus];
1316      if (status) {
1317        [reportStr appendFormat:@"%d %@\n",
1318         [[status code] intValue], [status reason]];
1319      }
1320      [reportStr appendString:@"\n"];
1321    }
1322
1323    NSBeginAlertSheet(@"Delete completed", nil, nil, nil,
1324                      [self window], nil, nil,
1325                      nil, nil, @"Delete All completed.\n%@", reportStr);
1326
1327    [self fetchAllGroupsAndContacts];
1328    [self updateUI];
1329  } else {
1330    // batch delete failed
1331    [batchTickets removeObject:ticket];
1332
1333    NSBeginAlertSheet(@"Delete failed", nil, nil, nil,
1334                      [self window], nil, nil,
1335                      nil, nil, @"Batch delete failed: %@", error);
1336  }
1337}
1338
1339#pragma mark Add an Item
1340
1341- (void)addAnItem {
1342  
1343  // make a new object for the selected segment type
1344  // (org, phone, postal, IM, group, e-mail, extended props)
1345  Class objClass = [self itemClassForSelectedSegment];
1346  id obj = [[[objClass alloc] init] autorelease];
1347  
1348  if ([obj respondsToSelector:@selector(setRel:)]) {
1349    // each item needs a rel or a label; we'll use other as a default rel
1350    [obj setRel:kGDataContactOther];
1351  }
1352  
1353  // display the item edit dialog
1354  EditEntryWindowController *controller = [[EditEntryWindowController alloc] init];
1355  [controller runModalForTarget:self
1356                       selector:@selector(addEditControllerFinished:)
1357                      groupFeed:mGroupFeed
1358                         object:obj];
1359}
1360
1361// callback from the edit item dialog
1362- (void)addEditControllerFinished:(EditEntryWindowController *)addEntryController {
1363  
1364  if ([addEntryController wasSaveClicked]) {
1365    
1366    // add the object into a copy of the selected entry, 
1367    // and update the contact
1368    GDataObject *obj = [addEntryController object];
1369    if (obj) {
1370      
1371      // make a new array of items with the addition added to it
1372      NSArray *oldItems = [self itemsForSelectedSegment];
1373      NSMutableArray *newItems = [NSMutableArray arrayWithArray:oldItems];
1374      [newItems addObject:obj];
1375      
1376      // replace the entry's item array with our new one
1377      NSString *keyForSelectedSegment = [self keyForSelectedSegment];
1378      
1379      id selectedEntryCopy = [[[self selectedContactOrGroup] copy] autorelease];
1380      [selectedEntryCopy setValue:newItems forKey:keyForSelectedSegment];
1381      
1382      // now update the entry on the server
1383      GDataServiceGoogleContact *service = [self contactService];
1384      
1385      [service fetchEntryByUpdatingEntry:selectedEntryCopy
1386                                       delegate:self
1387                              didFinishSelector:@selector(addItemTicket:addedEntry:error:)];
1388    }
1389  }
1390  [addEntryController autorelease];
1391}
1392
1393// add item callback
1394- (void)addItemTicket:(GDataServiceTicket *)ticket
1395           addedEntry:(GDataEntryContact *)object
1396                error:(NSError *)error {
1397  if (error == nil) {
1398    // tell the user that the add worked
1399    NSBeginAlertSheet(@"Added item", nil, nil, nil,
1400                      [self window], nil, nil,
1401                      nil, nil, @"Item added");
1402
1403    // refetch the current contact's items
1404    [self fetchAllGroupsAndContacts];
1405    [self updateUI];
1406  } else {
1407    NSBeginAlertSheet(@"Add failed", nil, nil, nil,
1408                      [self window], nil, nil,
1409                      nil, nil, @"Item add failed: %@\nUser info: %@",
1410                      error, [error userInfo]);
1411  }
1412}
1413
1414#pragma mark Edit an item
1415
1416- (void)editSelectedItem {
1417  
1418  // display the item edit dialog
1419  GDataObject *item = [self selectedItem];
1420  if (item) {
1421    EditEntryWindowController *controller = [[EditEntryWindowController alloc] init];
1422    [controller runModalForTarget:self
1423                         selector:@selector(editControllerFinished:)
1424                        groupFeed:mGroupFeed
1425                           object:item];
1426  }
1427}
1428
1429// callback from the edit item dialog
1430- (void)editControllerFinished:(EditEntryWindowController *)editContactController {
1431  
1432  if ([editContactController wasSaveClicked]) {
1433    
1434    // add the object into the selected contact, and update the contact
1435    GDataObject *obj = [editContactController object];
1436    if (obj) {
1437      
1438      // make a new array of items with the edited item replacing its predecessor
1439      NSArray *oldItems = [self itemsForSelectedSegment];
1440      NSMutableArray *newItems = [NSMutableArray arrayWithArray:oldItems];
1441      [newItems replaceObjectAtIndex:[newItems indexOfObject:[self selectedItem]]
1442                          withObject:obj];
1443      
1444      // replace the entry's item array with our new one
1445      NSString *keyForSelectedSegment = [self keyForSelectedSegment];
1446      
1447      id selectedEntryCopy = [[[self selectedContactOrGroup] copy] autorelease];
1448      [selectedEntryCopy setValue:newItems forKey:keyForSelectedSegment];
1449      
1450      // now update the entry on the server
1451      GDataServiceGoogleContact *service = [self contactService];
1452      
1453      [service fetchEntryByUpdatingEntry:selectedEntryCopy
1454                                delegate:self
1455                       didFinishSelector:@selector(editItemTicket:editedEntry:error:)];
1456    }
1457  }
1458  [editContactController autorelease];
1459}
1460
1461// edit item callback
1462- (void)editItemTicket:(GDataServiceTicket *)ticket
1463           editedEntry:(GDataEntryContact *)object
1464                 error:(NSError *)error {
1465  if (error == nil) {
1466    // tell the user that the update worked
1467    NSBeginAlertSheet(@"Updated Entry", nil, nil, nil,
1468                      [self window], nil, nil,
1469                      nil, nil, @"Entry updated");
1470
1471    // re-fetch the selected contact's items
1472    [self fetchAllGroupsAndContacts];
1473    [self updateUI];
1474  } else {
1475    NSBeginAlertSheet(@"Update failed", nil, nil, nil,
1476                      [self window], nil, nil,
1477                      nil, nil, @"Entry update failed: %@", error);
1478  }
1479}
1480
1481#pragma mark Delete an item
1482
1483- (void)deleteSelectedItem {
1484  
1485  // display the item edit dialog
1486  GDataObject *item = [self selectedItem];
1487  if (item) {
1488    
1489    // make the user confirm that the selected item should be deleted
1490    NSBeginAlertSheet(@"Delete Item", @"Delete", @"Cancel", nil,
1491                      [self window], self, 
1492                      @selector(itemDeleteSheetDidEnd:returnCode:contextInfo:),
1493                      nil, nil, @"Delete the item \"%@\"?",
1494                      [self displayNameForItem:item]);
1495  }
1496}
1497
1498// delete dialog callback
1499- (void)itemDeleteSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo {
1500  
1501  if (returnCode == NSAlertDefaultReturn) {
1502    
1503    // delete the item from the contact's item array
1504    
1505    NSArray *oldItems = [self itemsForSelectedSegment];
1506    NSMutableArray *newItems = [NSMutableArray arrayWithArray:oldItems];
1507    
1508    // using removeObject would remove all matching items; we just want to
1509    // remove the selected one
1510    [newItems removeObjectAtIndex:[mEntryTable selectedRow]];
1511    
1512

Large files files are truncated, but you can click here to view the full file