web_drag_source.mm revision 72a454cd3513ac24fbdd0e0cb9ad70b86a99b801
1// Copyright (c) 2010 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#import "chrome/browser/ui/cocoa/tab_contents/web_drag_source.h" 6 7#include "app/mac/nsimage_cache.h" 8#include "base/file_path.h" 9#include "base/string_util.h" 10#include "base/sys_string_conversions.h" 11#include "base/task.h" 12#include "base/threading/thread.h" 13#include "base/utf_string_conversions.h" 14#include "chrome/browser/browser_process.h" 15#include "chrome/browser/download/download_manager.h" 16#include "chrome/browser/download/download_util.h" 17#include "chrome/browser/download/drag_download_file.h" 18#include "chrome/browser/download/drag_download_util.h" 19#include "chrome/browser/renderer_host/render_view_host.h" 20#include "chrome/browser/tab_contents/tab_contents.h" 21#include "chrome/browser/tab_contents/tab_contents_view_mac.h" 22#include "net/base/file_stream.h" 23#include "net/base/net_util.h" 24#import "third_party/mozilla/NSPasteboard+Utils.h" 25#include "webkit/glue/webdropdata.h" 26 27using base::SysNSStringToUTF8; 28using base::SysUTF8ToNSString; 29using base::SysUTF16ToNSString; 30using net::FileStream; 31 32 33namespace { 34 35// An unofficial standard pasteboard title type to be provided alongside the 36// |NSURLPboardType|. 37NSString* const kNSURLTitlePboardType = @"public.url-name"; 38 39// Returns a filename appropriate for the drop data 40// TODO(viettrungluu): Refactor to make it common across platforms, 41// and move it somewhere sensible. 42FilePath GetFileNameFromDragData(const WebDropData& drop_data) { 43 // Images without ALT text will only have a file extension so we need to 44 // synthesize one from the provided extension and URL. 45 FilePath file_name([SysUTF16ToNSString(drop_data.file_description_filename) 46 fileSystemRepresentation]); 47 file_name = file_name.BaseName().RemoveExtension(); 48 49 if (file_name.empty()) { 50 // Retrieve the name from the URL. 51 string16 suggested_filename = 52 net::GetSuggestedFilename(drop_data.url, "", "", string16()); 53 file_name = FilePath( 54 [SysUTF16ToNSString(suggested_filename) fileSystemRepresentation]); 55 } 56 57 file_name = file_name.ReplaceExtension([SysUTF16ToNSString( 58 drop_data.file_extension) fileSystemRepresentation]); 59 60 return file_name; 61} 62 63// This class's sole task is to write out data for a promised file; the caller 64// is responsible for opening the file. 65class PromiseWriterTask : public Task { 66 public: 67 // Assumes ownership of file_stream. 68 PromiseWriterTask(const WebDropData& drop_data, 69 FileStream* file_stream); 70 virtual ~PromiseWriterTask(); 71 virtual void Run(); 72 73 private: 74 WebDropData drop_data_; 75 76 // This class takes ownership of file_stream_ and will close and delete it. 77 scoped_ptr<FileStream> file_stream_; 78}; 79 80// Takes the drop data and an open file stream (which it takes ownership of and 81// will close and delete). 82PromiseWriterTask::PromiseWriterTask(const WebDropData& drop_data, 83 FileStream* file_stream) : 84 drop_data_(drop_data) { 85 file_stream_.reset(file_stream); 86 DCHECK(file_stream_.get()); 87} 88 89PromiseWriterTask::~PromiseWriterTask() { 90 DCHECK(file_stream_.get()); 91 if (file_stream_.get()) 92 file_stream_->Close(); 93} 94 95void PromiseWriterTask::Run() { 96 CHECK(file_stream_.get()); 97 file_stream_->Write(drop_data_.file_contents.data(), 98 drop_data_.file_contents.length(), 99 NULL); 100 101 // Let our destructor take care of business. 102} 103 104} // namespace 105 106 107@interface WebDragSource(Private) 108 109- (void)fillPasteboard; 110- (NSImage*)dragImage; 111 112@end // @interface WebDragSource(Private) 113 114 115@implementation WebDragSource 116 117- (id)initWithContentsView:(TabContentsViewCocoa*)contentsView 118 dropData:(const WebDropData*)dropData 119 image:(NSImage*)image 120 offset:(NSPoint)offset 121 pasteboard:(NSPasteboard*)pboard 122 dragOperationMask:(NSDragOperation)dragOperationMask { 123 if ((self = [super init])) { 124 contentsView_ = contentsView; 125 DCHECK(contentsView_); 126 127 dropData_.reset(new WebDropData(*dropData)); 128 DCHECK(dropData_.get()); 129 130 dragImage_.reset([image retain]); 131 imageOffset_ = offset; 132 133 pasteboard_.reset([pboard retain]); 134 DCHECK(pasteboard_.get()); 135 136 dragOperationMask_ = dragOperationMask; 137 138 [self fillPasteboard]; 139 } 140 141 return self; 142} 143 144- (NSDragOperation)draggingSourceOperationMaskForLocal:(BOOL)isLocal { 145 return dragOperationMask_; 146} 147 148- (void)lazyWriteToPasteboard:(NSPasteboard*)pboard forType:(NSString*)type { 149 // NSHTMLPboardType requires the character set to be declared. Otherwise, it 150 // assumes US-ASCII. Awesome. 151 static const string16 kHtmlHeader = 152 ASCIIToUTF16("<meta http-equiv=\"Content-Type\" " 153 "content=\"text/html;charset=UTF-8\">"); 154 155 // Be extra paranoid; avoid crashing. 156 if (!dropData_.get()) { 157 NOTREACHED() << "No drag-and-drop data available for lazy write."; 158 return; 159 } 160 161 // HTML. 162 if ([type isEqualToString:NSHTMLPboardType]) { 163 DCHECK(!dropData_->text_html.empty()); 164 // See comment on |kHtmlHeader| above. 165 [pboard setString:SysUTF16ToNSString(kHtmlHeader + dropData_->text_html) 166 forType:NSHTMLPboardType]; 167 168 // URL. 169 } else if ([type isEqualToString:NSURLPboardType]) { 170 DCHECK(dropData_->url.is_valid()); 171 NSURL* url = [NSURL URLWithString:SysUTF8ToNSString(dropData_->url.spec())]; 172 [url writeToPasteboard:pboard]; 173 174 // URL title. 175 } else if ([type isEqualToString:kNSURLTitlePboardType]) { 176 [pboard setString:SysUTF16ToNSString(dropData_->url_title) 177 forType:kNSURLTitlePboardType]; 178 179 // File contents. 180 } else if ([type isEqualToString:NSFileContentsPboardType] || 181 [type isEqualToString:NSCreateFileContentsPboardType( 182 SysUTF16ToNSString(dropData_->file_extension))]) { 183 // TODO(viettrungluu: find something which is known to accept 184 // NSFileContentsPboardType to check that this actually works! 185 scoped_nsobject<NSFileWrapper> file_wrapper( 186 [[NSFileWrapper alloc] initRegularFileWithContents:[NSData 187 dataWithBytes:dropData_->file_contents.data() 188 length:dropData_->file_contents.length()]]); 189 [file_wrapper setPreferredFilename:SysUTF8ToNSString( 190 GetFileNameFromDragData(*dropData_).value())]; 191 [pboard writeFileWrapper:file_wrapper]; 192 193 // TIFF. 194 } else if ([type isEqualToString:NSTIFFPboardType]) { 195 // TODO(viettrungluu): This is a bit odd since we rely on Cocoa to render 196 // our image into a TIFF. This is also suboptimal since this is all done 197 // synchronously. I'm not sure there's much we can easily do about it. 198 scoped_nsobject<NSImage> image( 199 [[NSImage alloc] initWithData:[NSData 200 dataWithBytes:dropData_->file_contents.data() 201 length:dropData_->file_contents.length()]]); 202 [pboard setData:[image TIFFRepresentation] forType:NSTIFFPboardType]; 203 204 // Plain text. 205 } else if ([type isEqualToString:NSStringPboardType]) { 206 DCHECK(!dropData_->plain_text.empty()); 207 [pboard setString:SysUTF16ToNSString(dropData_->plain_text) 208 forType:NSStringPboardType]; 209 210 // Oops! 211 } else { 212 NOTREACHED() << "Asked for a drag pasteboard type we didn't offer."; 213 } 214} 215 216- (NSPoint)convertScreenPoint:(NSPoint)screenPoint { 217 DCHECK([contentsView_ window]); 218 NSPoint basePoint = [[contentsView_ window] convertScreenToBase:screenPoint]; 219 return [contentsView_ convertPoint:basePoint fromView:nil]; 220} 221 222- (void)startDrag { 223 NSEvent* currentEvent = [NSApp currentEvent]; 224 225 // Synthesize an event for dragging, since we can't be sure that 226 // [NSApp currentEvent] will return a valid dragging event. 227 NSWindow* window = [contentsView_ window]; 228 NSPoint position = [window mouseLocationOutsideOfEventStream]; 229 NSTimeInterval eventTime = [currentEvent timestamp]; 230 NSEvent* dragEvent = [NSEvent mouseEventWithType:NSLeftMouseDragged 231 location:position 232 modifierFlags:NSLeftMouseDraggedMask 233 timestamp:eventTime 234 windowNumber:[window windowNumber] 235 context:nil 236 eventNumber:0 237 clickCount:1 238 pressure:1.0]; 239 240 if (dragImage_) { 241 position.x -= imageOffset_.x; 242 // Deal with Cocoa's flipped coordinate system. 243 position.y -= [dragImage_.get() size].height - imageOffset_.y; 244 } 245 // Per kwebster, offset arg is ignored, see -_web_DragImageForElement: in 246 // third_party/WebKit/Source/WebKit/mac/Misc/WebNSViewExtras.m. 247 [window dragImage:[self dragImage] 248 at:position 249 offset:NSZeroSize 250 event:dragEvent 251 pasteboard:pasteboard_ 252 source:contentsView_ 253 slideBack:YES]; 254} 255 256- (void)endDragAt:(NSPoint)screenPoint 257 operation:(NSDragOperation)operation { 258 [contentsView_ tabContents]->SystemDragEnded(); 259 260 RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host(); 261 if (rvh) { 262 // Convert |screenPoint| to view coordinates and flip it. 263 NSPoint localPoint = NSMakePoint(0, 0); 264 if ([contentsView_ window]) 265 localPoint = [self convertScreenPoint:screenPoint]; 266 NSRect viewFrame = [contentsView_ frame]; 267 localPoint.y = viewFrame.size.height - localPoint.y; 268 // Flip |screenPoint|. 269 NSRect screenFrame = [[[contentsView_ window] screen] frame]; 270 screenPoint.y = screenFrame.size.height - screenPoint.y; 271 272 rvh->DragSourceEndedAt(localPoint.x, localPoint.y, 273 screenPoint.x, screenPoint.y, 274 static_cast<WebKit::WebDragOperation>(operation)); 275 } 276 277 // Make sure the pasteboard owner isn't us. 278 [pasteboard_ declareTypes:[NSArray array] owner:nil]; 279} 280 281- (void)moveDragTo:(NSPoint)screenPoint { 282 RenderViewHost* rvh = [contentsView_ tabContents]->render_view_host(); 283 if (rvh) { 284 // Convert |screenPoint| to view coordinates and flip it. 285 NSPoint localPoint = NSMakePoint(0, 0); 286 if ([contentsView_ window]) 287 localPoint = [self convertScreenPoint:screenPoint]; 288 NSRect viewFrame = [contentsView_ frame]; 289 localPoint.y = viewFrame.size.height - localPoint.y; 290 // Flip |screenPoint|. 291 NSRect screenFrame = [[[contentsView_ window] screen] frame]; 292 screenPoint.y = screenFrame.size.height - screenPoint.y; 293 294 rvh->DragSourceMovedTo(localPoint.x, localPoint.y, 295 screenPoint.x, screenPoint.y); 296 } 297} 298 299- (NSString*)dragPromisedFileTo:(NSString*)path { 300 // Be extra paranoid; avoid crashing. 301 if (!dropData_.get()) { 302 NOTREACHED() << "No drag-and-drop data available for promised file."; 303 return nil; 304 } 305 306 FilePath fileName = downloadFileName_.empty() ? 307 GetFileNameFromDragData(*dropData_) : downloadFileName_; 308 FilePath filePath(SysNSStringToUTF8(path)); 309 filePath = filePath.Append(fileName); 310 FileStream* fileStream = 311 drag_download_util::CreateFileStreamForDrop(&filePath); 312 if (!fileStream) 313 return nil; 314 315 if (downloadURL_.is_valid()) { 316 TabContents* tabContents = [contentsView_ tabContents]; 317 scoped_refptr<DragDownloadFile> dragFileDownloader(new DragDownloadFile( 318 filePath, 319 linked_ptr<net::FileStream>(fileStream), 320 downloadURL_, 321 tabContents->GetURL(), 322 tabContents->encoding(), 323 tabContents)); 324 325 // The finalizer will take care of closing and deletion. 326 dragFileDownloader->Start( 327 new drag_download_util::PromiseFileFinalizer(dragFileDownloader)); 328 } else { 329 // The writer will take care of closing and deletion. 330 g_browser_process->file_thread()->message_loop()->PostTask(FROM_HERE, 331 new PromiseWriterTask(*dropData_, fileStream)); 332 } 333 334 // Once we've created the file, we should return the file name. 335 return SysUTF8ToNSString(filePath.BaseName().value()); 336} 337 338@end // @implementation WebDragSource 339 340 341@implementation WebDragSource (Private) 342 343- (void)fillPasteboard { 344 DCHECK(pasteboard_.get()); 345 346 [pasteboard_ declareTypes:[NSArray array] owner:contentsView_]; 347 348 // HTML. 349 if (!dropData_->text_html.empty()) 350 [pasteboard_ addTypes:[NSArray arrayWithObject:NSHTMLPboardType] 351 owner:contentsView_]; 352 353 // URL (and title). 354 if (dropData_->url.is_valid()) 355 [pasteboard_ addTypes:[NSArray arrayWithObjects:NSURLPboardType, 356 kNSURLTitlePboardType, nil] 357 owner:contentsView_]; 358 359 // File. 360 if (!dropData_->file_contents.empty() || 361 !dropData_->download_metadata.empty()) { 362 NSString* fileExtension = 0; 363 364 if (dropData_->download_metadata.empty()) { 365 // |dropData_->file_extension| comes with the '.', which we must strip. 366 fileExtension = (dropData_->file_extension.length() > 0) ? 367 SysUTF16ToNSString(dropData_->file_extension.substr(1)) : @""; 368 } else { 369 string16 mimeType; 370 FilePath fileName; 371 if (drag_download_util::ParseDownloadMetadata( 372 dropData_->download_metadata, 373 &mimeType, 374 &fileName, 375 &downloadURL_)) { 376 std::string contentDisposition = 377 "attachment; filename=" + fileName.value(); 378 download_util::GenerateFileName(downloadURL_, 379 contentDisposition, 380 std::string(), 381 UTF16ToUTF8(mimeType), 382 &downloadFileName_); 383 fileExtension = SysUTF8ToNSString(downloadFileName_.Extension()); 384 } 385 } 386 387 if (fileExtension) { 388 // File contents (with and without specific type), file (HFS) promise, 389 // TIFF. 390 // TODO(viettrungluu): others? 391 [pasteboard_ addTypes:[NSArray arrayWithObjects: 392 NSFileContentsPboardType, 393 NSCreateFileContentsPboardType(fileExtension), 394 NSFilesPromisePboardType, 395 NSTIFFPboardType, 396 nil] 397 owner:contentsView_]; 398 399 // For the file promise, we need to specify the extension. 400 [pasteboard_ setPropertyList:[NSArray arrayWithObject:fileExtension] 401 forType:NSFilesPromisePboardType]; 402 } 403 } 404 405 // Plain text. 406 if (!dropData_->plain_text.empty()) 407 [pasteboard_ addTypes:[NSArray arrayWithObject:NSStringPboardType] 408 owner:contentsView_]; 409} 410 411- (NSImage*)dragImage { 412 if (dragImage_) 413 return dragImage_; 414 415 // Default to returning a generic image. 416 return app::mac::GetCachedImageWithName(@"nav.pdf"); 417} 418 419@end // @implementation WebDragSource (Private) 420