1// Copyright 2014 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#include "ui/base/dragdrop/os_exchange_data_provider_mac.h"
6
7#import <Cocoa/Cocoa.h>
8
9#include "base/logging.h"
10#include "base/memory/ptr_util.h"
11#include "base/pickle.h"
12#include "base/strings/sys_string_conversions.h"
13#include "base/strings/utf_string_conversions.h"
14#import "third_party/mozilla/NSPasteboard+Utils.h"
15#include "ui/base/clipboard/clipboard_constants.h"
16#include "ui/base/clipboard/clipboard_format_type.h"
17#import "ui/base/clipboard/clipboard_util_mac.h"
18#include "ui/base/clipboard/custom_data_helper.h"
19#import "ui/base/dragdrop/cocoa_dnd_util.h"
20#include "ui/base/dragdrop/file_info/file_info.h"
21#include "url/gurl.h"
22
23@interface CrPasteboardItemWrapper : NSObject <NSPasteboardWriting>
24- (instancetype)initWithPasteboardItem:(NSPasteboardItem*)pasteboardItem;
25@end
26
27@implementation CrPasteboardItemWrapper {
28  base::scoped_nsobject<NSPasteboardItem> _pasteboardItem;
29}
30
31- (instancetype)initWithPasteboardItem:(NSPasteboardItem*)pasteboardItem {
32  if ((self = [super init])) {
33    _pasteboardItem.reset([pasteboardItem retain]);
34  }
35
36  return self;
37}
38
39- (NSArray<NSString*>*)writableTypesForPasteboard:(NSPasteboard*)pasteboard {
40  // If the NSPasteboardItem hasn't been added to an NSPasteboard, then the
41  // -[NSPasteboardItem writableTypesForPasteboard:] will return -types. But if
42  // it has been added to a pasteboard, it will return nil. This pasteboard item
43  // was added implicitly by adding flavors to the owned pasteboard of
44  // OwningProvider, so call -types to actually get data.
45  //
46  // Merge in the ui::kChromeDragDummyPboardType type, so that all of Chromium
47  // is marked to receive the drags. TODO(avi): Wire up MacViews so that
48  // BridgedContentView properly registers the result of View::GetDropFormats()
49  // rather than OSExchangeDataProviderMac::SupportedPasteboardTypes().
50  return [[_pasteboardItem types]
51      arrayByAddingObject:ui::kChromeDragDummyPboardType];
52}
53
54- (NSPasteboardWritingOptions)writingOptionsForType:(NSString*)type
55                                         pasteboard:(NSPasteboard*)pasteboard {
56  // It is critical to return 0 here. If any flavors are promised, then when the
57  // app quits, AppKit will call in the promises, and the backing pasteboard
58  // will likely be long-deallocated. Yes, AppKit will call in promises for
59  // *all* promised flavors on *all* pasteboards, not just those pasteboards
60  // used for copy/paste.
61  return 0;
62}
63
64- (id)pasteboardPropertyListForType:(NSString*)type {
65  if ([type isEqual:ui::kChromeDragDummyPboardType])
66    return [NSData data];
67
68  // Like above, an NSPasteboardItem added to a pasteboard will return nil from
69  // -pasteboardPropertyListForType:, so call -dataForType: instead.
70  return [_pasteboardItem dataForType:type];
71}
72
73@end
74
75namespace ui {
76
77namespace {
78
79class OwningProvider : public OSExchangeDataProviderMac {
80 public:
81  OwningProvider()
82      : OSExchangeDataProviderMac(),
83        owned_pasteboard_(new ui::UniquePasteboard) {}
84  OwningProvider(const OwningProvider& provider) = default;
85
86  std::unique_ptr<OSExchangeData::Provider> Clone() const override {
87    return std::make_unique<OwningProvider>(*this);
88  }
89
90  NSPasteboard* GetPasteboard() const override {
91    return owned_pasteboard_->get();
92  }
93
94 private:
95  scoped_refptr<ui::UniquePasteboard> owned_pasteboard_;
96};
97
98class WrappingProvider : public OSExchangeDataProviderMac {
99 public:
100  WrappingProvider(NSPasteboard* pasteboard)
101      : OSExchangeDataProviderMac(), wrapped_pasteboard_([pasteboard retain]) {}
102  WrappingProvider(const WrappingProvider& provider) = default;
103
104  std::unique_ptr<OSExchangeData::Provider> Clone() const override {
105    return std::make_unique<WrappingProvider>(*this);
106  }
107
108  NSPasteboard* GetPasteboard() const override { return wrapped_pasteboard_; }
109
110 private:
111  base::scoped_nsobject<NSPasteboard> wrapped_pasteboard_;
112};
113
114}  // namespace
115
116OSExchangeDataProviderMac::OSExchangeDataProviderMac() = default;
117OSExchangeDataProviderMac::OSExchangeDataProviderMac(
118    const OSExchangeDataProviderMac&) = default;
119OSExchangeDataProviderMac& OSExchangeDataProviderMac::operator=(
120    const OSExchangeDataProviderMac&) = default;
121
122OSExchangeDataProviderMac::~OSExchangeDataProviderMac() = default;
123
124// static
125std::unique_ptr<OSExchangeDataProviderMac>
126OSExchangeDataProviderMac::CreateProvider() {
127  return std::make_unique<OwningProvider>();
128}
129
130// static
131std::unique_ptr<OSExchangeDataProviderMac>
132OSExchangeDataProviderMac::CreateProviderWrappingPasteboard(
133    NSPasteboard* pasteboard) {
134  return std::make_unique<WrappingProvider>(pasteboard);
135}
136
137void OSExchangeDataProviderMac::MarkOriginatedFromRenderer() {
138  NOTIMPLEMENTED();
139}
140
141bool OSExchangeDataProviderMac::DidOriginateFromRenderer() const {
142  NOTIMPLEMENTED();
143  return false;
144}
145
146void OSExchangeDataProviderMac::SetString(const base::string16& string) {
147  [GetPasteboard() setString:base::SysUTF16ToNSString(string)
148                     forType:NSPasteboardTypeString];
149}
150
151void OSExchangeDataProviderMac::SetURL(const GURL& url,
152                                       const base::string16& title) {
153  base::scoped_nsobject<NSPasteboardItem> item =
154      ClipboardUtil::PasteboardItemFromUrl(base::SysUTF8ToNSString(url.spec()),
155                                           base::SysUTF16ToNSString(title));
156  ui::ClipboardUtil::AddDataToPasteboard(GetPasteboard(), item);
157}
158
159void OSExchangeDataProviderMac::SetFilename(const base::FilePath& path) {
160  [GetPasteboard() setPropertyList:@[ base::SysUTF8ToNSString(path.value()) ]
161                           forType:NSFilenamesPboardType];
162}
163
164void OSExchangeDataProviderMac::SetFilenames(
165    const std::vector<FileInfo>& filenames) {
166  if (filenames.empty())
167    return;
168
169  NSMutableArray* paths = [NSMutableArray arrayWithCapacity:filenames.size()];
170
171  for (const auto& filename : filenames) {
172    NSString* path = base::SysUTF8ToNSString(filename.path.value());
173    [paths addObject:path];
174  }
175  [GetPasteboard() setPropertyList:paths forType:NSFilenamesPboardType];
176}
177
178void OSExchangeDataProviderMac::SetPickledData(
179    const ClipboardFormatType& format,
180    const base::Pickle& data) {
181  NSData* ns_data = [NSData dataWithBytes:data.data() length:data.size()];
182  [GetPasteboard() setData:ns_data forType:format.ToNSString()];
183}
184
185bool OSExchangeDataProviderMac::GetString(base::string16* data) const {
186  DCHECK(data);
187  NSString* item = [GetPasteboard() stringForType:NSPasteboardTypeString];
188  if (item) {
189    *data = base::SysNSStringToUTF16(item);
190    return true;
191  }
192
193  // There was no NSString, check for an NSURL.
194  GURL url;
195  base::string16 title;
196  bool result =
197      GetURLAndTitle(OSExchangeData::DO_NOT_CONVERT_FILENAMES, &url, &title);
198  if (result)
199    *data = base::UTF8ToUTF16(url.spec());
200
201  return result;
202}
203
204bool OSExchangeDataProviderMac::GetURLAndTitle(
205    OSExchangeData::FilenameToURLPolicy policy,
206    GURL* url,
207    base::string16* title) const {
208  DCHECK(url);
209  DCHECK(title);
210
211  if (ui::PopulateURLAndTitleFromPasteboard(url, title, GetPasteboard(),
212                                            false)) {
213    return true;
214  }
215
216  // If there are no URLs, try to convert a filename to a URL if the policy
217  // allows it. The title remains blank.
218  //
219  // This could be done in the call to PopulateURLAndTitleFromPasteboard above
220  // if |true| were passed in as the last parameter, but that function strips
221  // the trailing slashes off of paths and always returns the last path element
222  // as the title whereas no path conversion nor title is wanted.
223  base::FilePath path;
224  if (policy != OSExchangeData::DO_NOT_CONVERT_FILENAMES &&
225      GetFilename(&path)) {
226    NSURL* fileUrl =
227        [NSURL fileURLWithPath:base::SysUTF8ToNSString(path.value())];
228    *url =
229        GURL([[fileUrl absoluteString] stringByStandardizingPath].UTF8String);
230    return true;
231  }
232
233  return false;
234}
235
236bool OSExchangeDataProviderMac::GetFilename(base::FilePath* path) const {
237  NSArray* paths = [GetPasteboard() propertyListForType:NSFilenamesPboardType];
238  if ([paths count] == 0)
239    return false;
240
241  *path = base::FilePath(base::SysNSStringToUTF8(paths[0]));
242  return true;
243}
244
245bool OSExchangeDataProviderMac::GetFilenames(
246    std::vector<FileInfo>* filenames) const {
247  NSArray* paths = [GetPasteboard() propertyListForType:NSFilenamesPboardType];
248  if ([paths count] == 0)
249    return false;
250
251  for (NSString* path in paths)
252    filenames->push_back(
253        {base::FilePath(base::SysNSStringToUTF8(path)), base::FilePath()});
254
255  return true;
256}
257
258bool OSExchangeDataProviderMac::GetPickledData(
259    const ClipboardFormatType& format,
260    base::Pickle* data) const {
261  DCHECK(data);
262  NSData* ns_data = [GetPasteboard() dataForType:format.ToNSString()];
263  if (!ns_data)
264    return false;
265
266  *data =
267      base::Pickle(static_cast<const char*>([ns_data bytes]), [ns_data length]);
268  return true;
269}
270
271bool OSExchangeDataProviderMac::HasString() const {
272  base::string16 string;
273  return GetString(&string);
274}
275
276bool OSExchangeDataProviderMac::HasURL(
277    OSExchangeData::FilenameToURLPolicy policy) const {
278  GURL url;
279  base::string16 title;
280  return GetURLAndTitle(policy, &url, &title);
281}
282
283bool OSExchangeDataProviderMac::HasFile() const {
284  return [[GetPasteboard() types] containsObject:NSFilenamesPboardType];
285}
286
287bool OSExchangeDataProviderMac::HasCustomFormat(
288    const ClipboardFormatType& format) const {
289  return [[GetPasteboard() types] containsObject:format.ToNSString()];
290}
291
292void OSExchangeDataProviderMac::SetDragImage(
293    const gfx::ImageSkia& image,
294    const gfx::Vector2d& cursor_offset) {
295  drag_image_ = image;
296  cursor_offset_ = cursor_offset;
297}
298
299gfx::ImageSkia OSExchangeDataProviderMac::GetDragImage() const {
300  return drag_image_;
301}
302
303gfx::Vector2d OSExchangeDataProviderMac::GetDragImageOffset() const {
304  return cursor_offset_;
305}
306
307NSDraggingItem* OSExchangeDataProviderMac::GetDraggingItem() const {
308  // What's going on here is that initiating a drag (-[NSView
309  // beginDraggingSessionWithItems...]) requires a dragging item. Even though
310  // pasteboard items are NSPasteboardWriters, they are locked to their
311  // pasteboard and cannot be used to initiate a drag with another pasteboard
312  // (hello https://crbug.com/928684). Therefore, wrap them.
313  //
314  // OSExchangeDataProviderMac was written to the old NSPasteboard APIs that
315  // didn't account for more than one item. This kinda matches Views which also
316  // assumes that only one drag item can exist at a time. TODO(avi): Fix all of
317  // Views to be able to handle drags of more than one item. Then rewrite
318  // OSExchangeDataProviderMac to the new NSPasteboard item API.
319
320  NSArray* pasteboardItems = [GetPasteboard() pasteboardItems];
321  DCHECK(pasteboardItems);
322  DCHECK_EQ(1u, [pasteboardItems count]);
323
324  CrPasteboardItemWrapper* wrapper = [[[CrPasteboardItemWrapper alloc]
325      initWithPasteboardItem:[pasteboardItems firstObject]] autorelease];
326
327  NSDraggingItem* drag_item =
328      [[[NSDraggingItem alloc] initWithPasteboardWriter:wrapper] autorelease];
329
330  return drag_item;
331}
332
333// static
334NSArray* OSExchangeDataProviderMac::SupportedPasteboardTypes() {
335  return @[
336    kWebCustomDataPboardType, ui::ClipboardUtil::UTIForWebURLsAndTitles(),
337    NSURLPboardType, NSFilenamesPboardType, ui::kChromeDragDummyPboardType,
338    NSStringPboardType, NSHTMLPboardType, NSRTFPboardType,
339    NSFilenamesPboardType, ui::kWebCustomDataPboardType, NSPasteboardTypeString
340  ];
341}
342
343}  // namespace ui
344