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