PageRenderTime 58ms CodeModel.GetById 18ms RepoModel.GetById 1ms app.codeStats 0ms

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