| /* |
| * Copyright (C) 2017 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(IOS_FAMILY) && !PLATFORM(MACCATALYST) |
| |
| #import "InstanceMethodSwizzler.h" |
| #import "PlatformUtilities.h" |
| #import "Test.h" |
| #import "UIKitSPI.h" |
| #import <UIKit/UIDragInteraction.h> |
| #import <UIKit/UIDragItem.h> |
| #import <UIKit/UIDropInteraction.h> |
| #import <UIKit/UIInteraction.h> |
| #import <WebKit/WKWebViewPrivateForTesting.h> |
| #import <WebKit/_WKFocusedElementInfo.h> |
| #import <WebKit/_WKFormInputSession.h> |
| #import <wtf/RetainPtr.h> |
| #import <wtf/SoftLinking.h> |
| |
| using namespace TestWebKitAPI; |
| |
| @implementation WKWebView (DragAndDropTesting) |
| |
| - (UIView *)_dragDropInteractionView |
| { |
| return [self valueForKey:@"_currentContentView"]; |
| } |
| |
| - (id <UIDropInteractionDelegate>)dropInteractionDelegate |
| { |
| return (id <UIDropInteractionDelegate>)self._dragDropInteractionView; |
| } |
| |
| - (id <UIDragInteractionDelegate>)dragInteractionDelegate |
| { |
| return (id <UIDragInteractionDelegate>)self._dragDropInteractionView; |
| } |
| |
| - (UIDropInteraction *)dropInteraction |
| { |
| UIView *interactionView = self._dragDropInteractionView; |
| for (id <UIInteraction> interaction in interactionView.interactions) { |
| if ([interaction isKindOfClass:[UIDropInteraction class]]) |
| return (UIDropInteraction *)interaction; |
| } |
| return nil; |
| } |
| |
| - (UIDragInteraction *)dragInteraction |
| { |
| UIView *interactionView = self._dragDropInteractionView; |
| for (id <UIInteraction> interaction in interactionView.interactions) { |
| if ([interaction isKindOfClass:[UIDragInteraction class]]) |
| return (UIDragInteraction *)interaction; |
| } |
| return nil; |
| } |
| |
| @end |
| |
| @implementation MockDragDropSession |
| |
| - (instancetype)initWithItems:(NSArray <UIDragItem *>*)items location:(CGPoint)locationInWindow window:(UIWindow *)window allowMove:(BOOL)allowMove |
| { |
| if (self = [super init]) { |
| _mockItems = items; |
| _mockLocationInWindow = locationInWindow; |
| _window = window; |
| _allowMove = allowMove; |
| } |
| return self; |
| } |
| |
| - (BOOL)allowsMoveOperation |
| { |
| return _allowMove; |
| } |
| |
| - (BOOL)isRestrictedToDraggingApplication |
| { |
| return NO; |
| } |
| |
| - (BOOL)hasItemsConformingToTypeIdentifiers:(NSArray<NSString *> *)typeIdentifiers |
| { |
| for (NSString *typeIdentifier in typeIdentifiers) { |
| BOOL hasItemConformingToType = NO; |
| for (UIDragItem *item in self.items) |
| hasItemConformingToType |= [[item.itemProvider registeredTypeIdentifiers] containsObject:typeIdentifier]; |
| if (!hasItemConformingToType) |
| return NO; |
| } |
| return YES; |
| } |
| |
| - (BOOL)canLoadObjectsOfClass:(Class<NSItemProviderReading>)aClass |
| { |
| for (UIDragItem *item in self.items) { |
| if ([item.itemProvider canLoadObjectOfClass:aClass]) |
| return YES; |
| } |
| return NO; |
| } |
| |
| - (BOOL)canLoadObjectsOfClasses:(NSArray<Class<NSItemProviderReading>> *)classes |
| { |
| for (Class<NSItemProviderReading> aClass in classes) { |
| BOOL canLoad = NO; |
| for (UIDragItem *item in self.items) |
| canLoad |= [item.itemProvider canLoadObjectOfClass:aClass]; |
| if (!canLoad) |
| return NO; |
| } |
| return YES; |
| } |
| |
| - (NSArray<UIDragItem *> *)items |
| { |
| return _mockItems.get(); |
| } |
| |
| - (void)setItems:(NSArray<UIDragItem *> *)items |
| { |
| _mockItems = items; |
| } |
| |
| - (void)addItems:(NSArray<UIDragItem *> *)items |
| { |
| if (![items count]) |
| return; |
| |
| if (![_mockItems count]) |
| _mockItems = items; |
| else |
| _mockItems = [_mockItems arrayByAddingObjectsFromArray:items]; |
| } |
| |
| - (CGPoint)locationInView:(UIView *)view |
| { |
| return [_window convertPoint:_mockLocationInWindow toView:view]; |
| } |
| |
| @end |
| |
| @implementation MockDropSession |
| |
| - (instancetype)initWithProviders:(NSArray<NSItemProvider *> *)providers location:(CGPoint)locationInWindow window:(UIWindow *)window allowMove:(BOOL)allowMove |
| { |
| auto items = adoptNS([[NSMutableArray alloc] init]); |
| for (NSItemProvider *itemProvider in providers) |
| [items addObject:adoptNS([[UIDragItem alloc] initWithItemProvider:itemProvider]).get()]; |
| |
| return [super initWithItems:items.get() location:locationInWindow window:window allowMove:allowMove]; |
| } |
| |
| - (BOOL)isLocal |
| { |
| return YES; |
| } |
| |
| - (NSProgress *)progress |
| { |
| return [NSProgress discreteProgressWithTotalUnitCount:100]; |
| } |
| |
| - (void)setProgressIndicatorStyle:(UIDropSessionProgressIndicatorStyle)progressIndicatorStyle |
| { |
| } |
| |
| - (UIDropSessionProgressIndicatorStyle)progressIndicatorStyle |
| { |
| return UIDropSessionProgressIndicatorStyleNone; |
| } |
| |
| - (NSUInteger)operationMask |
| { |
| return 0; |
| } |
| |
| - (id <UIDragSession>)localDragSession |
| { |
| return nil; |
| } |
| |
| - (BOOL)hasItemsConformingToTypeIdentifier:(NSString *)typeIdentifier |
| { |
| ASSERT_NOT_REACHED(); |
| return NO; |
| } |
| |
| - (BOOL)canCreateItemsOfClass:(Class<NSItemProviderReading>)aClass |
| { |
| ASSERT_NOT_REACHED(); |
| return NO; |
| } |
| |
| - (NSProgress *)loadObjectsOfClass:(Class<NSItemProviderReading>)aClass completion:(void(^)(NSArray<__kindof id <NSItemProviderReading>> *objects))completion |
| { |
| ASSERT_NOT_REACHED(); |
| return nil; |
| } |
| |
| @end |
| |
| @implementation MockDragSession { |
| RetainPtr<id> _localContext; |
| } |
| |
| - (instancetype)initWithWindow:(UIWindow *)window allowMove:(BOOL)allowMove |
| { |
| return [super initWithItems:@[ ] location:CGPointZero window:window allowMove:allowMove]; |
| } |
| |
| - (NSUInteger)localOperationMask |
| { |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| - (NSUInteger)externalOperationMask |
| { |
| ASSERT_NOT_REACHED(); |
| return 0; |
| } |
| |
| - (id)session |
| { |
| return nil; |
| } |
| |
| - (id)localContext |
| { |
| return _localContext.get(); |
| } |
| |
| - (void)setLocalContext:(id)localContext |
| { |
| _localContext = localContext; |
| } |
| |
| @end |
| |
| static double progressIncrementStep = 0.033; |
| static double progressTimeStep = 0.016; |
| static NSString *TestWebKitAPISimulateCancelAllTouchesNotificationName = @"TestWebKitAPISimulateCancelAllTouchesNotificationName"; |
| |
| static NSArray *dragAndDropEventNames() |
| { |
| static NSArray *eventNames = nil; |
| static dispatch_once_t onceToken; |
| dispatch_once(&onceToken, ^() { |
| eventNames = @[ @"dragenter", @"dragover", @"drop", @"dragleave", @"dragstart" ]; |
| }); |
| return eventNames; |
| } |
| |
| @interface DragAndDropSimulatorApplication : UIApplication |
| @end |
| |
| @implementation DragAndDropSimulatorApplication |
| |
| IGNORE_WARNINGS_BEGIN("deprecated-implementations") |
| - (void)_cancelAllTouches |
| { |
| [[NSNotificationCenter defaultCenter] postNotificationName:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil]; |
| } |
| IGNORE_WARNINGS_END |
| |
| @end |
| |
| @interface DragAndDropSimulator () <UIDragAnimating> |
| @end |
| |
| @implementation DragAndDropSimulator { |
| RetainPtr<TestWKWebView> _webView; |
| RetainPtr<MockDragSession> _dragSession; |
| RetainPtr<MockDropSession> _dropSession; |
| RetainPtr<NSMutableArray> _observedEventNames; |
| RetainPtr<NSArray> _externalItemProviders; |
| RetainPtr<NSArray> _sourceItemProviders; |
| CGRect _finalSelectionStartRect; |
| CGPoint _startLocation; |
| CGPoint _endLocation; |
| CGRect _lastKnownDragCaretRect; |
| |
| RetainPtr<NSMutableDictionary<NSNumber *, NSValue *>>_remainingAdditionalItemRequestLocationsByProgress; |
| RetainPtr<NSMutableArray<NSValue *>>_queuedAdditionalItemRequestLocations; |
| RetainPtr<NSMutableArray> _liftPreviews; |
| RetainPtr<NSMutableArray<UITargetedDragPreview *>> _cancellationPreviews; |
| RetainPtr<NSMutableArray> _dropPreviews; |
| RetainPtr<NSMutableArray> _delayedDropPreviews; |
| RetainPtr<NSMutableArray> _defaultDropPreviewsForExternalItems; |
| |
| RetainPtr<NSMutableArray<_WKAttachment *>> _insertedAttachments; |
| RetainPtr<NSMutableArray<_WKAttachment *>> _removedAttachments; |
| |
| bool _hasStartedInputSession; |
| double _currentProgress; |
| bool _isDoneWithCurrentRun; |
| bool _isDoneWaitingForDelayedDropPreviews; |
| DragAndDropPhase _phase; |
| |
| RetainPtr<UIDropProposal> _lastKnownDropProposal; |
| |
| BlockPtr<BOOL(_WKActivatedElementInfo *)> _showCustomActionSheetBlock; |
| BlockPtr<NSArray *(NSItemProvider *, NSArray *, NSDictionary *)> _convertItemProvidersBlock; |
| BlockPtr<NSArray *(id <UIDropSession>)> _overridePerformDropBlock; |
| BlockPtr<UIDropOperation(UIDropOperation, id)> _overrideDragUpdateBlock; |
| BlockPtr<void(BOOL, NSArray *)> _dropCompletionBlock; |
| BlockPtr<void()> _sessionWillBeginBlock; |
| Vector<BlockPtr<void(UIViewAnimatingPosition)>> _dropAnimationCompletionBlocks; |
| } |
| |
| - (instancetype)initWithWebViewFrame:(CGRect)frame |
| { |
| return [self initWithWebViewFrame:frame configuration:nil]; |
| } |
| |
| - (instancetype)initWithWebViewFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration |
| { |
| if (configuration) |
| return [self initWithWebView:adoptNS([[TestWKWebView alloc] initWithFrame:frame configuration:configuration]).get()]; |
| |
| return [self initWithWebView:adoptNS([[TestWKWebView alloc] initWithFrame:frame]).get()]; |
| } |
| |
| - (instancetype)initWithWebView:(TestWKWebView *)webView |
| { |
| if (self = [super init]) { |
| _webView = webView; |
| _shouldEnsureUIApplication = NO; |
| _shouldBecomeFirstResponder = YES; |
| _shouldAllowMoveOperation = YES; |
| _dropAnimationTiming = DropAnimationShouldFinishAfterHandlingDrop; |
| [_webView setUIDelegate:self]; |
| [_webView _setInputDelegate:self]; |
| self.dragDestinationAction = WKDragDestinationActionAny & ~WKDragDestinationActionLoad; |
| } |
| return self; |
| } |
| |
| - (void)dealloc |
| { |
| if ([_webView UIDelegate] == self) |
| [_webView setUIDelegate:nil]; |
| |
| if ([_webView _inputDelegate] == self) |
| [_webView _setInputDelegate:nil]; |
| |
| [super dealloc]; |
| } |
| |
| - (void)_resetSimulatedState |
| { |
| _phase = DragAndDropPhaseBeginning; |
| _currentProgress = 0; |
| _isDoneWithCurrentRun = false; |
| _isDoneWaitingForDelayedDropPreviews = true; |
| _observedEventNames = adoptNS([[NSMutableArray alloc] init]); |
| _insertedAttachments = adoptNS([[NSMutableArray alloc] init]); |
| _removedAttachments = adoptNS([[NSMutableArray alloc] init]); |
| _finalSelectionStartRect = CGRectNull; |
| _dragSession = nil; |
| _dropSession = nil; |
| _lastKnownDropProposal = nil; |
| _lastKnownDragCaretRect = CGRectZero; |
| _remainingAdditionalItemRequestLocationsByProgress = nil; |
| _queuedAdditionalItemRequestLocations = adoptNS([[NSMutableArray alloc] init]); |
| _liftPreviews = adoptNS([[NSMutableArray alloc] init]); |
| _dropPreviews = adoptNS([[NSMutableArray alloc] init]); |
| _cancellationPreviews = adoptNS([[NSMutableArray alloc] init]); |
| _delayedDropPreviews = adoptNS([[NSMutableArray alloc] init]); |
| _hasStartedInputSession = false; |
| } |
| |
| - (NSArray *)observedEventNames |
| { |
| return _observedEventNames.get(); |
| } |
| |
| - (UIDropProposal *)lastKnownDropProposal |
| { |
| return _lastKnownDropProposal.get(); |
| } |
| |
| - (void)simulateAllTouchesCanceled:(NSNotification *)notification |
| { |
| [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil]; |
| _phase = DragAndDropPhaseCancelled; |
| _currentProgress = 1; |
| _isDoneWithCurrentRun = true; |
| if (_dragSession) |
| [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] session:_dragSession.get() didEndWithOperation:UIDropOperationCancel]; |
| } |
| |
| - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation |
| { |
| [self runFrom:startLocation to:endLocation additionalItemRequestLocations:nil]; |
| } |
| |
| - (void)runFrom:(CGPoint)startLocation to:(CGPoint)endLocation additionalItemRequestLocations:(ProgressToCGPointValueMap)additionalItemRequestLocations |
| { |
| NSNotificationCenter *defaultCenter = [NSNotificationCenter defaultCenter]; |
| [defaultCenter addObserver:self selector:@selector(simulateAllTouchesCanceled:) name:TestWebKitAPISimulateCancelAllTouchesNotificationName object:nil]; |
| |
| if (_shouldEnsureUIApplication) |
| UIApplicationInstantiateSingleton([DragAndDropSimulatorApplication class]); |
| |
| if (_shouldBecomeFirstResponder) |
| [_webView becomeFirstResponder]; |
| |
| [self _resetSimulatedState]; |
| |
| if (additionalItemRequestLocations) |
| _remainingAdditionalItemRequestLocationsByProgress = adoptNS([additionalItemRequestLocations mutableCopy]); |
| |
| for (NSString *eventName in dragAndDropEventNames()) { |
| [_webView performAfterReceivingMessage:eventName action:[strongSelf = retainPtr(self), name = retainPtr(eventName)] { |
| [strongSelf->_observedEventNames addObject:name.get()]; |
| }]; |
| } |
| |
| _startLocation = startLocation; |
| _endLocation = endLocation; |
| |
| if (self.externalItemProviders.count) { |
| _dropSession = adoptNS([[MockDropSession alloc] initWithProviders:self.externalItemProviders location:_startLocation window:[_webView window] allowMove:self.shouldAllowMoveOperation]); |
| _phase = DragAndDropPhaseBegan; |
| [self _advanceProgress]; |
| } else { |
| _dragSession = adoptNS([[MockDragSession alloc] initWithWindow:[_webView window] allowMove:self.shouldAllowMoveOperation]); |
| [_dragSession setMockLocationInWindow:_startLocation]; |
| [(id <UIDragInteractionDelegate_ForWebKitOnly>)[_webView dragInteractionDelegate] _dragInteraction:[_webView dragInteraction] prepareForSession:_dragSession.get() completion:[strongSelf = retainPtr(self)] { |
| if (strongSelf->_phase == DragAndDropPhaseCancelled) |
| return; |
| |
| strongSelf->_phase = DragAndDropPhaseBeginning; |
| [strongSelf _advanceProgress]; |
| }]; |
| } |
| |
| Util::run(&_isDoneWithCurrentRun); |
| Util::run(&_isDoneWaitingForDelayedDropPreviews); |
| [_webView clearMessageHandlers:dragAndDropEventNames()]; |
| [_webView waitForNextPresentationUpdate]; |
| |
| auto contentView = [_webView textInputContentView]; |
| _finalSelectionStartRect = [contentView caretRectForPosition:contentView.selectedTextRange.start]; |
| |
| [defaultCenter removeObserver:self]; |
| } |
| |
| - (void)_concludeDropAndPerformOperationIfNecessary |
| { |
| _lastKnownDragCaretRect = [_webView _dragCaretRect]; |
| auto operation = [_lastKnownDropProposal operation]; |
| if (operation != UIDropOperationCancel && operation != UIDropOperationForbidden) { |
| NSInteger dropPreviewIndex = 0; |
| __block NSUInteger numberOfPendingPreviews = [_dropSession items].count; |
| _isDoneWaitingForDelayedDropPreviews = !numberOfPendingPreviews; |
| BOOL canUseDefaultDropPreviewsForExternalItems = [_defaultDropPreviewsForExternalItems count] == [_dropSession items].count; |
| for (UIDragItem *item in [_dropSession items]) { |
| RetainPtr<UITargetedDragPreview> defaultPreview; |
| if (canUseDefaultDropPreviewsForExternalItems) |
| defaultPreview = [_defaultDropPreviewsForExternalItems objectAtIndex:dropPreviewIndex]; |
| else { |
| // Just fall back to an arbitrary non-null drag preview if the test didn't specify one. |
| defaultPreview = adoptNS([[UITargetedDragPreview alloc] initWithView:_webView.get()]); |
| } |
| |
| id <UIDropInteractionDelegate_Private> delegate = (id <UIDropInteractionDelegate_Private>)[_webView dropInteractionDelegate]; |
| UIDropInteraction *interaction = [_webView dropInteraction]; |
| [_dropPreviews addObject:[delegate dropInteraction:interaction previewForDroppingItem:item withDefault:defaultPreview.get()] ?: NSNull.null]; |
| [_delayedDropPreviews addObject:NSNull.null]; |
| [delegate _dropInteraction:interaction delayedPreviewProviderForDroppingItem:item previewProvider:^(UITargetedDragPreview *preview) { |
| if (preview) |
| [_delayedDropPreviews setObject:preview atIndexedSubscript:dropPreviewIndex]; |
| |
| if (!--numberOfPendingPreviews) { |
| _isDoneWaitingForDelayedDropPreviews = true; |
| [self _expectNoDropPreviewsWithUnparentedContainerViews]; |
| } |
| }]; |
| ++dropPreviewIndex; |
| } |
| |
| [self _expectNoDropPreviewsWithUnparentedContainerViews]; |
| |
| [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] performDrop:_dropSession.get()]; |
| _phase = DragAndDropPhasePerformingDrop; |
| |
| for (UIDragItem *item in [_dropSession items]) |
| [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] item:item willAnimateDropWithAnimator:self]; |
| |
| if (_dropAnimationTiming == DropAnimationShouldFinishBeforeHandlingDrop) { |
| [_webView evaluateJavaScript:@"" completionHandler:^(id, NSError *) { |
| // We need to at least ensure one round trip to the web process and back, to ensure that the UI process will have received any image placeholders |
| // that were just inserted as a result of performing the drop. However, this is guaranteed to run before the UI process receives the drop completion |
| // message, since item provider loading is asynchronous. |
| [self _invokeDropAnimationCompletionBlocksAndConcludeDrop]; |
| }]; |
| } |
| } else { |
| _isDoneWithCurrentRun = true; |
| _phase = DragAndDropPhaseCancelled; |
| [[_dropSession items] enumerateObjectsUsingBlock:^(UIDragItem *item, NSUInteger index, BOOL *) { |
| UITargetedDragPreview *defaultPreview = nil; |
| if ([_liftPreviews count] && [[_liftPreviews objectAtIndex:index] isEqual:NSNull.null]) |
| defaultPreview = [_liftPreviews objectAtIndex:index]; |
| |
| UITargetedDragPreview *preview = [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] previewForCancellingItem:item withDefault:defaultPreview]; |
| if (preview) |
| [_cancellationPreviews addObject:preview]; |
| }]; |
| [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] concludeDrop:_dropSession.get()]; |
| } |
| |
| [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] sessionDidEnd:_dropSession.get()]; |
| |
| if (_dragSession) { |
| auto delegate = [_webView dragInteractionDelegate]; |
| [delegate dragInteraction:[_webView dragInteraction] session:_dragSession.get() didEndWithOperation:operation]; |
| if ([delegate respondsToSelector:@selector(_clearToken:)]) |
| [(id <UITextInputMultiDocument>)delegate _clearToken:nil]; |
| [_webView becomeFirstResponder]; |
| } |
| } |
| |
| - (void)_enqueuePendingAdditionalItemRequestLocations |
| { |
| NSMutableArray *progressValuesToRemove = [NSMutableArray array]; |
| for (NSNumber *progressValue in _remainingAdditionalItemRequestLocationsByProgress.get()) { |
| double progress = progressValue.doubleValue; |
| if (progress > _currentProgress) |
| continue; |
| [progressValuesToRemove addObject:progressValue]; |
| [_queuedAdditionalItemRequestLocations addObject:[_remainingAdditionalItemRequestLocationsByProgress objectForKey:progressValue]]; |
| } |
| |
| for (NSNumber *progressToRemove in progressValuesToRemove) |
| [_remainingAdditionalItemRequestLocationsByProgress removeObjectForKey:progressToRemove]; |
| } |
| |
| - (BOOL)_sendQueuedAdditionalItemRequest |
| { |
| if (![_queuedAdditionalItemRequestLocations count]) |
| return NO; |
| |
| RetainPtr<NSValue> requestLocationValue = [_queuedAdditionalItemRequestLocations objectAtIndex:0]; |
| [_queuedAdditionalItemRequestLocations removeObjectAtIndex:0]; |
| |
| auto requestLocation = [[_webView window] convertPoint:[requestLocationValue CGPointValue] toView:_webView.get()]; |
| [(id <UIDragInteractionDelegate_ForWebKitOnly>)[_webView dragInteractionDelegate] _dragInteraction:[_webView dragInteraction] itemsForAddingToSession:_dragSession.get() withTouchAtPoint:requestLocation completion:[dragSession = _dragSession, dropSession = _dropSession] (NSArray *items) { |
| [dragSession addItems:items]; |
| [dropSession addItems:items]; |
| }]; |
| return YES; |
| } |
| |
| - (void)_advanceProgress |
| { |
| [self _enqueuePendingAdditionalItemRequestLocations]; |
| if ([self _sendQueuedAdditionalItemRequest]) { |
| [self _scheduleAdvanceProgress]; |
| return; |
| } |
| |
| _lastKnownDragCaretRect = [_webView _dragCaretRect]; |
| _currentProgress += progressIncrementStep; |
| CGPoint locationInWindow = self._currentLocation; |
| [_dragSession setMockLocationInWindow:locationInWindow]; |
| [_dropSession setMockLocationInWindow:locationInWindow]; |
| |
| if (_currentProgress >= 1) { |
| _currentProgress = 1; |
| [self _concludeDropAndPerformOperationIfNecessary]; |
| return; |
| } |
| |
| switch (_phase) { |
| case DragAndDropPhaseBeginning: { |
| NSMutableArray<NSItemProvider *> *itemProviders = [NSMutableArray array]; |
| NSArray *items = [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] itemsForBeginningSession:_dragSession.get()]; |
| if (!items.count) { |
| _phase = DragAndDropPhaseCancelled; |
| _currentProgress = 1; |
| _isDoneWithCurrentRun = true; |
| return; |
| } |
| |
| for (UIDragItem *item in items) { |
| [itemProviders addObject:item.itemProvider]; |
| UITargetedDragPreview *liftPreview = [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] previewForLiftingItem:item session:_dragSession.get()]; |
| EXPECT_TRUE(liftPreview || ![_webView window]); |
| [_liftPreviews addObject:liftPreview ?: NSNull.null]; |
| } |
| |
| _dropSession = adoptNS([[MockDropSession alloc] initWithProviders:itemProviders location:self._currentLocation window:[_webView window] allowMove:self.shouldAllowMoveOperation]); |
| [_dragSession setItems:items]; |
| _sourceItemProviders = itemProviders; |
| if (self.showCustomActionSheetBlock) { |
| // Defer progress until the custom action sheet is dismissed. |
| auto startLocationInView = [[_webView window] convertPoint:_startLocation toView:_webView.get()]; |
| [_webView _simulateLongPressActionAtLocation:startLocationInView]; |
| return; |
| } |
| |
| auto delegate = [_webView dragInteractionDelegate]; |
| if ([delegate respondsToSelector:@selector(_preserveFocusWithToken:destructively:)]) |
| [(id <UITextInputMultiDocument>)delegate _preserveFocusWithToken:nil destructively:NO]; |
| |
| [_webView resignFirstResponder]; |
| |
| [delegate dragInteraction:[_webView dragInteraction] sessionWillBegin:_dragSession.get()]; |
| |
| RetainPtr<WKWebView> retainedWebView = _webView; |
| dispatch_async(dispatch_get_main_queue(), ^() { |
| [retainedWebView resignFirstResponder]; |
| }); |
| |
| _phase = DragAndDropPhaseBegan; |
| break; |
| } |
| case DragAndDropPhaseBegan: |
| [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] sessionDidEnter:_dropSession.get()]; |
| _phase = DragAndDropPhaseEntered; |
| break; |
| case DragAndDropPhaseEntered: { |
| _lastKnownDropProposal = [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] sessionDidUpdate:_dropSession.get()]; |
| [_webView waitForNextPresentationUpdate]; |
| if (![self shouldAllowMoveOperation] && [_lastKnownDropProposal operation] == UIDropOperationMove) |
| _lastKnownDropProposal = adoptNS([[UIDropProposal alloc] initWithDropOperation:UIDropOperationCancel]); |
| break; |
| } |
| default: |
| break; |
| } |
| |
| [self _scheduleAdvanceProgress]; |
| } |
| |
| - (void)clearExternalDragInformation |
| { |
| _externalItemProviders = nil; |
| _defaultDropPreviewsForExternalItems = nil; |
| } |
| |
| - (CGPoint)_currentLocation |
| { |
| CGFloat distanceX = _endLocation.x - _startLocation.x; |
| CGFloat distanceY = _endLocation.y - _startLocation.y; |
| return CGPointMake(_startLocation.x + _currentProgress * distanceX, _startLocation.y + _currentProgress * distanceY); |
| } |
| |
| - (void)_scheduleAdvanceProgress |
| { |
| [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_advanceProgress) object:nil]; |
| [self performSelector:@selector(_advanceProgress) withObject:nil afterDelay:progressTimeStep]; |
| } |
| |
| - (NSArray *)sourceItemProviders |
| { |
| return _sourceItemProviders.get(); |
| } |
| |
| - (NSArray *)externalItemProviders |
| { |
| return _externalItemProviders.get(); |
| } |
| |
| - (void)setExternalItemProviders:(NSArray *)externalItemProviders |
| { |
| _externalItemProviders = adoptNS([externalItemProviders copy]); |
| } |
| |
| - (void)setExternalItemProviders:(NSArray<NSItemProvider *> *)itemProviders defaultDropPreviews:(NSArray<UITargetedDragPreview *> *)previews |
| { |
| ASSERT(itemProviders.count == previews.count); |
| self.externalItemProviders = itemProviders; |
| _defaultDropPreviewsForExternalItems = adoptNS(previews.copy); |
| } |
| |
| - (DragAndDropPhase)phase |
| { |
| return _phase; |
| } |
| |
| - (NSArray *)liftPreviews |
| { |
| return _liftPreviews.get(); |
| } |
| |
| - (NSArray<UITargetedDragPreview *> *)cancellationPreviews |
| { |
| return _cancellationPreviews.get(); |
| } |
| |
| - (NSArray<UITargetedDragPreview *> *)dropPreviews |
| { |
| return _dropPreviews.get(); |
| } |
| |
| - (NSArray<UITargetedDragPreview *> *)delayedDropPreviews |
| { |
| return _delayedDropPreviews.get(); |
| } |
| |
| - (CGRect)lastKnownDragCaretRect |
| { |
| return _lastKnownDragCaretRect; |
| } |
| |
| - (void)ensureInputSession |
| { |
| Util::run(&_hasStartedInputSession); |
| } |
| |
| - (NSArray<_WKAttachment *> *)insertedAttachments |
| { |
| return _insertedAttachments.get(); |
| } |
| |
| - (NSArray<_WKAttachment *> *)removedAttachments |
| { |
| return _removedAttachments.get(); |
| } |
| |
| - (void)endDataTransfer |
| { |
| [[_webView dragInteractionDelegate] dragInteraction:[_webView dragInteraction] sessionDidTransferItems:_dragSession.get()]; |
| } |
| |
| - (TestWKWebView *)webView |
| { |
| return _webView.get(); |
| } |
| |
| - (void)setShowCustomActionSheetBlock:(BOOL(^)(_WKActivatedElementInfo *))showCustomActionSheetBlock |
| { |
| _showCustomActionSheetBlock = showCustomActionSheetBlock; |
| } |
| |
| - (BOOL(^)(_WKActivatedElementInfo *))showCustomActionSheetBlock |
| { |
| return _showCustomActionSheetBlock.get(); |
| } |
| |
| - (void)setConvertItemProvidersBlock:(NSArray *(^)(NSItemProvider *, NSArray *, NSDictionary *))convertItemProvidersBlock |
| { |
| _convertItemProvidersBlock = convertItemProvidersBlock; |
| } |
| |
| - (NSArray *(^)(NSItemProvider *, NSArray *, NSDictionary *))convertItemProvidersBlock |
| { |
| return _convertItemProvidersBlock.get(); |
| } |
| |
| - (void)setOverridePerformDropBlock:(NSArray *(^)(id <UIDropSession>))overridePerformDropBlock |
| { |
| _overridePerformDropBlock = overridePerformDropBlock; |
| } |
| |
| - (NSArray *(^)(id <UIDropSession>))overridePerformDropBlock |
| { |
| return _overridePerformDropBlock.get(); |
| } |
| |
| - (void)setOverrideDragUpdateBlock:(UIDropOperation(^)(UIDropOperation, id <UIDropSession>))overrideDragUpdateBlock |
| { |
| _overrideDragUpdateBlock = overrideDragUpdateBlock; |
| } |
| |
| - (UIDropOperation(^)(UIDropOperation, id <UIDropSession>))overrideDragUpdateBlock |
| { |
| return _overrideDragUpdateBlock.get(); |
| } |
| |
| - (void)setDropCompletionBlock:(void(^)(BOOL, NSArray *))dropCompletionBlock |
| { |
| _dropCompletionBlock = dropCompletionBlock; |
| } |
| |
| - (void(^)(BOOL, NSArray *))dropCompletionBlock |
| { |
| return _dropCompletionBlock.get(); |
| } |
| |
| - (void)setSessionWillBeginBlock:(dispatch_block_t)block |
| { |
| _sessionWillBeginBlock = block; |
| } |
| |
| - (dispatch_block_t)sessionWillBeginBlock |
| { |
| return _sessionWillBeginBlock.get(); |
| } |
| |
| - (void)addAnimations:(void (^)())animations |
| { |
| // This is not implemented by the drag-and-drop simulator yet, since WebKit doesn't make use of |
| // "alongside" animations during drop. |
| ASSERT_NOT_REACHED(); |
| } |
| |
| - (void)addCompletion:(void (^)(UIViewAnimatingPosition))completion |
| { |
| _dropAnimationCompletionBlocks.append(makeBlockPtr(completion)); |
| } |
| |
| - (void)_expectNoDropPreviewsWithUnparentedContainerViews |
| { |
| auto checkDropPreview = [&](id dropPreviewOrNull) { |
| if (![dropPreviewOrNull isKindOfClass:UITargetedPreview.class]) |
| return; |
| |
| auto *previewContainer = [(UITargetedDragPreview *)dropPreviewOrNull target].container; |
| if (!previewContainer) |
| return; |
| |
| if ([previewContainer isKindOfClass:UIWindow.class]) |
| return; |
| |
| EXPECT_NOT_NULL(previewContainer.window); |
| }; |
| |
| for (id dropPreviewOrNull in _dropPreviews.get()) |
| checkDropPreview(dropPreviewOrNull); |
| |
| for (id dropPreviewOrNull in _delayedDropPreviews.get()) |
| checkDropPreview(dropPreviewOrNull); |
| } |
| |
| - (void)_invokeDropAnimationCompletionBlocksAndConcludeDrop |
| { |
| [self _expectNoDropPreviewsWithUnparentedContainerViews]; |
| for (auto block : std::exchange(_dropAnimationCompletionBlocks, { })) |
| block(UIViewAnimatingPositionEnd); |
| [[_webView dropInteractionDelegate] dropInteraction:[_webView dropInteraction] concludeDrop:_dropSession.get()]; |
| } |
| |
| - (BOOL)containsDraggedType:(NSString *)expectedType |
| { |
| for (NSItemProvider *itemProvider in self.sourceItemProviders) { |
| for (NSString *type in itemProvider.registeredTypeIdentifiers) { |
| if ([type isEqualToString:expectedType]) |
| return YES; |
| } |
| } |
| return NO; |
| } |
| |
| #pragma mark - WKUIDelegatePrivate |
| |
| - (void)_webView:(WKWebView *)webView dataInteraction:(UIDragInteraction *)interaction sessionWillBegin:(id <UIDragSession>)session |
| { |
| if (_sessionWillBeginBlock) |
| _sessionWillBeginBlock(); |
| } |
| |
| - (void)_webView:(WKWebView *)webView dataInteractionOperationWasHandled:(BOOL)handled forSession:(id)session itemProviders:(NSArray<NSItemProvider *> *)itemProviders |
| { |
| if (self.dropCompletionBlock) |
| self.dropCompletionBlock(handled, itemProviders); |
| |
| if (_dropAnimationTiming == DropAnimationShouldFinishBeforeHandlingDrop) { |
| _isDoneWithCurrentRun = true; |
| return; |
| } |
| |
| [_webView _doAfterReceivingEditDragSnapshotForTesting:^{ |
| [self _invokeDropAnimationCompletionBlocksAndConcludeDrop]; |
| _isDoneWithCurrentRun = true; |
| }]; |
| } |
| |
| - (UIDropProposal *)_webView:(WKWebView *)webView willUpdateDropProposalToProposal:(UIDropProposal *)proposal forSession:(id <UIDropSession>)session |
| { |
| if (!self.overrideDragUpdateBlock) |
| return proposal; |
| |
| return adoptNS([[UIDropProposal alloc] initWithDropOperation:self.overrideDragUpdateBlock(proposal.operation, session)]).autorelease(); |
| } |
| |
| - (NSArray *)_webView:(WKWebView *)webView adjustedDataInteractionItemProvidersForItemProvider:(NSItemProvider *)itemProvider representingObjects:(NSArray *)representingObjects additionalData:(NSDictionary *)additionalData |
| { |
| return self.convertItemProvidersBlock ? self.convertItemProvidersBlock(itemProvider, representingObjects, additionalData) : @[ itemProvider ]; |
| } |
| |
| IGNORE_WARNINGS_BEGIN("deprecated-implementations") |
| - (BOOL)_webView:(WKWebView *)webView showCustomSheetForElement:(_WKActivatedElementInfo *)element |
| IGNORE_WARNINGS_END |
| { |
| if (!self.showCustomActionSheetBlock) |
| return NO; |
| |
| dispatch_async(dispatch_get_main_queue(), [strongSelf = retainPtr(self)] { |
| [[strongSelf->_webView dragInteractionDelegate] dragInteraction:[strongSelf->_webView dragInteraction] sessionWillBegin:strongSelf->_dragSession.get()]; |
| strongSelf->_phase = DragAndDropPhaseBegan; |
| [strongSelf _scheduleAdvanceProgress]; |
| }); |
| |
| return self.showCustomActionSheetBlock(element); |
| } |
| |
| - (NSArray<UIDragItem *> *)_webView:(WKWebView *)webView willPerformDropWithSession:(id <UIDropSession>)session |
| { |
| return self.overridePerformDropBlock ? self.overridePerformDropBlock(session) : session.items; |
| } |
| |
| - (void)_webView:(WKWebView *)webView didInsertAttachment:(_WKAttachment *)attachment withSource:(NSString *)source |
| { |
| [_insertedAttachments addObject:attachment]; |
| } |
| |
| - (void)_webView:(WKWebView *)webView didRemoveAttachment:(_WKAttachment *)attachment |
| { |
| [_removedAttachments addObject:attachment]; |
| } |
| |
| - (WKDragDestinationAction)_webView:(WKWebView *)webView dragDestinationActionMaskForDraggingInfo:(id)draggingInfo |
| { |
| return self.dragDestinationAction; |
| } |
| |
| #pragma mark - _WKInputDelegate |
| |
| - (BOOL)_webView:(WKWebView *)webView focusShouldStartInputSession:(id <_WKFocusedElementInfo>)info |
| { |
| return _allowsFocusToStartInputSession; |
| } |
| |
| - (void)_webView:(WKWebView *)webView didStartInputSession:(id <_WKFormInputSession>)inputSession |
| { |
| _hasStartedInputSession = true; |
| } |
| |
| @end |
| |
| #endif // ENABLE(DRAG_SUPPORT) && PLATFORM(IOS_FAMILY) && !PLATFORM(MACCATALYST) |