OLD | NEW |
1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. | 1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #import "content/browser/web_contents/web_drag_source_mac.h" | 5 #import "content/browser/web_contents/web_drag_source_mac.h" |
6 | 6 |
7 #include <sys/param.h> | 7 #include <sys/param.h> |
8 | 8 |
9 #include "base/bind.h" | 9 #include "base/bind.h" |
10 #include "base/files/file_path.h" | 10 #include "base/files/file_path.h" |
11 #include "base/mac/mac_logging.h" | |
12 #include "base/mac/mac_util.h" | 11 #include "base/mac/mac_util.h" |
13 #include "base/mac/scoped_aedesc.h" | |
14 #include "base/pickle.h" | 12 #include "base/pickle.h" |
15 #include "base/strings/string_util.h" | 13 #include "base/strings/string_util.h" |
16 #include "base/strings/sys_string_conversions.h" | 14 #include "base/strings/sys_string_conversions.h" |
17 #include "base/strings/utf_string_conversions.h" | 15 #include "base/strings/utf_string_conversions.h" |
18 #include "base/threading/thread.h" | 16 #include "base/threading/thread.h" |
19 #include "base/threading/thread_restrictions.h" | 17 #include "base/threading/thread_restrictions.h" |
20 #include "content/browser/browser_thread_impl.h" | 18 #include "content/browser/browser_thread_impl.h" |
21 #include "content/browser/download/drag_download_file.h" | 19 #include "content/browser/download/drag_download_file.h" |
22 #include "content/browser/download/drag_download_util.h" | 20 #include "content/browser/download/drag_download_util.h" |
23 #include "content/browser/renderer_host/render_view_host_impl.h" | 21 #include "content/browser/renderer_host/render_view_host_impl.h" |
(...skipping 62 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
86 // This helper's sole task is to write out data for a promised file; the caller | 84 // This helper's sole task is to write out data for a promised file; the caller |
87 // is responsible for opening the file. It takes the drop data and an open file | 85 // is responsible for opening the file. It takes the drop data and an open file |
88 // stream. | 86 // stream. |
89 void PromiseWriterHelper(const DropData& drop_data, | 87 void PromiseWriterHelper(const DropData& drop_data, |
90 scoped_ptr<FileStream> file_stream) { | 88 scoped_ptr<FileStream> file_stream) { |
91 DCHECK(file_stream); | 89 DCHECK(file_stream); |
92 file_stream->WriteSync(drop_data.file_contents.data(), | 90 file_stream->WriteSync(drop_data.file_contents.data(), |
93 drop_data.file_contents.length()); | 91 drop_data.file_contents.length()); |
94 } | 92 } |
95 | 93 |
96 // Returns the drop location from a pasteboard. | |
97 NSString* GetDropLocation(NSPasteboard* pboard) { | |
98 // The API to get the drop location during a callback from | |
99 // kPasteboardTypeFileURLPromise is PasteboardCopyPasteLocation, which takes | |
100 // a PasteboardRef, which isn't bridged with NSPasteboard. Ugh. | |
101 | |
102 PasteboardRef pb_ref = NULL; | |
103 OSStatus status = PasteboardCreate(base::mac::NSToCFCast([pboard name]), | |
104 &pb_ref); | |
105 if (status != noErr || !pb_ref) { | |
106 OSSTATUS_DCHECK(false, status) << "Error finding Carbon pasteboard"; | |
107 return nil; | |
108 } | |
109 base::ScopedCFTypeRef<PasteboardRef> pb_ref_scoper(pb_ref); | |
110 PasteboardSynchronize(pb_ref); | |
111 | |
112 CFURLRef drop_url = NULL; | |
113 status = PasteboardCopyPasteLocation(pb_ref, &drop_url); | |
114 if (status != noErr || !drop_url) { | |
115 OSSTATUS_DCHECK(false, status) << "Error finding drop location"; | |
116 return nil; | |
117 } | |
118 base::ScopedCFTypeRef<CFURLRef> drop_url_scoper(drop_url); | |
119 | |
120 NSString* drop_path = [base::mac::CFToNSCast(drop_url) path]; | |
121 return drop_path; | |
122 } | |
123 | |
124 void SelectFileInFinder(const base::FilePath& file_path) { | |
125 DCHECK([NSThread isMainThread]); | |
126 | |
127 // Create the target of this AppleEvent, the Finder. | |
128 base::mac::ScopedAEDesc<AEAddressDesc> address; | |
129 const OSType finder_creator_code = 'MACS'; | |
130 OSErr status = AECreateDesc(typeApplSignature, // type | |
131 &finder_creator_code, // data | |
132 sizeof(finder_creator_code), // dataSize | |
133 address.OutPointer()); // result | |
134 if (status != noErr) { | |
135 OSSTATUS_LOG(WARNING, status) << "Could not create SelectFile() AE target"; | |
136 return; | |
137 } | |
138 | |
139 // Build the AppleEvent data structure that instructs Finder to select files. | |
140 base::mac::ScopedAEDesc<AppleEvent> the_event; | |
141 status = AECreateAppleEvent(kAEMiscStandards, // theAEEventClass | |
142 kAESelect, // theAEEventID | |
143 address, // target | |
144 kAutoGenerateReturnID, // returnID | |
145 kAnyTransactionID, // transactionID | |
146 the_event.OutPointer()); // result | |
147 if (status != noErr) { | |
148 OSSTATUS_LOG(WARNING, status) << "Could not create SelectFile() AE event"; | |
149 return; | |
150 } | |
151 | |
152 // Create the list of files (only ever one) to select. | |
153 base::mac::ScopedAEDesc<AEDescList> file_list; | |
154 status = AECreateList(NULL, // factoringPtr | |
155 0, // factoredSize | |
156 false, // isRecord | |
157 file_list.OutPointer()); // resultList | |
158 if (status != noErr) { | |
159 OSSTATUS_LOG(WARNING, status) | |
160 << "Could not create SelectFile() AE file list"; | |
161 return; | |
162 } | |
163 | |
164 // Add the single path to the file list. | |
165 NSURL* url = [NSURL fileURLWithPath:SysUTF8ToNSString(file_path.value())]; | |
166 base::ScopedCFTypeRef<CFDataRef> url_data( | |
167 CFURLCreateData(kCFAllocatorDefault, base::mac::NSToCFCast(url), | |
168 kCFStringEncodingUTF8, true)); | |
169 status = AEPutPtr(file_list.OutPointer(), // theAEDescList | |
170 0, // index | |
171 typeFileURL, // typeCode | |
172 CFDataGetBytePtr(url_data), // dataPtr | |
173 CFDataGetLength(url_data)); // dataSize | |
174 if (status != noErr) { | |
175 OSSTATUS_LOG(WARNING, status) | |
176 << "Could not add file path to AE list in SelectFile()"; | |
177 return; | |
178 } | |
179 | |
180 // Attach the file list to the AppleEvent. | |
181 status = AEPutParamDesc(the_event.OutPointer(), // theAppleEvent | |
182 keyDirectObject, // theAEKeyword | |
183 file_list); // theAEDesc | |
184 if (status != noErr) { | |
185 OSSTATUS_LOG(WARNING, status) | |
186 << "Could not put the AE file list the path in SelectFile()"; | |
187 return; | |
188 } | |
189 | |
190 // Send the actual event. Do not care about the reply. | |
191 base::mac::ScopedAEDesc<AppleEvent> reply; | |
192 status = AESend(the_event, // theAppleEvent | |
193 reply.OutPointer(), // reply | |
194 kAENoReply + kAEAlwaysInteract, // sendMode | |
195 kAENormalPriority, // sendPriority | |
196 kAEDefaultTimeout, // timeOutInTicks | |
197 NULL, // idleProc | |
198 NULL); // filterProc | |
199 if (status != noErr) { | |
200 OSSTATUS_LOG(WARNING, status) | |
201 << "Could not send AE to Finder in SelectFile()"; | |
202 } | |
203 } | |
204 | |
205 } // namespace | 94 } // namespace |
206 | 95 |
207 | 96 |
208 @interface WebDragSource(Private) | 97 @interface WebDragSource(Private) |
209 | 98 |
210 - (void)writePromisedFileTo:(NSString*)path; | |
211 - (void)fillPasteboard; | 99 - (void)fillPasteboard; |
212 - (NSImage*)dragImage; | 100 - (NSImage*)dragImage; |
213 | 101 |
214 @end // @interface WebDragSource(Private) | 102 @end // @interface WebDragSource(Private) |
215 | 103 |
216 | 104 |
217 @implementation WebDragSource | 105 @implementation WebDragSource |
218 | 106 |
219 - (id)initWithContents:(content::WebContentsImpl*)contents | 107 - (id)initWithContents:(content::WebContentsImpl*)contents |
220 view:(NSView*)contentsView | 108 view:(NSView*)contentsView |
(...skipping 96 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
317 ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle); | 205 ui::WriteCustomDataToPickle(dropData_->custom_data, &pickle); |
318 [pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()] | 206 [pboard setData:[NSData dataWithBytes:pickle.data() length:pickle.size()] |
319 forType:ui::kWebCustomDataPboardType]; | 207 forType:ui::kWebCustomDataPboardType]; |
320 | 208 |
321 // Dummy type. | 209 // Dummy type. |
322 } else if ([type isEqualToString:ui::kChromeDragDummyPboardType]) { | 210 } else if ([type isEqualToString:ui::kChromeDragDummyPboardType]) { |
323 // The dummy type _was_ promised and someone decided to call the bluff. | 211 // The dummy type _was_ promised and someone decided to call the bluff. |
324 [pboard setData:[NSData data] | 212 [pboard setData:[NSData data] |
325 forType:ui::kChromeDragDummyPboardType]; | 213 forType:ui::kChromeDragDummyPboardType]; |
326 | 214 |
327 // File promise. | |
328 } else if ([type isEqualToString: | |
329 base::mac::CFToNSCast(kPasteboardTypeFileURLPromise)]) { | |
330 NSString* destination = GetDropLocation(pboard); | |
331 if (destination) { | |
332 [self writePromisedFileTo:destination]; | |
333 | |
334 // And set some data. | |
335 [pboard setData:[NSData data] | |
336 forType:base::mac::CFToNSCast(kPasteboardTypeFileURLPromise)]; | |
337 } | |
338 | |
339 // Oops! | 215 // Oops! |
340 } else { | 216 } else { |
341 // Unknown drag pasteboard type. | 217 // Unknown drag pasteboard type. |
342 NOTREACHED(); | 218 NOTREACHED(); |
343 } | 219 } |
344 } | 220 } |
345 | 221 |
346 - (NSPoint)convertScreenPoint:(NSPoint)screenPoint { | 222 - (NSPoint)convertScreenPoint:(NSPoint)screenPoint { |
347 DCHECK([contentsView_ window]); | 223 DCHECK([contentsView_ window]); |
348 NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint]; | 224 NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint]; |
(...skipping 81 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... |
430 localPoint.y = viewFrame.size.height - localPoint.y; | 306 localPoint.y = viewFrame.size.height - localPoint.y; |
431 // Flip |screenPoint|. | 307 // Flip |screenPoint|. |
432 NSRect screenFrame = [[[contentsView_ window] screen] frame]; | 308 NSRect screenFrame = [[[contentsView_ window] screen] frame]; |
433 screenPoint.y = screenFrame.size.height - screenPoint.y; | 309 screenPoint.y = screenFrame.size.height - screenPoint.y; |
434 | 310 |
435 contents_->DragSourceMovedTo(localPoint.x, localPoint.y, | 311 contents_->DragSourceMovedTo(localPoint.x, localPoint.y, |
436 screenPoint.x, screenPoint.y); | 312 screenPoint.x, screenPoint.y); |
437 } | 313 } |
438 } | 314 } |
439 | 315 |
440 @end // @implementation WebDragSource | 316 - (NSString*)dragPromisedFileTo:(NSString*)path { |
441 | |
442 @implementation WebDragSource (Private) | |
443 | |
444 - (void)writePromisedFileTo:(NSString*)path { | |
445 // Be extra paranoid; avoid crashing. | 317 // Be extra paranoid; avoid crashing. |
446 if (!dropData_) { | 318 if (!dropData_) { |
447 NOTREACHED() << "No drag-and-drop data available for promised file."; | 319 NOTREACHED() << "No drag-and-drop data available for promised file."; |
448 return; | 320 return nil; |
449 } | 321 } |
450 | 322 |
451 base::FilePath filePath(SysNSStringToUTF8(path)); | 323 base::FilePath filePath(SysNSStringToUTF8(path)); |
452 filePath = filePath.Append(downloadFileName_); | 324 filePath = filePath.Append(downloadFileName_); |
453 | 325 |
454 // CreateFileStreamForDrop() will call base::PathExists(), | 326 // CreateFileStreamForDrop() will call base::PathExists(), |
455 // which is blocking. Since this operation is already blocking the | 327 // which is blocking. Since this operation is already blocking the |
456 // UI thread on OSX, it should be reasonable to let it happen. | 328 // UI thread on OSX, it should be reasonable to let it happen. |
457 base::ThreadRestrictions::ScopedAllowIO allowIO; | 329 base::ThreadRestrictions::ScopedAllowIO allowIO; |
458 scoped_ptr<FileStream> fileStream(content::CreateFileStreamForDrop( | 330 scoped_ptr<FileStream> fileStream(content::CreateFileStreamForDrop( |
459 &filePath, content::GetContentClient()->browser()->GetNetLog())); | 331 &filePath, content::GetContentClient()->browser()->GetNetLog())); |
460 if (!fileStream) | 332 if (!fileStream) |
461 return; | 333 return nil; |
462 | 334 |
463 if (downloadURL_.is_valid()) { | 335 if (downloadURL_.is_valid()) { |
464 scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile( | 336 scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile( |
465 filePath, | 337 filePath, |
466 fileStream.Pass(), | 338 fileStream.Pass(), |
467 downloadURL_, | 339 downloadURL_, |
468 content::Referrer(contents_->GetLastCommittedURL(), | 340 content::Referrer(contents_->GetLastCommittedURL(), |
469 dropData_->referrer_policy), | 341 dropData_->referrer_policy), |
470 contents_->GetEncoding(), | 342 contents_->GetEncoding(), |
471 contents_)); | 343 contents_)); |
472 | 344 |
473 // The finalizer will take care of closing and deletion. | 345 // The finalizer will take care of closing and deletion. |
474 dragFileDownloader->Start(new PromiseFileFinalizer( | 346 dragFileDownloader->Start(new PromiseFileFinalizer( |
475 dragFileDownloader.get())); | 347 dragFileDownloader.get())); |
476 } else { | 348 } else { |
477 // The writer will take care of closing and deletion. | 349 // The writer will take care of closing and deletion. |
478 BrowserThread::PostTask(BrowserThread::FILE, | 350 BrowserThread::PostTask(BrowserThread::FILE, |
479 FROM_HERE, | 351 FROM_HERE, |
480 base::Bind(&PromiseWriterHelper, | 352 base::Bind(&PromiseWriterHelper, |
481 *dropData_, | 353 *dropData_, |
482 base::Passed(&fileStream))); | 354 base::Passed(&fileStream))); |
483 } | 355 } |
484 SelectFileInFinder(filePath); | 356 |
| 357 // The DragDownloadFile constructor may have altered the value of |filePath| |
| 358 // if, say, an existing file at the drop site has the same name. Return the |
| 359 // actual name that was used to write the file. |
| 360 return SysUTF8ToNSString(filePath.BaseName().value()); |
485 } | 361 } |
486 | 362 |
| 363 @end // @implementation WebDragSource |
| 364 |
| 365 @implementation WebDragSource (Private) |
| 366 |
487 - (void)fillPasteboard { | 367 - (void)fillPasteboard { |
488 DCHECK(pasteboard_.get()); | 368 DCHECK(pasteboard_.get()); |
489 | 369 |
490 [pasteboard_ declareTypes:@[ui::kChromeDragDummyPboardType] | 370 [pasteboard_ declareTypes:@[ ui::kChromeDragDummyPboardType ] |
491 owner:contentsView_]; | 371 owner:contentsView_]; |
492 | 372 |
493 // URL (and title). | 373 // URL (and title). |
494 if (dropData_->url.is_valid()) { | 374 if (dropData_->url.is_valid()) { |
495 [pasteboard_ addTypes:@[NSURLPboardType, kNSURLTitlePboardType] | 375 [pasteboard_ addTypes:@[ NSURLPboardType, kNSURLTitlePboardType ] |
496 owner:contentsView_]; | 376 owner:contentsView_]; |
497 } | 377 } |
498 | 378 |
499 // MIME type. | 379 // MIME type. |
500 std::string mimeType; | 380 std::string mimeType; |
501 | 381 |
502 // File. | 382 // File. |
503 if (!dropData_->file_contents.empty() || | 383 if (!dropData_->file_contents.empty() || |
504 !dropData_->download_metadata.empty()) { | 384 !dropData_->download_metadata.empty()) { |
505 if (dropData_->download_metadata.empty()) { | 385 if (dropData_->download_metadata.empty()) { |
(...skipping 21 matching lines...) Expand all Loading... |
527 defaultName); | 407 defaultName); |
528 } | 408 } |
529 } | 409 } |
530 | 410 |
531 if (!mimeType.empty()) { | 411 if (!mimeType.empty()) { |
532 base::ScopedCFTypeRef<CFStringRef> mimeTypeCF( | 412 base::ScopedCFTypeRef<CFStringRef> mimeTypeCF( |
533 base::SysUTF8ToCFStringRef(mimeType)); | 413 base::SysUTF8ToCFStringRef(mimeType)); |
534 fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag( | 414 fileUTI_.reset(UTTypeCreatePreferredIdentifierForTag( |
535 kUTTagClassMIMEType, mimeTypeCF.get(), NULL)); | 415 kUTTagClassMIMEType, mimeTypeCF.get(), NULL)); |
536 | 416 |
537 NSArray* types = | 417 // File (HFS) promise. |
538 @[base::mac::CFToNSCast(kPasteboardTypeFileURLPromise), | 418 // There are two ways to drag/drop files. NSFilesPromisePboardType is the |
539 base::mac::CFToNSCast(kPasteboardTypeFilePromiseContent)]; | 419 // deprecated way, and kPasteboardTypeFilePromiseContent is the way that |
540 [pasteboard_ addTypes:types owner:contentsView_]; | 420 // does not work. kPasteboardTypeFilePromiseContent is thoroughly broken: |
| 421 // * API: There is no good way to get the location for the drop. |
| 422 // <http://lists.apple.com/archives/cocoa-dev/2012/Feb/msg00706.html> |
| 423 // * Behavior: A file dropped in the Finder is not selected. This can be |
| 424 // worked around by selecting the file in the Finder using AppleEvents, |
| 425 // but the drop target window will come to the front of the Finder's |
| 426 // window list (unlike the previous behavior). <http://crbug.com/278515> |
| 427 // * Behavior: Files dragged over app icons in the dock do not highlight |
| 428 // the dock icons, and the dock icons do not accept the drop. |
| 429 // <http://crbug.com/282916> |
| 430 // * Behavior: A file dropped onto the desktop is positioned at the upper |
| 431 // right of the desktop rather than at the position at which it was |
| 432 // dropped. <http://crbug.com/284942> |
| 433 NSArray* fileUTIList = @[ base::mac::CFToNSCast(fileUTI_.get()) ]; |
| 434 [pasteboard_ addTypes:@[ NSFilesPromisePboardType ] owner:contentsView_]; |
| 435 [pasteboard_ setPropertyList:fileUTIList |
| 436 forType:NSFilesPromisePboardType]; |
541 | 437 |
542 [pasteboard_ setPropertyList:@[base::mac::CFToNSCast(fileUTI_.get())] | 438 if (!dropData_->file_contents.empty()) |
543 forType:base::mac::CFToNSCast(kPasteboardTypeFilePromiseContent)]; | 439 [pasteboard_ addTypes:fileUTIList owner:contentsView_]; |
544 | |
545 if (!dropData_->file_contents.empty()) { | |
546 NSArray* types = @[base::mac::CFToNSCast(fileUTI_.get())]; | |
547 [pasteboard_ addTypes:types owner:contentsView_]; | |
548 } | |
549 } | 440 } |
550 } | 441 } |
551 | 442 |
552 // HTML. | 443 // HTML. |
553 bool hasHTMLData = !dropData_->html.string().empty(); | 444 bool hasHTMLData = !dropData_->html.string().empty(); |
554 // Mail.app and TextEdit accept drags that have both HTML and image flavors on | 445 // Mail.app and TextEdit accept drags that have both HTML and image flavors on |
555 // them, but don't process them correctly <http://crbug.com/55879>. Therefore, | 446 // them, but don't process them correctly <http://crbug.com/55879>. Therefore, |
556 // if there is an image flavor, don't put the HTML data on as HTML, but rather | 447 // if there is an image flavor, don't put the HTML data on as HTML, but rather |
557 // put it on as this Chrome-only flavor. | 448 // put it on as this Chrome-only flavor. |
558 // | 449 // |
559 // (The only time that Blink fills in the DropData::file_contents is with | 450 // (The only time that Blink fills in the DropData::file_contents is with |
560 // an image drop, but the MIME time is tested anyway for paranoia's sake.) | 451 // an image drop, but the MIME time is tested anyway for paranoia's sake.) |
561 bool hasImageData = !dropData_->file_contents.empty() && | 452 bool hasImageData = !dropData_->file_contents.empty() && |
562 fileUTI_ && | 453 fileUTI_ && |
563 UTTypeConformsTo(fileUTI_.get(), kUTTypeImage); | 454 UTTypeConformsTo(fileUTI_.get(), kUTTypeImage); |
564 if (hasHTMLData) { | 455 if (hasHTMLData) { |
565 if (hasImageData) { | 456 if (hasImageData) { |
566 [pasteboard_ addTypes:@[ui::kChromeDragImageHTMLPboardType] | 457 [pasteboard_ addTypes:@[ ui::kChromeDragImageHTMLPboardType ] |
567 owner:contentsView_]; | 458 owner:contentsView_]; |
568 } else { | 459 } else { |
569 [pasteboard_ addTypes:@[NSHTMLPboardType] owner:contentsView_]; | 460 [pasteboard_ addTypes:@[ NSHTMLPboardType ] owner:contentsView_]; |
570 } | 461 } |
571 } | 462 } |
572 | 463 |
573 // Plain text. | 464 // Plain text. |
574 if (!dropData_->text.string().empty()) { | 465 if (!dropData_->text.string().empty()) { |
575 [pasteboard_ addTypes:@[NSStringPboardType] | 466 [pasteboard_ addTypes:@[ NSStringPboardType ] |
576 owner:contentsView_]; | 467 owner:contentsView_]; |
577 } | 468 } |
578 | 469 |
579 if (!dropData_->custom_data.empty()) { | 470 if (!dropData_->custom_data.empty()) { |
580 [pasteboard_ addTypes:@[ui::kWebCustomDataPboardType] | 471 [pasteboard_ addTypes:@[ ui::kWebCustomDataPboardType ] |
581 owner:contentsView_]; | 472 owner:contentsView_]; |
582 } | 473 } |
583 } | 474 } |
584 | 475 |
585 - (NSImage*)dragImage { | 476 - (NSImage*)dragImage { |
586 if (dragImage_) | 477 if (dragImage_) |
587 return dragImage_; | 478 return dragImage_; |
588 | 479 |
589 // Default to returning a generic image. | 480 // Default to returning a generic image. |
590 return content::GetContentClient()->GetNativeImageNamed( | 481 return content::GetContentClient()->GetNativeImageNamed( |
591 IDR_DEFAULT_FAVICON).ToNSImage(); | 482 IDR_DEFAULT_FAVICON).ToNSImage(); |
592 } | 483 } |
593 | 484 |
594 @end // @implementation WebDragSource (Private) | 485 @end // @implementation WebDragSource (Private) |
OLD | NEW |