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
3// found in the LICENSE file.
4
5#import "content/browser/web_contents/web_drag_dest_mac.h"
6
7#import <Carbon/Carbon.h>
8
9#include "base/strings/sys_string_conversions.h"
10#include "content/browser/renderer_host/render_view_host_impl.h"
11#include "content/browser/web_contents/web_contents_impl.h"
12#include "content/public/browser/web_contents_delegate.h"
13#include "content/public/browser/web_drag_dest_delegate.h"
14#include "content/public/common/drop_data.h"
15#import "third_party/mozilla/NSPasteboard+Utils.h"
16#include "third_party/WebKit/public/web/WebInputEvent.h"
17#include "ui/base/clipboard/custom_data_helper.h"
18#import "ui/base/dragdrop/cocoa_dnd_util.h"
19#include "ui/base/window_open_disposition.h"
20
21using blink::WebDragOperationsMask;
22using content::DropData;
23using content::OpenURLParams;
24using content::Referrer;
25using content::WebContentsImpl;
26
27int GetModifierFlags() {
28  int modifier_state = 0;
29  UInt32 currentModifiers = GetCurrentKeyModifiers();
30  if (currentModifiers & ::shiftKey)
31    modifier_state |= blink::WebInputEvent::ShiftKey;
32  if (currentModifiers & ::controlKey)
33    modifier_state |= blink::WebInputEvent::ControlKey;
34  if (currentModifiers & ::optionKey)
35    modifier_state |= blink::WebInputEvent::AltKey;
36  if (currentModifiers & ::cmdKey)
37      modifier_state |= blink::WebInputEvent::MetaKey;
38  return modifier_state;
39}
40
41@implementation WebDragDest
42
43// |contents| is the WebContentsImpl representing this tab, used to communicate
44// drag&drop messages to WebCore and handle navigation on a successful drop
45// (if necessary).
46- (id)initWithWebContentsImpl:(WebContentsImpl*)contents {
47  if ((self = [super init])) {
48    webContents_ = contents;
49    canceled_ = false;
50  }
51  return self;
52}
53
54- (DropData*)currentDropData {
55  return dropData_.get();
56}
57
58- (void)setDragDelegate:(content::WebDragDestDelegate*)delegate {
59  delegate_ = delegate;
60}
61
62// Call to set whether or not we should allow the drop. Takes effect the
63// next time |-draggingUpdated:| is called.
64- (void)setCurrentOperation:(NSDragOperation)operation {
65  currentOperation_ = operation;
66}
67
68// Given a point in window coordinates and a view in that window, return a
69// flipped point in the coordinate system of |view|.
70- (NSPoint)flipWindowPointToView:(const NSPoint&)windowPoint
71                            view:(NSView*)view {
72  DCHECK(view);
73  NSPoint viewPoint =  [view convertPoint:windowPoint fromView:nil];
74  NSRect viewFrame = [view frame];
75  viewPoint.y = viewFrame.size.height - viewPoint.y;
76  return viewPoint;
77}
78
79// Given a point in window coordinates and a view in that window, return a
80// flipped point in screen coordinates.
81- (NSPoint)flipWindowPointToScreen:(const NSPoint&)windowPoint
82                              view:(NSView*)view {
83  DCHECK(view);
84  NSPoint screenPoint = [[view window] convertBaseToScreen:windowPoint];
85  NSScreen* screen = [[view window] screen];
86  NSRect screenFrame = [screen frame];
87  screenPoint.y = screenFrame.size.height - screenPoint.y;
88  return screenPoint;
89}
90
91// Return YES if the drop site only allows drops that would navigate.  If this
92// is the case, we don't want to pass messages to the renderer because there's
93// really no point (i.e., there's nothing that cares about the mouse position or
94// entering and exiting).  One example is an interstitial page (e.g., safe
95// browsing warning).
96- (BOOL)onlyAllowsNavigation {
97  return webContents_->ShowingInterstitialPage();
98}
99
100// Messages to send during the tracking of a drag, usually upon receiving
101// calls from the view system. Communicates the drag messages to WebCore.
102
103- (NSDragOperation)draggingEntered:(id<NSDraggingInfo>)info
104                              view:(NSView*)view {
105  // Save off the RVH so we can tell if it changes during a drag. If it does,
106  // we need to send a new enter message in draggingUpdated:.
107  currentRVH_ = webContents_->GetRenderViewHost();
108
109  // Fill out a DropData from pasteboard.
110  scoped_ptr<DropData> dropData;
111  dropData.reset(new DropData());
112  [self populateDropData:dropData.get()
113             fromPasteboard:[info draggingPasteboard]];
114
115  NSDragOperation mask = [info draggingSourceOperationMask];
116
117  // Give the delegate an opportunity to cancel the drag.
118  canceled_ = !webContents_->GetDelegate()->CanDragEnter(
119      webContents_,
120      *dropData,
121      static_cast<WebDragOperationsMask>(mask));
122  if (canceled_)
123    return NSDragOperationNone;
124
125  if ([self onlyAllowsNavigation]) {
126    if ([[info draggingPasteboard] containsURLData])
127      return NSDragOperationCopy;
128    return NSDragOperationNone;
129  }
130
131  if (delegate_) {
132    delegate_->DragInitialize(webContents_);
133    delegate_->OnDragEnter();
134  }
135
136  dropData_.swap(dropData);
137
138  // Create the appropriate mouse locations for WebCore. The draggingLocation
139  // is in window coordinates. Both need to be flipped.
140  NSPoint windowPoint = [info draggingLocation];
141  NSPoint viewPoint = [self flipWindowPointToView:windowPoint view:view];
142  NSPoint screenPoint = [self flipWindowPointToScreen:windowPoint view:view];
143  webContents_->GetRenderViewHost()->DragTargetDragEnter(
144      *dropData_,
145      gfx::Point(viewPoint.x, viewPoint.y),
146      gfx::Point(screenPoint.x, screenPoint.y),
147      static_cast<WebDragOperationsMask>(mask),
148      GetModifierFlags());
149
150  // We won't know the true operation (whether the drag is allowed) until we
151  // hear back from the renderer. For now, be optimistic:
152  currentOperation_ = NSDragOperationCopy;
153  return currentOperation_;
154}
155
156- (void)draggingExited:(id<NSDraggingInfo>)info {
157  DCHECK(currentRVH_);
158  if (currentRVH_ != webContents_->GetRenderViewHost())
159    return;
160
161  if (canceled_)
162    return;
163
164  if ([self onlyAllowsNavigation])
165    return;
166
167  if (delegate_)
168    delegate_->OnDragLeave();
169
170  webContents_->GetRenderViewHost()->DragTargetDragLeave();
171  dropData_.reset();
172}
173
174- (NSDragOperation)draggingUpdated:(id<NSDraggingInfo>)info
175                              view:(NSView*)view {
176  DCHECK(currentRVH_);
177  if (currentRVH_ != webContents_->GetRenderViewHost())
178    [self draggingEntered:info view:view];
179
180  if (canceled_)
181    return NSDragOperationNone;
182
183  if ([self onlyAllowsNavigation]) {
184    if ([[info draggingPasteboard] containsURLData])
185      return NSDragOperationCopy;
186    return NSDragOperationNone;
187  }
188
189  // Create the appropriate mouse locations for WebCore. The draggingLocation
190  // is in window coordinates.
191  NSPoint windowPoint = [info draggingLocation];
192  NSPoint viewPoint = [self flipWindowPointToView:windowPoint view:view];
193  NSPoint screenPoint = [self flipWindowPointToScreen:windowPoint view:view];
194  NSDragOperation mask = [info draggingSourceOperationMask];
195  webContents_->GetRenderViewHost()->DragTargetDragOver(
196      gfx::Point(viewPoint.x, viewPoint.y),
197      gfx::Point(screenPoint.x, screenPoint.y),
198      static_cast<WebDragOperationsMask>(mask),
199      GetModifierFlags());
200
201  if (delegate_)
202    delegate_->OnDragOver();
203
204  return currentOperation_;
205}
206
207- (BOOL)performDragOperation:(id<NSDraggingInfo>)info
208                              view:(NSView*)view {
209  if (currentRVH_ != webContents_->GetRenderViewHost())
210    [self draggingEntered:info view:view];
211
212  // Check if we only allow navigation and navigate to a url on the pasteboard.
213  if ([self onlyAllowsNavigation]) {
214    NSPasteboard* pboard = [info draggingPasteboard];
215    if ([pboard containsURLData]) {
216      GURL url;
217      ui::PopulateURLAndTitleFromPasteboard(&url, NULL, pboard, YES);
218      webContents_->OpenURL(OpenURLParams(
219          url, Referrer(), CURRENT_TAB, ui::PAGE_TRANSITION_AUTO_BOOKMARK,
220          false));
221      return YES;
222    } else {
223      return NO;
224    }
225  }
226
227  if (delegate_)
228    delegate_->OnDrop();
229
230  currentRVH_ = NULL;
231
232  // Create the appropriate mouse locations for WebCore. The draggingLocation
233  // is in window coordinates. Both need to be flipped.
234  NSPoint windowPoint = [info draggingLocation];
235  NSPoint viewPoint = [self flipWindowPointToView:windowPoint view:view];
236  NSPoint screenPoint = [self flipWindowPointToScreen:windowPoint view:view];
237  webContents_->GetRenderViewHost()->DragTargetDrop(
238      gfx::Point(viewPoint.x, viewPoint.y),
239      gfx::Point(screenPoint.x, screenPoint.y),
240      GetModifierFlags());
241
242  dropData_.reset();
243
244  return YES;
245}
246
247// Given |data|, which should not be nil, fill it in using the contents of the
248// given pasteboard. The types handled by this method should be kept in sync
249// with [WebContentsViewCocoa registerDragTypes].
250- (void)populateDropData:(DropData*)data
251          fromPasteboard:(NSPasteboard*)pboard {
252  DCHECK(data);
253  DCHECK(pboard);
254  NSArray* types = [pboard types];
255
256  data->did_originate_from_renderer =
257      [types containsObject:ui::kChromeDragDummyPboardType];
258
259  // Get URL if possible. To avoid exposing file system paths to web content,
260  // filenames in the drag are not converted to file URLs.
261  ui::PopulateURLAndTitleFromPasteboard(&data->url,
262                                        &data->url_title,
263                                        pboard,
264                                        NO);
265
266  // Get plain text.
267  if ([types containsObject:NSStringPboardType]) {
268    data->text = base::NullableString16(
269        base::SysNSStringToUTF16([pboard stringForType:NSStringPboardType]),
270        false);
271  }
272
273  // Get HTML. If there's no HTML, try RTF.
274  if ([types containsObject:NSHTMLPboardType]) {
275    NSString* html = [pboard stringForType:NSHTMLPboardType];
276    data->html = base::NullableString16(base::SysNSStringToUTF16(html), false);
277  } else if ([types containsObject:ui::kChromeDragImageHTMLPboardType]) {
278    NSString* html = [pboard stringForType:ui::kChromeDragImageHTMLPboardType];
279    data->html = base::NullableString16(base::SysNSStringToUTF16(html), false);
280  } else if ([types containsObject:NSRTFPboardType]) {
281    NSString* html = [pboard htmlFromRtf];
282    data->html = base::NullableString16(base::SysNSStringToUTF16(html), false);
283  }
284
285  // Get files.
286  if ([types containsObject:NSFilenamesPboardType]) {
287    NSArray* files = [pboard propertyListForType:NSFilenamesPboardType];
288    if ([files isKindOfClass:[NSArray class]] && [files count]) {
289      for (NSUInteger i = 0; i < [files count]; i++) {
290        NSString* filename = [files objectAtIndex:i];
291        BOOL exists = [[NSFileManager defaultManager]
292                           fileExistsAtPath:filename];
293        if (exists) {
294          data->filenames.push_back(ui::FileInfo(
295              base::FilePath::FromUTF8Unsafe(base::SysNSStringToUTF8(filename)),
296              base::FilePath()));
297        }
298      }
299    }
300  }
301
302  // TODO(pinkerton): Get file contents. http://crbug.com/34661
303
304  // Get custom MIME data.
305  if ([types containsObject:ui::kWebCustomDataPboardType]) {
306    NSData* customData = [pboard dataForType:ui::kWebCustomDataPboardType];
307    ui::ReadCustomDataIntoMap([customData bytes],
308                              [customData length],
309                              &data->custom_data);
310  }
311}
312
313@end
314