[Attachment Support] Support dragging attachment elements out as files on macOS
[WebKit-https.git] / Tools / TestWebKitAPI / mac / DragAndDropSimulatorMac.mm
1 /*
2  * Copyright (C) 2018 Apple Inc. All rights reserved.
3  *
4  * Redistribution and use in source and binary forms, with or without
5  * modification, are permitted provided that the following conditions
6  * are met:
7  * 1. Redistributions of source code must retain the above copyright
8  *    notice, this list of conditions and the following disclaimer.
9  * 2. Redistributions in binary form must reproduce the above copyright
10  *    notice, this list of conditions and the following disclaimer in the
11  *    documentation and/or other materials provided with the distribution.
12  *
13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23  * THE POSSIBILITY OF SUCH DAMAGE.
24  */
25
26 #import "config.h"
27 #import "DragAndDropSimulator.h"
28
29 #if ENABLE(DRAG_SUPPORT) && PLATFORM(MAC) && WK_API_ENABLED
30
31 #import "PlatformUtilities.h"
32 #import "TestDraggingInfo.h"
33 #import "TestWKWebView.h"
34 #import <cmath>
35 #import <wtf/WeakObjCPtr.h>
36
37 @class DragAndDropTestWKWebView;
38
39 @interface DragAndDropSimulator ()
40 - (void)beginDraggingSessionInWebView:(DragAndDropTestWKWebView *)webView withItems:(NSArray<NSDraggingItem *> *)items source:(id<NSDraggingSource>)source;
41 - (void)performDragInWebView:(DragAndDropTestWKWebView *)webView atLocation:(NSPoint)viewLocation withImage:(NSImage *)image pasteboard:(NSPasteboard *)pasteboard source:(id)source;
42 @property (nonatomic, readonly) NSDraggingSession *draggingSession;
43 @end
44
45 @interface DragAndDropTestWKWebView : TestWKWebView
46 @end
47
48 @implementation DragAndDropTestWKWebView {
49     WeakObjCPtr<DragAndDropSimulator> _dragAndDropSimulator;
50 }
51
52 - (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration simulator:(DragAndDropSimulator *)simulator
53 {
54     if (self = [super initWithFrame:frame configuration:configuration])
55         _dragAndDropSimulator = simulator;
56     return self;
57 }
58
59 - (void)dragImage:(NSImage *)image at:(NSPoint)viewLocation offset:(NSSize)initialOffset event:(NSEvent *)event pasteboard:(NSPasteboard *)pboard source:(id)sourceObj slideBack:(BOOL)slideFlag
60 {
61     [_dragAndDropSimulator performDragInWebView:self atLocation:viewLocation withImage:image pasteboard:pboard source:sourceObj];
62 }
63
64 - (NSDraggingSession *)beginDraggingSessionWithItems:(NSArray<NSDraggingItem *> *)items event:(NSEvent *)event source:(id<NSDraggingSource>)source
65 {
66     [_dragAndDropSimulator beginDraggingSessionInWebView:self withItems:items source:source];
67     return [_dragAndDropSimulator draggingSession];
68 }
69
70 - (void)waitForPendingMouseEvents
71 {
72     __block bool doneProcessMouseEvents = false;
73     [self _doAfterProcessingAllPendingMouseEvents:^{
74         doneProcessMouseEvents = true;
75     }];
76     TestWebKitAPI::Util::run(&doneProcessMouseEvents);
77 }
78
79 @end
80
81 // This exceeds the default drag hysteresis of all potential drag types.
82 const double initialMouseDragDistance = 45;
83 const double dragUpdateProgressIncrement = 0.05;
84
85 static NSImage *defaultExternalDragImage()
86 {
87     return [[[NSImage alloc] initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"icon" withExtension:@"png" subdirectory:@"TestWebKitAPI.resources"]] autorelease];
88 }
89
90 @implementation DragAndDropSimulator {
91     RetainPtr<DragAndDropTestWKWebView> _webView;
92     RetainPtr<TestDraggingInfo> _draggingInfo;
93     RetainPtr<NSPasteboard> _externalDragPasteboard;
94     RetainPtr<NSImage> _externalDragImage;
95     RetainPtr<NSArray<NSURL *>> _externalPromisedFiles;
96     RetainPtr<NSMutableArray<_WKAttachment *>> _insertedAttachments;
97     RetainPtr<NSMutableArray<_WKAttachment *>> _removedAttachments;
98     RetainPtr<NSMutableArray<NSURL *>> _filePromiseDestinationURLs;
99     RetainPtr<NSDraggingSession> _draggingSession;
100     RetainPtr<NSMutableArray<NSFilePromiseProvider *>> _filePromiseProviders;
101     BlockPtr<void()> _willEndDraggingHandler;
102     NSPoint _startLocationInWindow;
103     NSPoint _endLocationInWindow;
104     double _progress;
105     bool _doneWaitingForDraggingSession;
106 }
107
108 @synthesize currentDragOperation=_currentDragOperation;
109 @synthesize initialDragImageLocationInView=_initialDragImageLocationInView;
110
111 - (instancetype)initWithWebViewFrame:(CGRect)frame
112 {
113     return [self initWithWebViewFrame:frame configuration:nil];
114 }
115
116 - (instancetype)initWithWebViewFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
117 {
118     if (self = [super init]) {
119         _webView = adoptNS([[DragAndDropTestWKWebView alloc] initWithFrame:frame configuration:configuration ?: [[[WKWebViewConfiguration alloc] init] autorelease] simulator:self]);
120         _filePromiseDestinationURLs = adoptNS([NSMutableArray new]);
121         [_webView setUIDelegate:self];
122     }
123     return self;
124 }
125
126 - (void)dealloc
127 {
128     for (NSURL *url in _filePromiseDestinationURLs.get())
129         [[NSFileManager defaultManager] removeItemAtURL:url error:nil];
130
131     [super dealloc];
132 }
133
134 - (NSPoint)flipAboutXAxisInHostWindow:(NSPoint)point
135 {
136     return { point.x, NSHeight([[_webView hostWindow] frame]) - point.y };
137 }
138
139 - (NSPoint)locationInViewForCurrentProgress
140 {
141     return {
142         _startLocationInWindow.x + (_endLocationInWindow.x - _startLocationInWindow.x) * _progress,
143         _startLocationInWindow.y + (_endLocationInWindow.y - _startLocationInWindow.y) * _progress
144     };
145 }
146
147 - (double)initialProgressForMouseDrag
148 {
149     double totalDistance = std::sqrt(std::pow(_startLocationInWindow.x - _endLocationInWindow.x, 2) + std::pow(_startLocationInWindow.y - _endLocationInWindow.y, 2));
150     return !totalDistance ? 1 : std::min<double>(1, initialMouseDragDistance / totalDistance);
151 }
152
153 - (void)runFrom:(CGPoint)flippedStartLocation to:(CGPoint)flippedEndLocation
154 {
155     _insertedAttachments = adoptNS([NSMutableArray new]);
156     _removedAttachments = adoptNS([NSMutableArray new]);
157     _doneWaitingForDraggingSession = true;
158     _startLocationInWindow = [self flipAboutXAxisInHostWindow:flippedStartLocation];
159     _endLocationInWindow = [self flipAboutXAxisInHostWindow:flippedEndLocation];
160     _currentDragOperation = NSDragOperationNone;
161     _draggingInfo = nil;
162     _draggingSession = nil;
163     _progress = 0;
164     _filePromiseProviders = adoptNS([NSMutableArray new]);
165
166     if (NSPasteboard *pasteboard = self.externalDragPasteboard) {
167         NSPoint startLocationInView = [_webView convertPoint:_startLocationInWindow fromView:nil];
168         NSImage *dragImage = self.externalDragImage ?: defaultExternalDragImage();
169         [self performDragInWebView:_webView.get() atLocation:startLocationInView withImage:dragImage pasteboard:pasteboard source:nil];
170         return;
171     }
172
173     _progress = [self initialProgressForMouseDrag];
174     if (_progress == 1) {
175         [NSException raise:@"DragAndDropSimulator" format:@"Drag start (%@) and drag end (%@) locations are too close!", NSStringFromPoint(flippedStartLocation), NSStringFromPoint(flippedEndLocation)];
176         return;
177     }
178
179     [_webView mouseEnterAtPoint:_startLocationInWindow];
180     [_webView mouseMoveToPoint:_startLocationInWindow withFlags:0];
181     [_webView mouseDownAtPoint:_startLocationInWindow simulatePressure:NO];
182     [_webView mouseDragToPoint:[self locationInViewForCurrentProgress]];
183     [_webView waitForPendingMouseEvents];
184
185     TestWebKitAPI::Util::run(&_doneWaitingForDraggingSession);
186
187     [_webView mouseUpAtPoint:_endLocationInWindow];
188     [_webView waitForPendingMouseEvents];
189 }
190
191 - (void)beginDraggingSessionInWebView:(DragAndDropTestWKWebView *)webView withItems:(NSArray<NSDraggingItem *> *)items source:(id<NSDraggingSource>)source
192 {
193     NSMutableArray *pasteboardObjects = [NSMutableArray arrayWithCapacity:items.count];
194     NSMutableArray<NSString *> *promisedFileTypes = [NSMutableArray array];
195     for (NSDraggingItem *item in items) {
196         id pasteboardObject = item.item;
197         [pasteboardObjects addObject:pasteboardObject];
198         if ([pasteboardObject isKindOfClass:[NSFilePromiseProvider class]]) {
199             [_filePromiseProviders addObject:pasteboardObject];
200             [promisedFileTypes addObject:[(NSFilePromiseProvider *)pasteboardObject fileType]];
201         }
202     }
203
204     NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard];
205     [pasteboard clearContents];
206     [pasteboard writeObjects:pasteboardObjects];
207     if (promisedFileTypes.count) {
208         // Match AppKit behavior by writing legacy file promise types to the pasteboard as well.
209         [pasteboard setPropertyList:promisedFileTypes forType:NSFilesPromisePboardType];
210         [pasteboard addTypes:@[@"NSPromiseContentsPboardType", (NSString *)kPasteboardTypeFileURLPromise] owner:nil];
211     }
212
213     _draggingSession = adoptNS([[NSDraggingSession alloc] init]);
214     _doneWaitingForDraggingSession = false;
215     _initialDragImageLocationInView = items[0].draggingFrame.origin;
216     id dragImageContents = items[0].imageComponents.firstObject.contents;
217     [self initializeDraggingInfo:pasteboard dragImage:[dragImageContents isKindOfClass:[NSImage class]] ? dragImageContents : nil source:source];
218
219     _currentDragOperation = [_webView draggingEntered:_draggingInfo.get()];
220     [_webView waitForNextPresentationUpdate];
221     [self performSelector:@selector(continueDragSession) withObject:nil afterDelay:0];
222 }
223
224 - (void)continueDragSession
225 {
226     _progress = std::min<double>(1, _progress + dragUpdateProgressIncrement);
227
228     if (_progress < 1) {
229         [_draggingInfo setDraggingLocation:[self locationInViewForCurrentProgress]];
230         _currentDragOperation = [_webView draggingUpdated:_draggingInfo.get()];
231         [_webView waitForNextPresentationUpdate];
232         [self performSelector:@selector(continueDragSession) withObject:nil afterDelay:0];
233         return;
234     }
235
236     [_draggingInfo setDraggingLocation:_endLocationInWindow];
237
238     if (_willEndDraggingHandler)
239         _willEndDraggingHandler();
240
241     if (_currentDragOperation != NSDragOperationNone && [_webView prepareForDragOperation:_draggingInfo.get()])
242         [_webView performDragOperation:_draggingInfo.get()];
243     else if (_currentDragOperation == NSDragOperationNone)
244         [_webView draggingExited:_draggingInfo.get()];
245     [_webView waitForNextPresentationUpdate];
246     [(id <NSDraggingSource>)_webView.get() draggingSession:_draggingSession.get() endedAtPoint:_endLocationInWindow operation:_currentDragOperation];
247
248     _doneWaitingForDraggingSession = true;
249 }
250
251 - (void)performDragInWebView:(DragAndDropTestWKWebView *)webView atLocation:(NSPoint)viewLocation withImage:(NSImage *)image pasteboard:(NSPasteboard *)pasteboard source:(id)source
252 {
253     _initialDragImageLocationInView = viewLocation;
254     [self initializeDraggingInfo:pasteboard dragImage:image source:source];
255
256     _currentDragOperation = [_webView draggingEntered:_draggingInfo.get()];
257     [_webView waitForNextPresentationUpdate];
258
259     while (_progress != 1) {
260         _progress = std::min<double>(1, _progress + dragUpdateProgressIncrement);
261         [_draggingInfo setDraggingLocation:[self locationInViewForCurrentProgress]];
262         _currentDragOperation = [_webView draggingUpdated:_draggingInfo.get()];
263         [_webView waitForNextPresentationUpdate];
264     }
265
266     [_draggingInfo setDraggingLocation:_endLocationInWindow];
267
268     if (_willEndDraggingHandler)
269         _willEndDraggingHandler();
270
271     if (_currentDragOperation != NSDragOperationNone && [_webView prepareForDragOperation:_draggingInfo.get()])
272         [_webView performDragOperation:_draggingInfo.get()];
273     else if (_currentDragOperation == NSDragOperationNone)
274         [_webView draggingExited:_draggingInfo.get()];
275     [_webView waitForNextPresentationUpdate];
276
277     if (!self.externalDragPasteboard) {
278         [_webView draggedImage:[_draggingInfo draggedImage] endedAt:_endLocationInWindow operation:_currentDragOperation];
279         [_webView waitForNextPresentationUpdate];
280     }
281 }
282
283 - (void)initializeDraggingInfo:(NSPasteboard *)pasteboard dragImage:(NSImage *)image source:(id)source
284 {
285     _draggingInfo = adoptNS([[TestDraggingInfo alloc] initWithDragAndDropSimulator:self]);
286     [_draggingInfo setDraggedImage:image];
287     [_draggingInfo setDraggingPasteboard:pasteboard];
288     [_draggingInfo setDraggingSource:source];
289     [_draggingInfo setDraggingLocation:[self locationInViewForCurrentProgress]];
290     [_draggingInfo setDraggingSourceOperationMask:NSDragOperationEvery];
291     [_draggingInfo setNumberOfValidItemsForDrop:pasteboard.pasteboardItems.count];
292 }
293
294 - (NSArray<_WKAttachment *> *)insertedAttachments
295 {
296     return _insertedAttachments.get();
297 }
298
299 - (NSArray<_WKAttachment *> *)removedAttachments
300 {
301     return _removedAttachments.get();
302 }
303
304 - (TestWKWebView *)webView
305 {
306     return _webView.get();
307 }
308
309 - (void)setExternalDragPasteboard:(NSPasteboard *)externalDragPasteboard
310 {
311     _externalDragPasteboard = externalDragPasteboard;
312 }
313
314 - (NSPasteboard *)externalDragPasteboard
315 {
316     return _externalDragPasteboard.get();
317 }
318
319 - (void)setExternalDragImage:(NSImage *)externalDragImage
320 {
321     _externalDragImage = externalDragImage;
322 }
323
324 - (NSImage *)externalDragImage
325 {
326     return _externalDragImage.get();
327 }
328
329 - (NSDraggingSession *)draggingSession
330 {
331     return _draggingSession.get();
332 }
333
334 - (id <NSDraggingInfo>)draggingInfo
335 {
336     return _draggingInfo.get();
337 }
338
339 - (dispatch_block_t)willEndDraggingHandler
340 {
341     return _willEndDraggingHandler.get();
342 }
343
344 - (void)setWillEndDraggingHandler:(dispatch_block_t)willEndDraggingHandler
345 {
346     _willEndDraggingHandler = makeBlockPtr(willEndDraggingHandler);
347 }
348
349 - (NSArray<NSURL *> *)externalPromisedFiles
350 {
351     return _externalPromisedFiles.get();
352 }
353
354 static BOOL getFilePathsAndTypeIdentifiers(NSArray<NSURL *> *fileURLs, NSArray<NSString *> **outFilePaths, NSArray<NSString *> **outTypeIdentifiers)
355 {
356     NSMutableArray *filePaths = [NSMutableArray arrayWithCapacity:fileURLs.count];
357     NSMutableArray *typeIdentifiers = [NSMutableArray arrayWithCapacity:fileURLs.count];
358     for (NSURL *url in fileURLs) {
359         NSString *typeIdentifier = nil;
360         NSError *error = nil;
361         BOOL foundUTI = [url getResourceValue:&typeIdentifier forKey:NSURLTypeIdentifierKey error:&error];
362         if (!foundUTI || error) {
363             [NSException raise:@"DragAndDropSimulator" format:@"Failed to get UTI for promised file: %@ with error: %@", url, error];
364             continue;
365         }
366         [typeIdentifiers addObject:typeIdentifier];
367         [filePaths addObject:url.path];
368     }
369
370     if (fileURLs.count != filePaths.count)
371         return NO;
372
373     if (outTypeIdentifiers)
374         *outTypeIdentifiers = typeIdentifiers;
375
376     if (outFilePaths)
377         *outFilePaths = filePaths;
378
379     return YES;
380 }
381
382 - (void)writePromisedFiles:(NSArray<NSURL *> *)fileURLs
383 {
384     NSArray *paths = nil;
385     NSArray *types = nil;
386     if (!getFilePathsAndTypeIdentifiers(fileURLs, &paths, &types))
387         return;
388
389     NSMutableArray *names = [NSMutableArray arrayWithCapacity:paths.count];
390     for (NSString *path in paths)
391         [names addObject:path.lastPathComponent];
392
393     _externalPromisedFiles = fileURLs;
394     _externalDragPasteboard = [NSPasteboard pasteboardWithUniqueName];
395     [_externalDragPasteboard declareTypes:@[NSFilesPromisePboardType, NSFilenamesPboardType] owner:nil];
396     [_externalDragPasteboard setPropertyList:types forType:NSFilesPromisePboardType];
397     [_externalDragPasteboard setPropertyList:names forType:NSFilenamesPboardType];
398 }
399
400 - (void)writeFiles:(NSArray<NSURL *> *)fileURLs
401 {
402     NSArray *paths = nil;
403     if (!getFilePathsAndTypeIdentifiers(fileURLs, &paths, nil))
404         return;
405
406     _externalDragPasteboard = [NSPasteboard pasteboardWithName:NSDragPboard];
407     [_externalDragPasteboard declareTypes:@[NSFilenamesPboardType] owner:nil];
408     [_externalDragPasteboard setPropertyList:paths forType:NSFilenamesPboardType];
409 }
410
411 - (NSArray<NSURL *> *)receivePromisedFiles
412 {
413     auto destinationURLs = adoptNS([NSMutableArray new]);
414     for (NSFilePromiseProvider *provider in _filePromiseProviders.get()) {
415         if (!provider.delegate)
416             continue;
417
418         int suffix = 1;
419         NSString *baseFileName = [provider.delegate filePromiseProvider:provider fileNameForType:provider.fileType];
420         NSString *uniqueFileName = baseFileName;
421         while ([[NSFileManager defaultManager] fileExistsAtPath:[NSTemporaryDirectory() stringByAppendingPathComponent:uniqueFileName]])
422             uniqueFileName = [NSString stringWithFormat:@"%@ %d", baseFileName, ++suffix];
423
424         NSURL *destinationURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:uniqueFileName]];
425         __block bool done = false;
426         [provider.delegate filePromiseProvider:provider writePromiseToURL:destinationURL completionHandler:^(NSError *) {
427             done = true;
428         }];
429         TestWebKitAPI::Util::run(&done);
430         [destinationURLs addObject:destinationURL];
431         [_filePromiseDestinationURLs addObject:destinationURL];
432     }
433     return destinationURLs.autorelease();
434 }
435
436 - (void)endDataTransfer
437 {
438 }
439
440 - (void)_webView:(WKWebView *)webView didInsertAttachment:(_WKAttachment *)attachment withSource:(NSString *)source
441 {
442     [_insertedAttachments addObject:attachment];
443 }
444
445 - (void)_webView:(WKWebView *)webView didRemoveAttachment:(_WKAttachment *)attachment
446 {
447     [_removedAttachments addObject:attachment];
448 }
449
450 @end
451
452 #endif // ENABLE(DRAG_SUPPORT) && PLATFORM(MAC) && WK_API_ENABLED