blob: 766f105b0ad2c908dee70dcad7ffc43bf07c7cdc [file] [log] [blame]
/*
* Copyright (C) 2018 Apple Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions
* are met:
* 1. Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright
* notice, this list of conditions and the following disclaimer in the
* documentation and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
* AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
* THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
* PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
* BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
* CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
* SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
* INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
* CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
#import "config.h"
#import "DragAndDropSimulator.h"
#if ENABLE(DRAG_SUPPORT) && PLATFORM(MAC)
#import "PlatformUtilities.h"
#import "TestDraggingInfo.h"
#import "TestWKWebView.h"
#import <WebKit/WKWebViewPrivateForTesting.h>
#import <cmath>
#import <wtf/WeakObjCPtr.h>
@class DragAndDropTestWKWebView;
@interface DragAndDropSimulator ()
- (void)beginDraggingSessionInWebView:(DragAndDropTestWKWebView *)webView withItems:(NSArray<NSDraggingItem *> *)items source:(id<NSDraggingSource>)source;
- (void)performDragInWebView:(DragAndDropTestWKWebView *)webView atLocation:(NSPoint)viewLocation withImage:(NSImage *)image pasteboard:(NSPasteboard *)pasteboard source:(id)source;
@property (nonatomic, readonly) NSDraggingSession *draggingSession;
@end
@interface DragAndDropTestWKWebView : TestWKWebView
@end
@implementation DragAndDropTestWKWebView {
WeakObjCPtr<DragAndDropSimulator> _dragAndDropSimulator;
}
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration simulator:(DragAndDropSimulator *)simulator
{
if (self = [super initWithFrame:frame configuration:configuration])
_dragAndDropSimulator = simulator;
return self;
}
IGNORE_WARNINGS_BEGIN("deprecated-implementations")
- (void)dragImage:(NSImage *)image at:(NSPoint)viewLocation offset:(NSSize)initialOffset event:(NSEvent *)event pasteboard:(NSPasteboard *)pboard source:(id)sourceObj slideBack:(BOOL)slideFlag
IGNORE_WARNINGS_END
{
[_dragAndDropSimulator performDragInWebView:self atLocation:viewLocation withImage:image pasteboard:pboard source:sourceObj];
}
- (NSDraggingSession *)beginDraggingSessionWithItems:(NSArray<NSDraggingItem *> *)items event:(NSEvent *)event source:(id<NSDraggingSource>)source
{
[_dragAndDropSimulator beginDraggingSessionInWebView:self withItems:items source:source];
return [_dragAndDropSimulator draggingSession];
}
@end
// This exceeds the default drag hysteresis of all potential drag types.
const double initialMouseDragDistance = 45;
const double dragUpdateProgressIncrement = 0.05;
static RetainPtr<NSImage> defaultExternalDragImage()
{
return adoptNS([[NSImage alloc] initWithContentsOfURL:[[NSBundle mainBundle] URLForResource:@"icon" withExtension:@"png" subdirectory:@"TestWebKitAPI.resources"]]);
}
@implementation DragAndDropSimulator {
RetainPtr<DragAndDropTestWKWebView> _webView;
RetainPtr<TestDraggingInfo> _draggingInfo;
RetainPtr<NSPasteboard> _externalDragPasteboard;
RetainPtr<NSImage> _externalDragImage;
RetainPtr<NSArray<NSURL *>> _externalPromisedFiles;
RetainPtr<NSMutableArray<_WKAttachment *>> _insertedAttachments;
RetainPtr<NSMutableArray<_WKAttachment *>> _removedAttachments;
RetainPtr<NSMutableArray<NSURL *>> _filePromiseDestinationURLs;
RetainPtr<NSDraggingSession> _draggingSession;
RetainPtr<NSMutableArray<NSFilePromiseProvider *>> _filePromiseProviders;
BlockPtr<void()> _willEndDraggingHandler;
NSPoint _startLocationInWindow;
NSPoint _endLocationInWindow;
double _progress;
bool _doneWaitingForDraggingSession;
bool _doneWaitingForDrop;
}
@synthesize currentDragOperation = _currentDragOperation;
@synthesize initialDragImageLocationInView = _initialDragImageLocationInView;
- (instancetype)initWithWebViewFrame:(CGRect)frame
{
return [self initWithWebViewFrame:frame configuration:nil];
}
- (instancetype)initWithWebViewFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
{
if (self = [super init]) {
_webView = adoptNS([[DragAndDropTestWKWebView alloc] initWithFrame:frame configuration:configuration ?: adoptNS([[WKWebViewConfiguration alloc] init]).get() simulator:self]);
_filePromiseDestinationURLs = adoptNS([NSMutableArray new]);
[_webView setUIDelegate:self];
self.dragDestinationAction = WKDragDestinationActionAny & ~WKDragDestinationActionLoad;
[[NSPasteboard pasteboardWithName:NSPasteboardNameDrag] clearContents];
}
return self;
}
- (void)dealloc
{
for (NSURL *url in _filePromiseDestinationURLs.get())
[[NSFileManager defaultManager] removeItemAtURL:url error:nil];
[super dealloc];
}
- (NSPoint)flipAboutXAxisInHostWindow:(NSPoint)point
{
return { point.x, NSHeight([[_webView hostWindow] frame]) - point.y };
}
- (NSPoint)locationInViewForCurrentProgress
{
return {
_startLocationInWindow.x + (_endLocationInWindow.x - _startLocationInWindow.x) * _progress,
_startLocationInWindow.y + (_endLocationInWindow.y - _startLocationInWindow.y) * _progress
};
}
- (double)initialProgressForMouseDrag
{
double totalDistance = std::hypot(_startLocationInWindow.x - _endLocationInWindow.x, _startLocationInWindow.y - _endLocationInWindow.y);
return !totalDistance ? 1 : std::min<double>(1, initialMouseDragDistance / totalDistance);
}
- (void)runFrom:(CGPoint)flippedStartLocation to:(CGPoint)flippedEndLocation
{
_insertedAttachments = adoptNS([NSMutableArray new]);
_removedAttachments = adoptNS([NSMutableArray new]);
_doneWaitingForDraggingSession = true;
_doneWaitingForDrop = true;
_startLocationInWindow = [self flipAboutXAxisInHostWindow:flippedStartLocation];
_endLocationInWindow = [self flipAboutXAxisInHostWindow:flippedEndLocation];
_currentDragOperation = NSDragOperationNone;
_draggingInfo = nil;
_draggingSession = nil;
_progress = 0;
_filePromiseProviders = adoptNS([NSMutableArray new]);
if (NSPasteboard *pasteboard = self.externalDragPasteboard) {
NSPoint startLocationInView = [_webView convertPoint:_startLocationInWindow fromView:nil];
auto dragImage = !self.externalDragImage ? defaultExternalDragImage(): nil;
[self performDragInWebView:_webView.get() atLocation:startLocationInView withImage:dragImage.get() pasteboard:pasteboard source:nil];
TestWebKitAPI::Util::run(&_doneWaitingForDrop);
return;
}
_progress = [self initialProgressForMouseDrag];
if (_progress == 1) {
[NSException raise:@"DragAndDropSimulator" format:@"Drag start (%@) and drag end (%@) locations are too close!", NSStringFromPoint(flippedStartLocation), NSStringFromPoint(flippedEndLocation)];
return;
}
[_webView mouseEnterAtPoint:_startLocationInWindow];
[_webView mouseMoveToPoint:_startLocationInWindow withFlags:0];
[_webView mouseDownAtPoint:_startLocationInWindow simulatePressure:NO];
// Make sure that we exceed the minimum 150ms delay between handling mousedown and drag when dragging a text selection.
[_webView setEventTimestampOffset:0.25];
[_webView mouseDragToPoint:[self locationInViewForCurrentProgress]];
[_webView waitForPendingMouseEvents];
TestWebKitAPI::Util::run(&_doneWaitingForDraggingSession);
[_webView mouseUpAtPoint:_endLocationInWindow];
[_webView waitForPendingMouseEvents];
TestWebKitAPI::Util::run(&_doneWaitingForDrop);
[_webView setEventTimestampOffset:0];
}
- (void)beginDraggingSessionInWebView:(DragAndDropTestWKWebView *)webView withItems:(NSArray<NSDraggingItem *> *)items source:(id<NSDraggingSource>)source
{
NSMutableArray *pasteboardObjects = [NSMutableArray arrayWithCapacity:items.count];
NSMutableArray<NSString *> *promisedFileTypes = [NSMutableArray array];
for (NSDraggingItem *item in items) {
id pasteboardObject = item.item;
[pasteboardObjects addObject:pasteboardObject];
if ([pasteboardObject isKindOfClass:[NSFilePromiseProvider class]]) {
[_filePromiseProviders addObject:pasteboardObject];
[promisedFileTypes addObject:[(NSFilePromiseProvider *)pasteboardObject fileType]];
}
}
NSPasteboard *pasteboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[pasteboard clearContents];
[pasteboard writeObjects:pasteboardObjects];
if (promisedFileTypes.count) {
// Match AppKit behavior by writing legacy file promise types to the pasteboard as well.
[pasteboard setPropertyList:promisedFileTypes forType:NSFilesPromisePboardType];
[pasteboard addTypes:@[@"NSPromiseContentsPboardType", (NSString *)kPasteboardTypeFileURLPromise] owner:nil];
}
_draggingSession = adoptNS([[NSDraggingSession alloc] init]);
_doneWaitingForDraggingSession = false;
_initialDragImageLocationInView = items[0].draggingFrame.origin;
id dragImageContents = items[0].imageComponents.firstObject.contents;
[self initializeDraggingInfo:pasteboard dragImage:[dragImageContents isKindOfClass:[NSImage class]] ? dragImageContents : nil source:source];
_currentDragOperation = [_webView draggingEntered:_draggingInfo.get()];
[_webView waitForNextPresentationUpdate];
[self performSelector:@selector(continueDragSession) withObject:nil afterDelay:0];
}
- (void)continueDragSession
{
_progress = std::min<double>(1, _progress + dragUpdateProgressIncrement);
if (_progress < 1) {
[_draggingInfo setDraggingLocation:[self locationInViewForCurrentProgress]];
_currentDragOperation = [_webView draggingUpdated:_draggingInfo.get()];
[_webView waitForNextPresentationUpdate];
[self performSelector:@selector(continueDragSession) withObject:nil afterDelay:0];
return;
}
[_draggingInfo setDraggingLocation:_endLocationInWindow];
if (_willEndDraggingHandler)
_willEndDraggingHandler();
if (_currentDragOperation != NSDragOperationNone && [_webView prepareForDragOperation:_draggingInfo.get()]) {
_doneWaitingForDrop = false;
[_webView performDragOperation:_draggingInfo.get()];
} else if (_currentDragOperation == NSDragOperationNone)
[_webView draggingExited:_draggingInfo.get()];
[_webView waitForNextPresentationUpdate];
[(id <NSDraggingSource>)_webView.get() draggingSession:_draggingSession.get() endedAtPoint:_endLocationInWindow operation:_currentDragOperation];
_doneWaitingForDraggingSession = true;
}
- (void)performDragInWebView:(DragAndDropTestWKWebView *)webView atLocation:(NSPoint)viewLocation withImage:(NSImage *)image pasteboard:(NSPasteboard *)pasteboard source:(id)source
{
_initialDragImageLocationInView = viewLocation;
[self initializeDraggingInfo:pasteboard dragImage:image source:source];
_currentDragOperation = [_webView draggingEntered:_draggingInfo.get()];
[_webView waitForNextPresentationUpdate];
while (_progress != 1) {
_progress = std::min<double>(1, _progress + dragUpdateProgressIncrement);
[_draggingInfo setDraggingLocation:[self locationInViewForCurrentProgress]];
_currentDragOperation = [_webView draggingUpdated:_draggingInfo.get()];
[_webView waitForNextPresentationUpdate];
}
[_draggingInfo setDraggingLocation:_endLocationInWindow];
if (_willEndDraggingHandler)
_willEndDraggingHandler();
if (_currentDragOperation != NSDragOperationNone && [_webView prepareForDragOperation:_draggingInfo.get()]) {
_doneWaitingForDrop = false;
[_webView performDragOperation:_draggingInfo.get()];
} else if (_currentDragOperation == NSDragOperationNone)
[_webView draggingExited:_draggingInfo.get()];
[_webView waitForNextPresentationUpdate];
if (!self.externalDragPasteboard) {
[_webView draggedImage:[_draggingInfo draggedImage] endedAt:_endLocationInWindow operation:_currentDragOperation];
[_webView waitForNextPresentationUpdate];
}
}
- (void)initializeDraggingInfo:(NSPasteboard *)pasteboard dragImage:(NSImage *)image source:(id)source
{
_draggingInfo = adoptNS([[TestDraggingInfo alloc] initWithDragAndDropSimulator:self]);
[_draggingInfo setDraggedImage:image];
[_draggingInfo setDraggingPasteboard:pasteboard];
[_draggingInfo setDraggingSource:source];
[_draggingInfo setDraggingLocation:[self locationInViewForCurrentProgress]];
[_draggingInfo setDraggingSourceOperationMask:NSDragOperationEvery];
[_draggingInfo setNumberOfValidItemsForDrop:pasteboard.pasteboardItems.count];
}
- (NSArray<_WKAttachment *> *)insertedAttachments
{
return _insertedAttachments.get();
}
- (NSArray<_WKAttachment *> *)removedAttachments
{
return _removedAttachments.get();
}
- (TestWKWebView *)webView
{
return _webView.get();
}
- (void)setExternalDragPasteboard:(NSPasteboard *)externalDragPasteboard
{
_externalDragPasteboard = externalDragPasteboard;
}
- (NSPasteboard *)externalDragPasteboard
{
return _externalDragPasteboard.get();
}
- (void)setExternalDragImage:(NSImage *)externalDragImage
{
_externalDragImage = externalDragImage;
}
- (NSImage *)externalDragImage
{
return _externalDragImage.get();
}
- (NSDraggingSession *)draggingSession
{
return _draggingSession.get();
}
- (id <NSDraggingInfo>)draggingInfo
{
return _draggingInfo.get();
}
- (dispatch_block_t)willEndDraggingHandler
{
return _willEndDraggingHandler.get();
}
- (void)setWillEndDraggingHandler:(dispatch_block_t)willEndDraggingHandler
{
_willEndDraggingHandler = makeBlockPtr(willEndDraggingHandler);
}
- (NSArray<NSURL *> *)externalPromisedFiles
{
return _externalPromisedFiles.get();
}
- (void)clearExternalDragInformation
{
_externalPromisedFiles = nil;
_externalDragImage = nil;
_externalDragPasteboard = nil;
}
static BOOL getFilePathsAndTypeIdentifiers(NSArray<NSURL *> *fileURLs, NSArray<NSString *> **outFilePaths, NSArray<NSString *> **outTypeIdentifiers)
{
NSMutableArray *filePaths = [NSMutableArray arrayWithCapacity:fileURLs.count];
NSMutableArray *typeIdentifiers = [NSMutableArray arrayWithCapacity:fileURLs.count];
for (NSURL *url in fileURLs) {
NSString *typeIdentifier = nil;
NSError *error = nil;
BOOL foundUTI = [url getResourceValue:&typeIdentifier forKey:NSURLTypeIdentifierKey error:&error];
if (!foundUTI || error) {
[NSException raise:@"DragAndDropSimulator" format:@"Failed to get UTI for promised file: %@ with error: %@", url, error];
continue;
}
[typeIdentifiers addObject:typeIdentifier];
[filePaths addObject:url.path];
}
if (fileURLs.count != filePaths.count)
return NO;
if (outTypeIdentifiers)
*outTypeIdentifiers = typeIdentifiers;
if (outFilePaths)
*outFilePaths = filePaths;
return YES;
}
- (void)writePromisedFiles:(NSArray<NSURL *> *)fileURLs
{
NSArray *paths = nil;
NSArray *types = nil;
if (!getFilePathsAndTypeIdentifiers(fileURLs, &paths, &types))
return;
NSMutableArray *names = [NSMutableArray arrayWithCapacity:paths.count];
for (NSString *path in paths)
[names addObject:path.lastPathComponent];
_externalPromisedFiles = fileURLs;
_externalDragPasteboard = [NSPasteboard pasteboardWithUniqueName];
[_externalDragPasteboard declareTypes:@[NSFilesPromisePboardType, NSFilenamesPboardType] owner:nil];
[_externalDragPasteboard setPropertyList:types forType:NSFilesPromisePboardType];
[_externalDragPasteboard setPropertyList:names forType:NSFilenamesPboardType];
}
- (void)writeFiles:(NSArray<NSURL *> *)fileURLs
{
NSArray *paths = nil;
if (!getFilePathsAndTypeIdentifiers(fileURLs, &paths, nil))
return;
_externalDragPasteboard = [NSPasteboard pasteboardWithName:NSDragPboard];
[_externalDragPasteboard declareTypes:@[NSFilenamesPboardType] owner:nil];
[_externalDragPasteboard setPropertyList:paths forType:NSFilenamesPboardType];
}
- (NSArray<NSURL *> *)receivePromisedFiles
{
auto destinationURLs = adoptNS([NSMutableArray new]);
for (NSFilePromiseProvider *provider in _filePromiseProviders.get()) {
if (!provider.delegate)
continue;
int suffix = 1;
NSString *baseFileName = [provider.delegate filePromiseProvider:provider fileNameForType:provider.fileType];
NSString *uniqueFileName = baseFileName;
while ([[NSFileManager defaultManager] fileExistsAtPath:[NSTemporaryDirectory() stringByAppendingPathComponent:uniqueFileName]])
uniqueFileName = [NSString stringWithFormat:@"%@ %d", baseFileName, ++suffix];
NSURL *destinationURL = [NSURL fileURLWithPath:[NSTemporaryDirectory() stringByAppendingPathComponent:uniqueFileName]];
__block bool done = false;
[provider.delegate filePromiseProvider:provider writePromiseToURL:destinationURL completionHandler:^(NSError *) {
done = true;
}];
TestWebKitAPI::Util::run(&done);
[destinationURLs addObject:destinationURL];
[_filePromiseDestinationURLs addObject:destinationURL];
}
return destinationURLs.autorelease();
}
- (void)endDataTransfer
{
}
- (BOOL)containsDraggedType:(NSString *)expectedType
{
for (NSPasteboardType type in [NSPasteboard pasteboardWithName:NSPasteboardNameDrag].types) {
if ([type isEqualToString:expectedType])
return YES;
}
return NO;
}
#pragma mark - WKUIDelegatePrivate
- (void)_webView:(WKWebView *)webView didInsertAttachment:(_WKAttachment *)attachment withSource:(NSString *)source
{
[_insertedAttachments addObject:attachment];
}
- (void)_webView:(WKWebView *)webView didRemoveAttachment:(_WKAttachment *)attachment
{
[_removedAttachments addObject:attachment];
}
- (void)_webView:(WKWebView *)webView didPerformDragOperation:(BOOL)handled
{
_doneWaitingForDrop = true;
}
- (WKDragDestinationAction)_webView:(WKWebView *)webView dragDestinationActionMaskForDraggingInfo:(id)draggingInfo
{
return self.dragDestinationAction;
}
@end
#endif // ENABLE(DRAG_SUPPORT) && PLATFORM(MAC)