/core/externals/update-engine/externals/gdata-objectivec-client/Examples/DocsSample/DocsSampleWindowController.m
Objective C | 1611 lines | 1008 code | 315 blank | 288 comment | 161 complexity | 240f55a740271bde7cd9e25be55cc531 MD5 | raw file
1/* Copyright (c) 2007 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// DocsSampleWindowController.m 18// 19 20// 21// IMPORTANT: 22// 23// The XML-based API for Google Docs has been replaced with a more efficient 24// and easier-to-use JSON API. The JSON API is documented at 25// 26// https://developers.google.com/drive/ 27// 28// See the new Objective-C client library and sample code at 29// http://code.google.com/p/google-api-objectivec-client/ 30// 31// This sample application and library support for the XML-based Docs 32// API will eventually be removed. 33// 34 35 36#import "DocsSampleWindowController.h" 37 38#import "GData/GTMOAuth2WindowController.h" 39 40enum { 41 // upload pop-up menu items 42 kUploadAsGoogleDoc = 0, 43 kUploadOriginal = 1, 44 kUploadOCR = 2, 45 kUploadDE = 3, 46 kUploadJA = 4, 47 kUploadEN = 5 48}; 49 50@interface DocsSampleWindowController (PrivateMethods) 51- (void)updateUI; 52- (void)updateChangeFolderPopup; 53- (void)updateSelectedDocumentThumbnailImage; 54- (void)imageFetcher:(GTMHTTPFetcher *)fetcher finishedWithData:(NSData *)data error:(NSError *)error; 55 56- (void)fetchDocList; 57- (void)fetchRevisionsForSelectedDoc; 58 59- (void)uploadFileAtPath:(NSString *)path; 60- (void)showDownloadPanelForEntry:(GDataEntryBase *)entry suggestedTitle:(NSString *)title; 61- (void)saveDocumentEntry:(GDataEntryBase *)docEntry toPath:(NSString *)path; 62- (void)saveDocEntry:(GDataEntryBase *)entry toPath:(NSString *)savePath exportFormat:(NSString *)exportFormat authService:(GDataServiceGoogle *)service; 63 64- (GDataServiceGoogleDocs *)docsService; 65- (GDataEntryDocBase *)selectedDoc; 66- (GDataEntryDocRevision *)selectedRevision; 67 68- (GDataFeedDocList *)docListFeed; 69- (void)setDocListFeed:(GDataFeedDocList *)feed; 70- (NSError *)docListFetchError; 71- (void)setDocListFetchError:(NSError *)error; 72- (GDataServiceTicket *)docListFetchTicket; 73- (void)setDocListFetchTicket:(GDataServiceTicket *)ticket; 74 75- (GDataFeedDocRevision *)revisionFeed; 76- (void)setRevisionFeed:(GDataFeedDocRevision *)feed; 77- (NSError *)revisionFetchError; 78- (void)setRevisionFetchError:(NSError *)error; 79- (GDataServiceTicket *)revisionFetchTicket; 80- (void)setRevisionFetchTicket:(GDataServiceTicket *)ticket; 81 82- (GDataEntryDocListMetadata *)metadataEntry; 83- (void)setMetadataEntry:(GDataEntryDocListMetadata *)entry; 84 85- (GDataServiceTicket *)uploadTicket; 86- (void)setUploadTicket:(GDataServiceTicket *)ticket; 87 88- (void)displayAlert:(NSString *)title format:(NSString *)format, ...; 89@end 90 91@implementation DocsSampleWindowController 92 93static NSString *const kKeychainItemName = @"DocsSample: Google Docs"; 94 95static DocsSampleWindowController* gDocsSampleWindowController = nil; 96 97+ (DocsSampleWindowController *)sharedDocsSampleWindowController { 98 99 if (!gDocsSampleWindowController) { 100 gDocsSampleWindowController = [[DocsSampleWindowController alloc] init]; 101 } 102 return gDocsSampleWindowController; 103} 104 105 106- (id)init { 107 return [self initWithWindowNibName:@"DocsSampleWindow"]; 108} 109 110- (void)awakeFromNib { 111 // Load the OAuth token from the keychain, if it was previously saved 112 NSString *clientID = [mClientIDField stringValue]; 113 NSString *clientSecret = [mClientSecretField stringValue]; 114 115 GTMOAuth2Authentication *auth; 116 auth = [GTMOAuth2WindowController authForGoogleFromKeychainForName:kKeychainItemName 117 clientID:clientID 118 clientSecret:clientSecret]; 119 [[self docsService] setAuthorizer:auth]; 120 121 // Set the result text field to have a distinctive color and mono-spaced font 122 // to aid in understanding of each operation. 123 [mDocListResultTextField setTextColor:[NSColor darkGrayColor]]; 124 125 NSFont *resultTextFont = [NSFont fontWithName:@"Monaco" size:9]; 126 [mDocListResultTextField setFont:resultTextFont]; 127 128 [self updateUI]; 129} 130 131- (void)dealloc { 132 [mDocListFeed release]; 133 [mDocListFetchTicket release]; 134 [mDocListFetchError release]; 135 136 [mRevisionFeed release]; 137 [mRevisionFetchTicket release]; 138 [mRevisionFetchError release]; 139 140 [mMetadataEntry release]; 141 142 [mUploadTicket cancelTicket]; 143 [mUploadTicket release]; 144 145 [super dealloc]; 146} 147 148#pragma mark - 149 150- (NSString *)signedInUsername { 151 // Get the email address of the signed-in user 152 GTMOAuth2Authentication *auth = [[self docsService] authorizer]; 153 BOOL isSignedIn = auth.canAuthorize; 154 if (isSignedIn) { 155 return auth.userEmail; 156 } else { 157 return nil; 158 } 159} 160 161- (BOOL)isSignedIn { 162 NSString *name = [self signedInUsername]; 163 return (name != nil); 164} 165 166- (void)runSigninThenInvokeSelector:(SEL)signInDoneSel { 167 // Applications should have client ID and client secret strings 168 // hardcoded into the source, but the sample application asks the 169 // developer for the strings 170 NSString *clientID = [mClientIDField stringValue]; 171 NSString *clientSecret = [mClientSecretField stringValue]; 172 173 if ([clientID length] == 0 || [clientSecret length] == 0) { 174 // Remind the developer that client ID and client secret are needed 175 [mClientIDButton performSelector:@selector(performClick:) 176 withObject:self 177 afterDelay:0.5]; 178 return; 179 } 180 181 // Show the OAuth 2 sign-in controller 182 NSString *scope = [GTMOAuth2Authentication scopeWithStrings: 183 [GDataServiceGoogleDocs authorizationScope], 184 [GDataServiceGoogleSpreadsheet authorizationScope], 185 nil]; 186 187 NSBundle *frameworkBundle = [NSBundle bundleForClass:[GTMOAuth2WindowController class]]; 188 GTMOAuth2WindowController *windowController; 189 windowController = [GTMOAuth2WindowController controllerWithScope:scope 190 clientID:clientID 191 clientSecret:clientSecret 192 keychainItemName:kKeychainItemName 193 resourceBundle:frameworkBundle]; 194 195 [windowController setUserData:NSStringFromSelector(signInDoneSel)]; 196 [windowController signInSheetModalForWindow:[self window] 197 completionHandler:^(GTMOAuth2Authentication *auth, NSError *error) { 198 // callback 199 if (error == nil) { 200 [[self docsService] setAuthorizer:auth]; 201 202 NSString *selStr = [windowController userData]; 203 if (selStr) { 204 [self performSelector:NSSelectorFromString(selStr)]; 205 } 206 } else { 207 [self setDocListFetchError:error]; 208 [self updateUI]; 209 } 210 }]; 211} 212 213#pragma mark - 214 215- (void)updateUI { 216 BOOL isSignedIn = [self isSignedIn]; 217 NSString *username = [self signedInUsername]; 218 [mSignedInButton setTitle:(isSignedIn ? @"Sign Out" : @"Sign In")]; 219 [mSignedInField setStringValue:(isSignedIn ? username : @"No")]; 220 221 // docList list display 222 [mDocListTable reloadData]; 223 224 GDataEntryDocBase *selectedDoc = [self selectedDoc]; 225 226 // spin indicator when retrieving feed 227 BOOL isFetchingDocList = (mDocListFetchTicket != nil); 228 if (isFetchingDocList) { 229 [mDocListProgressIndicator startAnimation:self]; 230 } else { 231 [mDocListProgressIndicator stopAnimation:self]; 232 } 233 [mDocListCancelButton setEnabled:isFetchingDocList]; 234 235 // show the doclist feed fetch result error or the selected entry 236 NSString *docResultStr = @""; 237 if (mDocListFetchError) { 238 docResultStr = [mDocListFetchError description]; 239 } else { 240 if (selectedDoc) { 241 docResultStr = [selectedDoc description]; 242 } 243 } 244 [mDocListResultTextField setString:docResultStr]; 245 246 [self updateSelectedDocumentThumbnailImage]; 247 248 // revision list display 249 [mRevisionsTable reloadData]; 250 251 GDataEntryDocRevision *selectedRevision = [self selectedRevision]; 252 253 // spin indicator when retrieving feed 254 BOOL isFetchingRevisions = (mRevisionFetchTicket != nil); 255 if (isFetchingRevisions) { 256 [mRevisionsProgressIndicator startAnimation:self]; 257 } else { 258 [mRevisionsProgressIndicator stopAnimation:self]; 259 } 260 [mRevisionsCancelButton setEnabled:isFetchingRevisions]; 261 262 // show the revision feed fetch result error or the selected entry 263 NSString *revisionsResultStr = @""; 264 if (mRevisionFetchError) { 265 revisionsResultStr = [mRevisionFetchError description]; 266 } else { 267 if (selectedRevision) { 268 revisionsResultStr = [selectedRevision description]; 269 } 270 } 271 [mRevisionsResultTextField setString:revisionsResultStr]; 272 273 BOOL isSelectedDocAStandardGDocsType = 274 [selectedDoc isKindOfClass:[GDataEntryStandardDoc class]] 275 || [selectedDoc isKindOfClass:[GDataEntrySpreadsheetDoc class]] 276 || [selectedDoc isKindOfClass:[GDataEntryPresentationDoc class]]; 277 278 // enable the button for viewing the selected doc in a browser 279 BOOL doesDocHaveHTMLLink = ([selectedDoc HTMLLink] != nil); 280 [mViewSelectedDocButton setEnabled:doesDocHaveHTMLLink]; 281 282 BOOL doesRevisionHaveExportURL = ([[[selectedRevision content] sourceURI] length] > 0); 283 [mDownloadSelectedRevisionButton setEnabled:doesRevisionHaveExportURL]; 284 285 BOOL doesDocHaveExportURL = ([[[selectedDoc content] sourceURI] length] > 0); 286 [mDownloadSelectedDocButton setEnabled:doesDocHaveExportURL]; 287 288 BOOL doesDocHaveEditLink = ([selectedDoc editLink] != nil); 289 [mDeleteSelectedDocButton setEnabled:doesDocHaveEditLink]; 290 291 [mDuplicateSelectedDocButton setEnabled:isSelectedDocAStandardGDocsType]; 292 293 // enable the "Show Changes" button 294 BOOL hasFeed = (mDocListFeed != nil); 295 [mShowChangesButton setEnabled:hasFeed]; 296 297 // enable the publishing checkboxes when a publishable revision is selected 298 BOOL isRevisionSelected = (selectedRevision != nil); 299 BOOL isRevisionPublishable = isRevisionSelected 300 && isSelectedDocAStandardGDocsType; 301 302 [mPublishCheckbox setEnabled:isRevisionPublishable]; 303 [mAutoRepublishCheckbox setEnabled:isRevisionPublishable]; 304 [mPublishOutsideDomainCheckbox setEnabled:isRevisionPublishable]; 305 306 // enable the "Update Publishing" button when the selected revision is 307 // publishable and the checkbox settings differ from the current publishing 308 // setting for the selected revision 309 BOOL isPublished = [[selectedRevision publish] boolValue]; 310 BOOL isPublishedChecked = ([mPublishCheckbox state] == NSOnState); 311 312 BOOL isAutoRepublished = [[selectedRevision publishAuto] boolValue]; 313 BOOL isAutoRepublishedChecked = ([mAutoRepublishCheckbox state] == NSOnState); 314 315 BOOL isExternalPublished = [[selectedRevision publishOutsideDomain] boolValue]; 316 BOOL isExternalPublishedChecked = ([mPublishOutsideDomainCheckbox state] == NSOnState); 317 318 BOOL canUpdatePublishing = isRevisionPublishable 319 && ((isPublished != isPublishedChecked) 320 || (isAutoRepublished != isAutoRepublishedChecked) 321 || (isExternalPublished != isExternalPublishedChecked)); 322 323 [mUpdatePublishingButton setEnabled:canUpdatePublishing]; 324 325 // enable uploading buttons 326 BOOL isUploading = (mUploadTicket != nil); 327 BOOL canPostToFeed = ([mDocListFeed postLink] != nil); 328 329 [mUploadFileButton setEnabled:(canPostToFeed && !isUploading)]; 330 [mStopUploadButton setEnabled:isUploading]; 331 [mPauseUploadButton setEnabled:isUploading]; 332 [mCreateFolderButton setEnabled:canPostToFeed]; 333 334 BOOL isUploadPaused = [mUploadTicket isUploadPaused]; 335 NSString *pauseTitle = (isUploadPaused ? @"Resume" : @"Pause"); 336 [mPauseUploadButton setTitle:pauseTitle]; 337 338 // enable the "Upload Original Document" menu item only if the user metadata 339 // indicates support for generic file uploads 340 GDataDocFeature *feature = [mMetadataEntry featureForName:kGDataDocsFeatureNameUploadAny]; 341 BOOL canUploadGenericDocs = (feature != nil); 342 343 NSMenuItem *genericMenuItem = [[mUploadPopup menu] itemWithTag:kUploadOriginal]; 344 [genericMenuItem setEnabled:canUploadGenericDocs]; 345 346 // fill in the add-to-folder pop-up for the selected doc 347 [self updateChangeFolderPopup]; 348 349 // show the title of the file currently uploading 350 NSString *uploadingStr = @""; 351 NSString *uploadingTitle = [[(GDataEntryBase *) 352 [mDocListFetchTicket postedObject] title] stringValue]; 353 354 if (uploadingTitle) { 355 uploadingStr = [NSString stringWithFormat:@"Uploading: %@", uploadingTitle]; 356 } 357 [mUploadingTextField setStringValue:uploadingStr]; 358 359 // Show or hide the text indicating that the client ID or client secret are 360 // needed 361 BOOL hasClientIDStrings = [[mClientIDField stringValue] length] > 0 362 && [[mClientSecretField stringValue] length] > 0; 363 [mClientIDRequiredTextField setHidden:hasClientIDStrings]; 364} 365 366- (void)updateChangeFolderPopup { 367 368 // replace all menu items in the button with the folder titles and pointers 369 // of the feed's folder entries, but preserve the pop-up's "Change Folder" 370 // title as the first item 371 372 NSString *title = [mFolderMembershipPopup title]; 373 374 NSMenu *addMenu = [[[NSMenu alloc] initWithTitle:title] autorelease]; 375 [addMenu setAutoenablesItems:NO]; 376 [addMenu addItemWithTitle:title action:nil keyEquivalent:@""]; 377 [mFolderMembershipPopup setMenu:addMenu]; 378 379 // get all folder entries 380 NSArray *folderEntries = [mDocListFeed entriesWithCategoryKind:kGDataCategoryFolderDoc]; 381 382 // get hrefs of folders that already contain the selected doc 383 GDataEntryDocBase *doc = [self selectedDoc]; 384 NSArray *parentLinks = [doc parentLinks]; 385 NSArray *parentHrefs = [parentLinks valueForKey:@"href"]; 386 387 // disable the pop-up if a folder entry is selected 388 BOOL isMovableDocSelected = (doc != nil) 389 && ![doc isKindOfClass:[GDataEntryFolderDoc class]]; 390 [mFolderMembershipPopup setEnabled:isMovableDocSelected]; 391 392 if (isMovableDocSelected) { 393 // step through the folders in this feed, add them to the 394 // pop-up, and add a checkmark to the names of folders that 395 // contain the selected document 396 NSEnumerator *folderEnum = [folderEntries objectEnumerator]; 397 GDataEntryFolderDoc *folderEntry; 398 while ((folderEntry = [folderEnum nextObject]) != nil) { 399 400 NSString *title = [[folderEntry title] stringValue]; 401 NSMenuItem *item = [addMenu addItemWithTitle:title 402 action:@selector(changeFolderSelected:) 403 keyEquivalent:@""]; 404 [item setTarget:self]; 405 [item setRepresentedObject:folderEntry]; 406 407 NSString *folderHref = [[folderEntry selfLink] href]; 408 409 BOOL shouldCheckItem = (folderHref != nil) 410 && [parentHrefs containsObject:folderHref]; 411 [item setState:shouldCheckItem]; 412 } 413 } 414} 415 416- (void)updateSelectedDocumentThumbnailImage { 417 static NSString* priorImageURLStr = nil; 418 419 GDataEntryDocBase *doc = [self selectedDoc]; 420 GDataLink *thumbnailLink = [doc thumbnailLink]; 421 NSString *newImageURLStr = [thumbnailLink href]; 422 423 if (!AreEqualOrBothNil(newImageURLStr, priorImageURLStr)) { 424 // the image has changed 425 priorImageURLStr = newImageURLStr; 426 427 [mDocListImageView setImage:nil]; 428 429 if ([newImageURLStr length] > 0) { 430 // We need an authorized fetcher to download the document thumbnail, as 431 // the fetch requires authentication. 432 // 433 // We could attach an authorizer to the fetcher, but it's simpler to 434 // use the GData service's fetcherService to make the new fetcher, as 435 // that will already have the authorizer attached. 436 GTMHTTPFetcherService *fetcherService = [[self docsService] fetcherService]; 437 GTMHTTPFetcher *fetcher = [fetcherService fetcherWithURLString:newImageURLStr]; 438 [fetcher setCommentWithFormat:@"thumbnail for \"%@\"", [[doc title] stringValue]]; 439 [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { 440 // callback 441 if (error == nil) { 442 NSImage *image = [[[NSImage alloc] initWithData:data] autorelease]; 443 [mDocListImageView setImage:image]; 444 } else { 445 NSLog(@"Error %@ loading image %@", 446 error, [[fetcher mutableRequest] URL]); 447 } 448 }]; 449 } 450 } 451} 452 453- (void)displayAlert:(NSString *)title format:(NSString *)format, ... { 454 NSString *result = format; 455 if (format) { 456 va_list argList; 457 va_start(argList, format); 458 result = [[[NSString alloc] initWithFormat:format 459 arguments:argList] autorelease]; 460 va_end(argList); 461 } 462 NSBeginAlertSheet(title, nil, nil, nil, [self window], nil, nil, 463 nil, nil, @"%@", result); 464} 465 466#pragma mark IBActions 467 468- (IBAction)signInClicked:(id)sender { 469 if (![self isSignedIn]) { 470 // Sign in 471 [self runSigninThenInvokeSelector:@selector(updateUI)]; 472 } else { 473 // Sign out 474 GDataServiceGoogleDocs *service = [self docsService]; 475 476 [GTMOAuth2WindowController removeAuthFromKeychainForName:kKeychainItemName]; 477 [service setAuthorizer:nil]; 478 [self updateUI]; 479 } 480} 481 482- (IBAction)getDocListClicked:(id)sender { 483 if (![self isSignedIn]) { 484 [self runSigninThenInvokeSelector:@selector(fetchDocList)]; 485 } else { 486 [self fetchDocList]; 487 } 488} 489 490- (IBAction)cancelDocListFetchClicked:(id)sender { 491 [mDocListFetchTicket cancelTicket]; 492 [self setDocListFetchTicket:nil]; 493 [self updateUI]; 494} 495 496- (IBAction)cancelRevisionsFetchClicked:(id)sender { 497 [mRevisionFetchTicket cancelTicket]; 498 [self setRevisionFetchTicket:nil]; 499 [self updateUI]; 500} 501 502- (IBAction)viewSelectedDocClicked:(id)sender { 503 504 NSURL *docURL = [[[self selectedDoc] HTMLLink] URL]; 505 506 if (docURL) { 507 [[NSWorkspace sharedWorkspace] openURL:docURL]; 508 } else { 509 NSBeep(); 510 } 511} 512 513#pragma mark - 514 515- (IBAction)downloadSelectedDocClicked:(id)sender { 516 517 GDataEntryDocBase *docEntry = [self selectedDoc]; 518 519 NSString *saveTitle = [[docEntry title] stringValue]; 520 521 [self showDownloadPanelForEntry:docEntry 522 suggestedTitle:saveTitle]; 523} 524 525- (IBAction)downloadSelectedRevisionClicked:(id)sender { 526 527 GDataEntryDocRevision *revisionEntry = [self selectedRevision]; 528 529 GDataEntryDocBase *docEntry = [self selectedDoc]; 530 531 NSString *docName = [[docEntry title] stringValue]; 532 NSString *revisionName = [[revisionEntry title] stringValue]; 533 NSString *saveTitle = [NSString stringWithFormat:@"%@ (%@)", 534 docName, revisionName]; 535 536 // the revision entry doesn't tell us the kind of document being saved, so 537 // we'll explicitly put it into a property of the entry 538 Class documentClass = [docEntry class]; 539 [revisionEntry setProperty:documentClass 540 forKey:@"document class"]; 541 542 [self showDownloadPanelForEntry:revisionEntry 543 suggestedTitle:saveTitle]; 544} 545 546- (void)showDownloadPanelForEntry:(GDataEntryBase *)entry 547 suggestedTitle:(NSString *)title { 548 549 NSString *sourceURI = [[entry content] sourceURI]; 550 if (sourceURI) { 551 // We will download drawings as pdf and other files as text 552 BOOL isDrawing = [entry isKindOfClass:[GDataEntryDrawingDoc class]]; 553 NSString *filename = title; 554 NSString *fileExtension = (isDrawing ? @"pdf" : @"txt"); 555 if (![[title pathExtension] isEqual:fileExtension]) { 556 // The title string needs the file extension to be a file name 557 filename = [title stringByAppendingPathExtension:fileExtension]; 558 } 559 560 NSSavePanel *savePanel = [NSSavePanel savePanel]; 561 [savePanel setNameFieldStringValue:filename]; 562 [savePanel beginSheetModalForWindow:[self window] 563 completionHandler:^(NSInteger result) { 564 // callback 565 if (result == NSOKButton) { 566 // user clicked OK 567 NSString *savePath = [[savePanel URL] path]; 568 [self saveDocumentEntry:entry 569 toPath:savePath]; 570 } 571 }]; 572 } else { 573 NSBeep(); 574 } 575} 576 577// formerly saveSelectedDocumentToPath: 578- (void)saveDocumentEntry:(GDataEntryBase *)docEntry 579 toPath:(NSString *)savePath { 580 // downloading docs, per 581 // http://code.google.com/apis/documents/docs/3.0/developers_guide_protocol.html#DownloadingDocs 582 583 // when downloading a revision entry, we've added a property above indicating 584 // the class of document for which this is a revision 585 Class classProperty = [docEntry propertyForKey:@"document class"]; 586 if (!classProperty) { 587 classProperty = [docEntry class]; 588 } 589 590 // since the user has already fetched the doc list, the service object 591 // has the proper authentication token. 592 GDataServiceGoogleDocs *docsService = [self docsService]; 593 594 BOOL isDrawing = [classProperty isEqual:[GDataEntryDrawingDoc class]]; 595 NSString *exportFormat = (isDrawing ? @"pdf" : @"txt"); 596 [self saveDocEntry:docEntry 597 toPath:savePath 598 exportFormat:exportFormat 599 authService:docsService]; 600} 601 602- (void)saveDocEntry:(GDataEntryBase *)entry 603 toPath:(NSString *)savePath 604 exportFormat:(NSString *)exportFormat 605 authService:(GDataServiceGoogle *)service { 606 607 // the content src attribute is used for downloading 608 NSURL *exportURL = [[entry content] sourceURL]; 609 if (exportURL != nil) { 610 // we'll use GDataQuery as a convenient way to append the exportFormat 611 // parameter of the docs export API to the content src URL 612 GDataQuery *query = [GDataQuery queryWithFeedURL:exportURL]; 613 [query addCustomParameterWithName:@"exportFormat" 614 value:exportFormat]; 615 NSURL *downloadURL = [query URL]; 616 617 // Read the document's contents asynchronously from the network 618 619 // requestForURL:ETag:httpMethod: sets the user agent header of the 620 // request and, when using ClientLogin, adds the authorization header 621 NSURLRequest *request = [service requestForURL:downloadURL 622 ETag:nil 623 httpMethod:nil]; 624 625 GTMHTTPFetcher *fetcher = [GTMHTTPFetcher fetcherWithRequest:request]; 626 [fetcher setAuthorizer:[service authorizer]]; 627 [fetcher setDownloadPath:savePath]; 628 [fetcher setCommentWithFormat:@"downloading \"%@\"", [[entry title] stringValue]]; 629 [fetcher beginFetchWithCompletionHandler:^(NSData *data, NSError *error) { 630 // callback 631 if (error == nil) { 632 // Successfully saved the document 633 // 634 // Since a downloadPath property was specified, the data argument is 635 // nil, and the file data has been written to disk. 636 } else { 637 NSLog(@"Error saving document: %@", error); 638 NSBeep(); 639 } 640 }]; 641 } 642} 643 644/* When signing in with ClientLogin, we need to create a SpreadsheetService 645 instance to do an authenticated download of spreadsheet documents. 646 647 Since this sample signs in with OAuth 2, which allows multiple scopes, 648 we do not need to use a SpreadsheetService, but here is what it looks 649 like for ClientLogin. 650 651- (void)saveSpreadsheet:(GDataEntrySpreadsheetDoc *)docEntry 652 toPath:(NSString *)savePath { 653 // to download a spreadsheet document, we need a spreadsheet service object, 654 // and we first need to fetch a feed or entry with the service object so that 655 // it has a valid auth token 656 GDataServiceGoogleSpreadsheet *spreadsheetService; 657 spreadsheetService = [[[GDataServiceGoogleSpreadsheet alloc] init] autorelease]; 658 659 GDataServiceGoogleDocs *docsService = [self docsService]; 660 [spreadsheetService setUserAgent:[docsService userAgent]]; 661 [spreadsheetService setUserCredentialsWithUsername:[docsService username] 662 password:[docsService password]]; 663 GDataServiceTicket *ticket; 664 ticket = [spreadsheetService authenticateWithDelegate:self 665 didAuthenticateSelector:@selector(spreadsheetTicket:authenticatedWithError:)]; 666 667 // we'll hang on to the spreadsheet service object with a ticket property 668 // since we need it to create an authorized NSURLRequest 669 [ticket setProperty:docEntry forKey:@"docEntry"]; 670 [ticket setProperty:savePath forKey:@"savePath"]; 671} 672 673- (void)spreadsheetTicket:(GDataServiceTicket *)ticket 674 authenticatedWithError:(NSError *)error { 675 if (error == nil) { 676 GDataEntrySpreadsheetDoc *docEntry = [ticket propertyForKey:@"docEntry"]; 677 NSString *savePath = [ticket propertyForKey:@"savePath"]; 678 679 [self saveDocEntry:docEntry 680 toPath:savePath 681 exportFormat:@"tsv" 682 authService:[ticket service]]; 683 } else { 684 // failed to authenticate; give up 685 NSLog(@"Spreadsheet authentication error: %@", error); 686 return; 687 } 688} 689*/ 690 691#pragma mark - 692 693- (IBAction)uploadFileClicked:(id)sender { 694 // ask the user to choose a file 695 NSOpenPanel *openPanel = [NSOpenPanel openPanel]; 696 [openPanel setPrompt:@"Upload"]; 697 [openPanel beginSheetModalForWindow:[self window] 698 completionHandler:^(NSInteger result) { 699 // callback 700 if (result == NSOKButton) { 701 // user chose a file and clicked OK 702 // 703 // start uploading (deferred to the main thread since 704 // we currently have a sheet displayed) 705 NSString *path = [[openPanel URL] path]; 706 [self performSelectorOnMainThread:@selector(uploadFileAtPath:) 707 withObject:path 708 waitUntilDone:NO]; 709 } 710 }]; 711} 712 713- (IBAction)pauseUploadClicked:(id)sender { 714 if ([mUploadTicket isUploadPaused]) { 715 [mUploadTicket resumeUpload]; 716 } else { 717 [mUploadTicket pauseUpload]; 718 } 719 [self updateUI]; 720} 721 722- (IBAction)stopUploadClicked:(id)sender { 723 [mUploadTicket cancelTicket]; 724 [self setUploadTicket:nil]; 725 726 [mUploadProgressIndicator setDoubleValue:0.0]; 727 [self updateUI]; 728} 729 730#pragma mark - 731 732- (IBAction)publishCheckboxClicked:(id)sender { 733 // enable or disable the Update Publishing button 734 [self updateUI]; 735} 736 737- (IBAction)updatePublishingClicked:(id)sender { 738 GDataServiceGoogleDocs *service = [self docsService]; 739 740 GDataEntryDocRevision *revisionEntry = [self selectedRevision]; 741 742 // update the revision elements to match the checkboxes 743 // 744 // we'll modify a copy of the selected entry so we don't leave an inaccurate 745 // entry in the feed if our fetch fails 746 GDataEntryDocRevision *revisionCopy = [[revisionEntry copy] autorelease]; 747 748 BOOL shouldPublish = ([mPublishCheckbox state] == NSOnState); 749 [revisionCopy setPublish:[NSNumber numberWithBool:shouldPublish]]; 750 751 BOOL shouldAutoRepublish = ([mAutoRepublishCheckbox state] == NSOnState); 752 [revisionCopy setPublishAuto:[NSNumber numberWithBool:shouldAutoRepublish]]; 753 754 BOOL shouldPublishExternally = ([mPublishOutsideDomainCheckbox state] == NSOnState); 755 [revisionCopy setPublishOutsideDomain:[NSNumber numberWithBool:shouldPublishExternally]]; 756 757 [service fetchEntryByUpdatingEntry:revisionCopy 758 completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) { 759 // callback 760 if (error == nil) { 761 [self displayAlert:@"Updated" 762 format:@"Updated publish status for \"%@\"", 763 [[entry title] stringValue]]; 764 765 // re-fetch the document list 766 [self fetchRevisionsForSelectedDoc]; 767 } else { 768 [self displayAlert:@"Updated failed" 769 format:@"Failed to update publish status: %@", 770 error]; 771 } 772 }]; 773} 774 775#pragma mark - 776 777- (IBAction)createFolderClicked:(id)sender { 778 GDataServiceGoogleDocs *service = [self docsService]; 779 780 GDataEntryFolderDoc *docEntry = [GDataEntryFolderDoc documentEntry]; 781 782 NSString *title = [NSString stringWithFormat:@"New Folder %@", [NSDate date]]; 783 [docEntry setTitleWithString:title]; 784 785 NSURL *postURL = [[mDocListFeed postLink] URL]; 786 787 [service fetchEntryByInsertingEntry:docEntry 788 forFeedURL:postURL 789 completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) { 790 // callback 791 if (error == nil) { 792 [self displayAlert:@"Created folder" 793 format:@"Created folder \"%@\"", 794 [[entry title] stringValue]]; 795 796 // re-fetch the document list 797 [self fetchDocList]; 798 [self updateUI]; 799 } else { 800 [self displayAlert:@"Create failed" 801 format:@"Folder create failed: %@", error]; 802 } 803 }]; 804} 805 806#pragma mark - 807 808static long long gLargestPriorChangestamp = 0; 809 810- (IBAction)showChangesClicked:(id)sender { 811 NSURL *changesFeedURL = [GDataServiceGoogleDocs changesFeedURLForUserID:kGDataServiceDefaultUser]; 812 GDataServiceGoogleDocs *service = [self docsService]; 813 814 if (gLargestPriorChangestamp == 0) { 815 // First click 816 // 817 // We have not previously fetched the changes feed, so request it without 818 // entries to determine a benchmark changestamp 819 GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:changesFeedURL]; 820 821 // The server currently ignores zero as a max-results value (b/5027926), 822 // so we'll request one entry and ignore it 823 [query setMaxResults:1]; 824 825 GDataServiceTicket *ticket; 826 ticket = [service fetchFeedWithQuery:query 827 completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) { 828 // callback 829 if (error == nil) { 830 GDataFeedDocChange *changeFeed = (GDataFeedDocChange *)feed; 831 NSNumber *num = [changeFeed largestChangestamp]; 832 [self displayAlert:@"Initial changestamp obtained" 833 format:@"Value: %@", num]; 834 gLargestPriorChangestamp = [num longLongValue]; 835 } else { 836 [self displayAlert:@"Fetch failed" 837 format:@"Fetch of changes failed: %@", 838 error]; 839 } 840 }]; 841 // We don't want additional pages of this feed, since we only care about 842 // the changestamp benchmark 843 [ticket setShouldFollowNextLinks:NO]; 844 } else { 845 // Second and later clicks 846 // 847 // We have previously fetched the changes feed, so request all changes 848 // since that benchmark changestamp 849 GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:changesFeedURL]; 850 [query setStartIndex:(1 + gLargestPriorChangestamp)]; 851 852 // We'll reduce the number pages fetches needed to obtain the entire feed 853 // by requesting a large page size. A large page size and automatic next 854 // link following are not really practical on mobile devices, though, as 855 // the entries of the changes feed are big. 856 [query setMaxResults:100]; 857 858 [service fetchFeedWithQuery:query 859 completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) { 860 // callback 861 if (error == nil) { 862 // We obtained a feed of changes 863 // 864 // Report the titles of added and updated docs, and the 865 // entry identifier of removed docs 866 GDataFeedDocChange *changeFeed = (GDataFeedDocChange *)feed; 867 868 NSMutableString *output = [NSMutableString stringWithFormat: 869 @"Changed entries (%lu):", 870 (unsigned long) [[feed entries] count]]; 871 for (GDataEntryDocBase *entry in changeFeed) { 872 if ([entry isRemoved]) { 873 // Removed 874 [output appendFormat:@"\nRemoved (%@)", [entry identifier]]; 875 } else { 876 // Added or updated 877 [output appendFormat:@"\n%@", [[entry title] stringValue]]; 878 } 879 } 880 [self displayAlert:@"Changed entries" 881 format:@"%@", output]; 882 883 // Update the benchmark value 884 gLargestPriorChangestamp = [[changeFeed largestChangestamp] longLongValue]; 885 } else { 886 [self displayAlert:@"Fetch failed" 887 format:@"Fetch of changes since %lld failed: %@", 888 gLargestPriorChangestamp, error]; 889 } 890 }]; 891 } 892} 893 894#pragma mark - 895 896- (IBAction)deleteSelectedDocClicked:(id)sender { 897 898 GDataEntryDocBase *doc = [self selectedDoc]; 899 if (doc) { 900 // make the user confirm that the selected doc should be deleted 901 NSBeginAlertSheet(@"Delete Document", @"Delete", @"Cancel", nil, 902 [self window], self, 903 @selector(deleteDocSheetDidEnd:returnCode:contextInfo:), 904 nil, nil, @"Delete the document \"%@\"?", 905 [[doc title] stringValue]); 906 } 907} 908 909// delete dialog callback 910- (void)deleteDocSheetDidEnd:(NSWindow *)sheet returnCode:(int)returnCode contextInfo:(void *)contextInfo { 911 912 if (returnCode == NSAlertDefaultReturn) { 913 914 // delete the document entry 915 GDataEntryDocBase *entry = [self selectedDoc]; 916 917 if (entry) { 918 GDataServiceGoogleDocs *service = [self docsService]; 919 [service deleteEntry:entry 920 delegate:self 921 didFinishSelector:@selector(deleteDocEntryTicket:deletedEntry:error:)]; 922 } 923 } 924} 925 926// entry delete callback 927- (void)deleteDocEntryTicket:(GDataServiceTicket *)ticket 928 deletedEntry:(GDataEntryDocBase *)object 929 error:(NSError *)error { 930 if (error == nil) { 931 // note: object is nil in the delete callback 932 [self displayAlert:@"Deleted Doc" 933 format:@"Document deleted"]; 934 935 // re-fetch the document list 936 [self fetchDocList]; 937 [self updateUI]; 938 } else { 939 [self displayAlert:@"Delete failed" 940 format:@"Document delete failed: %@", error]; 941 } 942} 943 944#pragma mark - 945 946- (IBAction)duplicateSelectedDocClicked:(id)sender { 947 948 GDataEntryDocBase *selectedDoc = [self selectedDoc]; 949 if (selectedDoc) { 950 // make a new entry of the same class as the selected document entry, 951 // with just the title set and an identifier equal to the selected 952 // doc's resource ID 953 GDataEntryDocBase *newEntry = [[selectedDoc class] documentEntry]; 954 955 [newEntry setIdentifier:[selectedDoc resourceID]]; 956 957 NSString *oldTitle = [[selectedDoc title] stringValue]; 958 NSString *newTitle = [oldTitle stringByAppendingString:@" copy"]; 959 [newEntry setTitleWithString:newTitle]; 960 961 GDataServiceGoogleDocs *service = [self docsService]; 962 NSURL *postURL = [[mDocListFeed postLink] URL]; 963 964 [service fetchEntryByInsertingEntry:newEntry 965 forFeedURL:postURL 966 completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) { 967 // callback 968 if (error == nil) { 969 [self displayAlert:@"Copied Doc" 970 format:@"Document duplicate \"%@\" created", [[newEntry title] stringValue]]; 971 972 // re-fetch the document list 973 [self fetchDocList]; 974 [self updateUI]; 975 } else { 976 [self displayAlert:@"Copy failed" 977 format:@"Document duplicate failed: %@", error]; 978 } 979 }]; 980 } 981} 982 983#pragma mark - 984 985- (IBAction)changeFolderSelected:(id)sender { 986 987 // the selected menu item represents a folder; fetch the folder's feed 988 // 989 // with the folder's feed, we can insert or remove the selected document 990 // entry in the folder's feed 991 992 GDataEntryFolderDoc *folderEntry = [sender representedObject]; 993 NSURL *folderFeedURL = [[folderEntry content] sourceURL]; 994 if (folderFeedURL != nil) { 995 996 GDataServiceGoogleDocs *service = [self docsService]; 997 998 GDataServiceTicket *ticket; 999 ticket = [service fetchFeedWithURL:folderFeedURL 1000 delegate:self 1001 didFinishSelector:@selector(fetchFolderTicket:finishedWithFeed:error:)]; 1002 1003 // save the selected doc in the ticket's userData 1004 GDataEntryDocBase *doc = [self selectedDoc]; 1005 [ticket setUserData:doc]; 1006 } 1007} 1008 1009// folder feed fetch callback 1010- (void)fetchFolderTicket:(GDataServiceTicket *)ticket 1011 finishedWithFeed:(GDataFeedDocList *)feed 1012 error:(NSError *)error { 1013 1014 if (error == nil) { 1015 GDataEntryDocBase *docEntry = [ticket userData]; 1016 1017 GDataServiceGoogleDocs *service = [self docsService]; 1018 GDataServiceTicket *ticket2; 1019 1020 // if the entry is not in the folder's feed, insert it; otherwise, delete 1021 // it from the folder's feed 1022 GDataEntryDocBase *foundEntry = [feed entryForIdentifier:[docEntry identifier]]; 1023 if (foundEntry == nil) { 1024 // the doc isn't currently in this folder's feed 1025 // 1026 // post the doc to the folder's feed 1027 NSURL *postURL = [[feed postLink] URL]; 1028 1029 ticket2 = [service fetchEntryByInsertingEntry:docEntry 1030 forFeedURL:postURL 1031 completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) { 1032 // callback 1033 if (error == nil) { 1034 [self displayAlert:@"Added" 1035 format:@"Added document \"%@\" to feed \"%@\"", 1036 [[entry title] stringValue], 1037 [[feed title] stringValue]]; 1038 1039 // re-fetch the document list 1040 [self fetchDocList]; 1041 [self updateUI]; 1042 } else { 1043 [self displayAlert:@"Insert failed" 1044 format:@"Insert to folder feed failed: %@", error]; 1045 } 1046 }]; 1047 } else { 1048 // the doc is alrady in the folder's feed, so remove it 1049 ticket2 = [service deleteEntry:foundEntry 1050 completionHandler:^(GDataServiceTicket *ticket, id nilObject, NSError *error) { 1051 // callback 1052 if (error == nil) { 1053 [self displayAlert:@"Removed" 1054 format:@"Removed document from feed \"%@\"", [[feed title] stringValue]]; 1055 1056 // re-fetch the document list 1057 [self fetchDocList]; 1058 [self updateUI]; 1059 } else { 1060 [self displayAlert:@"Fetch failed" 1061 format:@"Remove from folder feed failed: %@", 1062 error]; 1063 } 1064 }]; 1065 } 1066 } else { 1067 // failed to fetch feed of folders 1068 [self displayAlert:@"Fetch failed" 1069 format:@"Fetch of folder feed failed: %@", error]; 1070 } 1071} 1072 1073#pragma mark - 1074 1075- (IBAction)APIConsoleClicked:(id)sender { 1076 NSURL *url = [NSURL URLWithString:@"https://code.google.com/apis/console"]; 1077 [[NSWorkspace sharedWorkspace] openURL:url]; 1078} 1079 1080- (IBAction)loggingCheckboxClicked:(id)sender { 1081 [GTMHTTPFetcher setLoggingEnabled:[sender state]]; 1082} 1083 1084#pragma mark - 1085 1086// get an docList service object with the current username/password 1087// 1088// A "service" object handles networking tasks. Service objects 1089// contain user authentication information as well as networking 1090// state information (such as cookies and the "last modified" date for 1091// fetched data.) 1092 1093- (GDataServiceGoogleDocs *)docsService { 1094 1095 static GDataServiceGoogleDocs* service = nil; 1096 1097 if (!service) { 1098 service = [[GDataServiceGoogleDocs alloc] init]; 1099 1100 [service setShouldCacheResponseData:YES]; 1101 [service setServiceShouldFollowNextLinks:YES]; 1102 [service setIsServiceRetryEnabled:YES]; 1103 } 1104 1105 return service; 1106} 1107 1108// get the doc selected in the list, or nil if none 1109- (GDataEntryDocBase *)selectedDoc { 1110 1111 int rowIndex = [mDocListTable selectedRow]; 1112 if (rowIndex > -1) { 1113 GDataEntryDocBase *doc = [mDocListFeed entryAtIndex:rowIndex]; 1114 return doc; 1115 } 1116 return nil; 1117} 1118 1119// get the doc revision in the list, or nil if none 1120- (GDataEntryDocRevision *)selectedRevision { 1121 1122 int rowIndex = [mRevisionsTable selectedRow]; 1123 if (rowIndex > -1) { 1124 GDataEntryDocRevision *entry = [mRevisionFeed entryAtIndex:rowIndex]; 1125 return entry; 1126 } 1127 return nil; 1128} 1129 1130#pragma mark Fetch doc list user metadata 1131 1132- (void)fetchMetadataEntry { 1133 [self setMetadataEntry:nil]; 1134 1135 NSURL *entryURL = [GDataServiceGoogleDocs metadataEntryURLForUserID:kGDataServiceDefaultUser]; 1136 GDataServiceGoogleDocs *service = [self docsService]; 1137 [service fetchEntryWithURL:entryURL 1138 completionHandler:^(GDataServiceTicket *ticket, GDataEntryBase *entry, NSError *error) { 1139 // callback 1140 [self setMetadataEntry:(GDataEntryDocListMetadata *)entry]; 1141 1142 // enable or disable features 1143 [self updateUI]; 1144 1145 if (error != nil) { 1146 NSLog(@"Error fetching user metadata: %@", error); 1147 } 1148 }]; 1149} 1150 1151#pragma mark Fetch doc list 1152 1153// begin retrieving the list of the user's docs 1154- (void)fetchDocList { 1155 1156 [self setDocListFeed:nil]; 1157 [self setDocListFetchError:nil]; 1158 [self setDocListFetchTicket:nil]; 1159 1160 GDataServiceGoogleDocs *service = [self docsService]; 1161 GDataServiceTicket *ticket; 1162 1163 // Fetching a feed gives us 25 responses by default. We need to use 1164 // the feed's "next" link to get any more responses. If we want more than 25 1165 // at a time, instead of calling fetchDocsFeedWithURL, we can create a 1166 // GDataQueryDocs object, as shown here. 1167 1168 NSURL *feedURL = [GDataServiceGoogleDocs docsFeedURL]; 1169 1170 GDataQueryDocs *query = [GDataQueryDocs documentQueryWithFeedURL:feedURL]; 1171 [query setMaxResults:1000]; 1172 [query setShouldShowFolders:YES]; 1173 1174 ticket = [service fetchFeedWithQuery:query 1175 completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) { 1176 // callback 1177 [self setDocListFeed:(GDataFeedDocList *)feed]; 1178 [self setDocListFetchError:error]; 1179 [self setDocListFetchTicket:nil]; 1180 1181 [self updateUI]; 1182 }]; 1183 1184 [self setDocListFetchTicket:ticket]; 1185 1186 // update our metadata entry for this user 1187 [self fetchMetadataEntry]; 1188 1189 [self updateUI]; 1190} 1191 1192#pragma mark Fetch revisions or content feed 1193 1194- (void)fetchRevisionsForSelectedDoc { 1195 1196 [self setRevisionFeed:nil]; 1197 [self setRevisionFetchError:nil]; 1198 [self setRevisionFetchTicket:nil]; 1199 1200 GDataEntryDocBase *selectedDoc = [self selectedDoc]; 1201 GDataFeedLink *revisionFeedLink = [selectedDoc revisionFeedLink]; 1202 NSURL *revisionFeedURL = [revisionFeedLink URL]; 1203 if (revisionFeedURL) { 1204 1205 GDataServiceGoogleDocs *service = [self docsService]; 1206 GDataServiceTicket *ticket; 1207 ticket = [service fetchFeedWithURL:revisionFeedURL 1208 completionHandler:^(GDataServiceTicket *ticket, GDataFeedBase *feed, NSError *error) { 1209 // callback 1210 [self setRevisionFeed:(GDataFeedDocRevision *)feed]; 1211 [self setRevisionFetchError:error]; 1212 [self setRevisionFetchTicket:nil]; 1213 1214 [self updateUI]; 1215 }]; 1216 1217 [self setRevisionFetchTicket:ticket]; 1218 1219 } 1220 1221 [self updateUI]; 1222} 1223 1224#pragma mark Upload 1225 1226- (void)getMIMEType:(NSString **)mimeType andEntryClass:(Class *)class forExtension:(NSString *)extension { 1227 1228 // Mac OS X's UTI database doesn't know MIME types for .doc and .xls 1229 // so GDataEntryBase's MIMETypeForFileAtPath method isn't helpful here 1230 1231 struct MapEntry { 1232 NSString *extension; 1233 NSString *mimeType; 1234 NSString *className; 1235 }; 1236 1237 static struct MapEntry sMap[] = { 1238 { @"csv", @"text/csv", @"GDataEntryStandardDoc" }, 1239 { @"doc", @"application/msword", @"GDataEntryStandardDoc" }, 1240 { @"docx", @"application/vnd.openxmlformats-officedocument.wordprocessingml.document", @"GDataEntryStandardDoc" }, 1241 { @"ods", @"application/vnd.oasis.opendocument.spreadsheet", @"GDataEntrySpreadsheetDoc" }, 1242 { @"odt", @"application/vnd.oasis.opendocument.text", @"GDataEntryStandardDoc" }, 1243 { @"pps", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" }, 1244 { @"ppt", @"application/vnd.ms-powerpoint", @"GDataEntryPresentationDoc" }, 1245 { @"rtf", @"application/rtf", @"GDataEntryStandardDoc" }, 1246 { @"sxw", @"application/vnd.sun.xml.writer", @"GDataEntryStandardDoc" }, 1247 { @"txt", @"text/plain", @"GDataEntryStandardDoc" }, 1248 { @"xls", @"application/vnd.ms-excel", @"GDataEntrySpreadsheetDoc" }, 1249 { @"xlsx", @"application/vnd.openxmlformats-officedocument.spreadsheetml.sheet", @"GDataEntrySpreadsheetDoc" }, 1250 { @"jpg", @"image/jpeg", @"GDataEntryStandardDoc" }, 1251 { @"jpeg", @"image/jpeg", @"GDataEntryStandardDoc" }, 1252 { @"png", @"image/png", @"GDataEntryStandardDoc" }, 1253 { @"bmp", @"image/bmp", @"GDataEntryStandardDoc" }, 1254 { @"gif", @"image/gif", @"GDataEntryStandardDoc" }, 1255 { @"html", @"text/html", @"GDataEntryStandardDoc" }, 1256 { @"htm", @"text/html", @"GDataEntryStandardDoc" }, 1257 { @"tsv", @"text/tab-separated-values", @"GDataEntryStandardDoc" }, 1258 { @"tab", @"text/tab-separated-values", @"GDataEntryStandardDoc" }, 1259 { @"pdf", @"application/pdf", @"GDataEntryPDFDoc" }, 1260 { nil, nil, nil } 1261 }; 1262 1263 NSString *lowerExtn = [extension lowercaseString]; 1264 1265 for (int idx = 0; sMap[idx].extension != nil; idx++) { 1266 if ([lowerExtn isEqual:sMap[idx].extension]) { 1267 *mimeType = sMap[idx].mimeType; 1268 *class = NSClassFromString(sMap[idx].className); 1269 return; 1270 } 1271 } 1272 1273 *mimeType = nil; 1274 *class = nil; 1275 return; 1276} 1277 1278- (void)uploadFileAtPath:(NSString *)path { 1279 1280 NSString *errorMsg = nil; 1281 1282 // make a new entry for the file 1283 1284 NSString *mimeType = nil; 1285 Class entryClass = nil; 1286 1287 NSString *extn = [path pathExtension]; 1288 [self getMIMEType:&mimeType andEntryClass:&entryClass forExtension:extn]; 1289 1290 if (!mimeType) { 1291 // for other file types, see if we can get the type from the Mac OS 1292 // and use a generic file document entry class 1293 mimeType = [GDataUtilities MIMETypeForFileAtPath:path 1294 defaultMIMEType:nil]; 1295 entryClass = [GDataEntryFileDoc class]; 1296 } 1297 1298 if (!mimeType) { 1299 errorMsg = [NSString stringWithFormat:@"need MIME type for file %@", path]; 1300 } 1301 1302 if (mimeType && entryClass) { 1303 1304 GDataEntryDocBase *newEntry = [entryClass documentEntry]; 1305 1306 NSString *title = [[NSFileManager defaultManager] displayNameAtPath:path]; 1307 [newEntry setTitleWithString:title]; 1308 1309 NSFileHandle *uploadFileHandle = [NSFileHandle fileHandleForReadingAtPath:path]; 1310 if (!uploadFileHandle) { 1311 errorMsg = [NSString stringWithFormat:@"cannot read file %@", path]; 1312 } 1313 1314 if (uploadFileHandle) { 1315 [newEntry setUploadFileHandle:uploadFileHandle]; 1316 1317 [newEntry setUploadMIMEType:mimeType]; 1318 [newEntry setUploadSlug:[path lastPathComponent]]; 1319 1320 NSURL *uploadURL = [GDataServiceGoogleDocs docsUploadURL]; 1321 1322 // add the OCR or translation parameters, if the user set the pop-up 1323 // button appropriately 1324 int popupTag = [[mUploadPopup selectedItem] tag]; 1325 if (popupTag != 0) { 1326 NSString *targetLanguage = nil; 1327 BOOL shouldConvertToGoogleDoc = YES; 1328 BOOL shouldOCR = NO; 1329 1330 switch (popupTag) { 1331 // upload original file 1332 case kUploadOriginal: shouldConvertToGoogleDoc = NO; break; 1333 1334 // OCR 1335 case kUploadOCR: shouldOCR = YES; break; 1336 1337 // translation 1338 case kUploadDE: targetLanguage = @"de"; break; // german 1339 case kUploadJA: targetLanguage = @"ja"; break; // japanese 1340 case kUploadEN: targetLanguage = @"en"; break; // english 1341 1342 default: break; 1343 } 1344 1345 GDataQueryDocs *query = [GDataQueryDocs queryWithFeedURL:uploadURL]; 1346 1347 [query setShouldConvertUpload:shouldConvertToGoogleDoc]; 1348 [query setShouldOCRUpload:shouldOCR]; 1349 1350 // we'll leave out the sourceLanguage parameter to get 1351 // auto-detection of the file's language 1352 // 1353 // language codes: http://www.loc.gov/standards/iso639-2/php/code_list.php 1354 [query setTargetLanguage:targetLanguage]; 1355 1356 uploadURL = [query URL]; 1357 } 1358 1359 // make service tickets call back into our upload progress selector 1360 GDataServiceGoogleDocs *service = [self docsService]; 1361 1362 // insert the entry into the docList feed 1363 // 1364 // to update (replace) an existing entry by uploading a new file, 1365 // use the fetchEntryByUpdatingEntry:forEntryURL: with the URL from 1366 // the entry's uploadEditLink 1367 GDataServiceTicket *ticket; 1368 ticket = [service fetchEntryByInsertingEntry:newEntry 1369 forFeedURL:uploadURL 1370 delegate:self 1371 didFinishSelector:@selector(uploadFileTicket:finishedWithEntry:error:)]; 1372 1373 [ticket setUploadProgressHandler:^(GDataServiceTicketBase *ticket, unsigned long long numberOfBytesRead, unsigned long long dataLength) { 1374 // progress callback 1375 [mUploadProgressIndicator setMinValue:0.0]; 1376 [mUploadProgressIndicator setMaxValue:(double)dataLength]; 1377 [mUploadProgressIndicator setDoubleValue:(double)numberOfBytesRead]; 1378 }]; 1379 1380 // we turned automatic retry on when we allocated the service, but we 1381 // could also turn it on just for this ticket 1382 1383 [self setUploadTicket:ticket]; 1384 } 1385 } 1386 1387 if (errorMsg) { 1388 // we're currently in the middle of the file selection sheet, so defer our 1389 // error sheet 1390 [self displayAlert:@"Upload Error" 1391 format:@"%@", errorMsg]; 1392 } 1393 1394 [self updateUI]; 1395} 1396 1397// upload finished callback 1398- (void)uploadFileTicket:(GDataServiceTicket *)ticket 1399 finishedWithEntry:(GDataEntryDocBase *)entry 1400 error:(NSError *)error { 1401 1402 [self setUploadTicket:nil]; 1403 [mUploadProgressIndicator setDoubleValue:0.0]; 1404 1405 if (error == nil) { 1406 // refetch the current doc list 1407 [self fetchDocList]; 1408 1409 // tell the user that the add worked 1410 [self displayAlert:@"Uploaded file" 1411 format:@"File uploaded: %@", [[entry title] stringValue]]; 1412 } else { 1413 [self displayAlert:@"Upload failed" 1414 format:@"File upload failed: %@", error]; 1415 } 1416 [self updateUI]; 1417} 1418 1419#pragma mark Client ID Sheet 1420 1421// Client ID and Client Secret Sheet 1422// 1423// Sample apps need this sheet to ask for the client ID and client secret 1424// strings 1425// 1426// Your application will just hardcode the client ID and client secret strings 1427// into the source rather than ask the user for them. 1428// 1429// The string values are obtained from the API Console, 1430// https://code.google.com/apis/console 1431 1432- (IBAction)clientIDClicked:(id)sender { 1433 // Show the sheet for developers to enter their client ID and client secret 1434 [NSApp beginSheet:mClientIDSheet 1435 modalForWindow:[self window] 1436 modalDelegate:self 1437 didEndSelector:@selector(clientIDSheetDidEnd:returnCode:contextInfo:) 1438 contextInfo:NULL]; 1439} 1440 1441- (IBAction)clientIDDoneClicked:(id)sender { 1442 [NSApp endSheet:mClientIDSheet returnCode:NSOKButton]; 1443} 1444 1445- (void)clientIDSheetDidEnd:(NSWindow *)sheet returnCode:(NSInteger)returnCode contextInfo:(void *)contextInfo { 1446 [sheet orderOut:self]; 1447 [self updateUI]; 1448} 1449 1450#pragma mark TableView delegate methods 1451 1452// 1453// table view delegate methods 1454// 1455 1456- (void)tableViewSelectionDidChange:(NSNotification *)notification { 1457 if ([notification object] == mDocListTable) { 1458 // the user selected a document entry, so fetch its revisions 1459 [self fetchRevisionsForSelectedDoc]; 1460 } else { 1461 // the user selected a revision entry 1462 // 1463 // update the publishing checkboxes to match the newly-selected revision 1464 1465 GDataEntryDocRevision *selectedRevision = [self selectedRevision]; 1466 1467 BOOL isPublished = [[selectedRevision publish] boolValue]; 1468 [mPublishCheckbox setState:(isPublished ? NSOnState : NSOffState)]; 1469 1470 BOOL isAutoRepublished = [[selectedRevision publishAuto] boolValue]; 1471 [mAutoRepublishCheckbox setState:(isAutoRepublished ? NSOnState : NSOffState)]; 1472 1473 BOOL isExternalPublished = [[selectedRevision publishOutsideDomain] boolValue]; 1474 [mPublishOutsideDomainCheckbox setState:(isExternalPublished ? NSOnState : NSOffState)]; 1475 1476 [self updateUI]; 1477 } 1478} 1479 1480// table view data source methods 1481- (int)numberOfRowsInTableView:(NSTableView *)tableView { 1482 if (tableView == mDocListTable) { 1483 return [[mDocListFeed entries] count]; 1484 } 1485 1486 if (tableView == mRevisionsTable) { 1487 return [[mRevisionFeed entries] count]; 1488 } 1489 1490 return 0; 1491} 1492 1493- (id)tableView:(NSTableView *)tableView objectValueForTableColumn:(NSTableColumn *)tableColumn row:(int)row { 1494 1495 if (tableView == mDocListTable) { 1496 // get the docList entry's title, and the kind of document 1497 GDataEntryDocBase *doc = [mDocListFeed entryAtIndex:row]; 1498 1499 NSString *docKind = @"unknown"; 1500 1501 // the kind category for a doc entry includes a label like "document" 1502 // or "spreadsheet" 1503 NSArray *categories; 1504 categories = [GDataCategory categoriesWithScheme:kGDataCategoryScheme 1505 fromCategories:[doc categories]]; 1506 if ([categories count] >= 1) { 1507 docKind = [[categories objectAtIndex:0] label]; 1508 } 1509 1510 // mark if the document is starred 1511 if ([doc isStarred]) { 1512 const UniChar kStarChar = 0x2605; 1513 docKind = [NSString stringWithFormat:@"%C, %@", kStarChar, docKind]; 1514 } 1515 1516 NSString *displayStr = [NSString stringWithFormat:@"%@ (%@)", 1517 [[doc title] stringValue], docKind]; 1518 return displayStr; 1519 } 1520 1521 if (tableView == mRevisionsTable) { 1522 // get the revision entry 1523 GDataEntryDocRevision *revisionEntry; 1524 revisionEntry = [mRevisionFeed entryAtIndex:row]; 1525 1526 NSString *displayStr = [NSString stringWithFormat:@"%@ (edited %@)", 1527 [[revisionEntry title] stringValue], 1528 [[revisionEntry editedDate] date]]; 1529 return displayStr; 1530 } 1531 return nil; 1532} 1533 1534#pragma mark Setters and Getters 1535 1536- (GDataFeedDocList *)docListFeed { 1537 return mDocListFeed; 1538} 1539 1540- (void)setDocListFeed:(GDataFeedDocList *)feed { 1541 [mDocListFeed autorelease]; 1542 mDocListFeed = [feed retain]; 1543} 1544 1545- (NSError *)docListFetchError { 1546 return mDocListFetchError; 1547} 1548 1549- (void)setDocListFetchError:(NSError *)error { 1550 [mDocListFetchError release]; 1551 mDocListFetchError = [error retain]; 1552} 1553 1554- (GDataServiceTicket *)docListFetchTicket { 1555 return mDocListFetchTicket; 1556} 1557 1558- (void)setDocListFetchTicket:(GDataServiceTicket *)ticket { 1559 [mDocListFetchTicket release]; 1560 mDocListFetchTicket = [ticket retain]; 1561} 1562 1563 1564- (GDataFeedDocRevision *)revisionFeed { 1565 return mRevisionFeed; 1566} 1567 1568- (void)setRevisionFeed:(GDataFeedDocRevision *)feed { 1569 [mRevisionFeed autorelease]; 1570 mRevisionFeed = [feed retain]; 1571} 1572 1573- (NSError *)revisionFetchError { 1574 return mRevisionFetchError; 1575} 1576 1577- (void)setRevisionFetchError:(NSError *)error { 1578 [mRevisionFetchError release]; 1579 mRevisionFetchError = [error retain]; 1580} 1581 1582- (GDataServiceTicket *)revisionFetchTicket { 1583 return mRevisionFetchTicket; 1584} 1585 1586- (void)setRevisionFetchTicket:(GDataServiceTicket *)ticket { 1587 [mRevisionFetchTicket release]; 1588 mRevisionFetchTicket = [ticket retain]; 1589} 1590 1591 1592- (GDataEntryDocListMetadata *)metadataEntry { 1593 return mMetadataEntry; 1594} 1595 1596- (void)setMetadataEntry:(GDataEntryDocListMetadata *)entry { 1597 [mMetadataEntry release]; 1598 mMetadataEntry = [entry retain]; 1599} 1600 1601 1602- (GDataServiceTicket *)uploadTicket { 1603 return mUploadTicket; 1604} 1605 1606- (void)setUploadTicket:(GDataServiceTicket *)ticket { 1607 [mUploadTicket release]; 1608 mUploadTicket = [ticket retain]; 1609} 1610 1611@end