PageRenderTime 53ms CodeModel.GetById 11ms RepoModel.GetById 0ms app.codeStats 1ms

/core/externals/update-engine/externals/gdata-objectivec-client/Examples/DocsSample/DocsSampleWindowController.m

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