/* * Copyright (C) 2012-2020 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 "WKContentViewInteraction.h" #if PLATFORM(IOS_FAMILY) #import "APIUIClient.h" #import "CocoaImage.h" #import "CompletionHandlerCallChecker.h" #import "DocumentEditingContext.h" #import "ImageAnalysisUtilities.h" #import "InputViewUpdateDeferrer.h" #import "InsertTextOptions.h" #import "Logging.h" #import "NativeWebKeyboardEvent.h" #import "NativeWebTouchEvent.h" #import "PageClient.h" #import "RemoteLayerTreeDrawingAreaProxy.h" #import "RemoteLayerTreeViews.h" #import "RemoteScrollingCoordinatorProxy.h" #import "RevealItem.h" #import "SmartMagnificationController.h" #import "TextChecker.h" #import "TextInputSPI.h" #import "TextRecognitionUpdateResult.h" #import "UIKitSPI.h" #import "UserInterfaceIdiom.h" #import "WKActionSheetAssistant.h" #import "WKContextMenuElementInfoInternal.h" #import "WKContextMenuElementInfoPrivate.h" #import "WKDatePickerViewController.h" #import "WKDateTimeInputControl.h" #import "WKError.h" #import "WKFocusedFormControlView.h" #import "WKFormSelectControl.h" #import "WKFrameInfoInternal.h" #import "WKHighlightLongPressGestureRecognizer.h" #import "WKImageAnalysisGestureRecognizer.h" #import "WKImagePreviewViewController.h" #import "WKInspectorNodeSearchGestureRecognizer.h" #import "WKMouseGestureRecognizer.h" #import "WKNSURLExtras.h" #import "WKPreviewActionItemIdentifiers.h" #import "WKPreviewActionItemInternal.h" #import "WKPreviewElementInfoInternal.h" #import "WKQuickboardViewControllerDelegate.h" #import "WKSelectMenuListViewController.h" #import "WKSyntheticFlagsChangedWebEvent.h" #import "WKTextInputListViewController.h" #import "WKTextPlaceholder.h" #import "WKTextSelectionRect.h" #import "WKTimePickerViewController.h" #import "WKUIDelegatePrivate.h" #import "WKWebViewConfiguration.h" #import "WKWebViewConfigurationPrivate.h" #import "WKWebViewIOS.h" #import "WKWebViewPrivate.h" #import "WKWebViewPrivateForTesting.h" #import "WebAutocorrectionContext.h" #import "WebAutocorrectionData.h" #import "WebDataListSuggestionsDropdownIOS.h" #import "WebEvent.h" #import "WebFoundTextRange.h" #import "WebIOSEventFactory.h" #import "WebPageMessages.h" #import "WebPageProxyMessages.h" #import "WebProcessProxy.h" #import "_WKActivatedElementInfoInternal.h" #import "_WKDragActionsInternal.h" #import "_WKElementAction.h" #import "_WKElementActionInternal.h" #import "_WKFocusedElementInfo.h" #import "_WKInputDelegate.h" #import "_WKTextInputContextInternal.h" #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import // FIXME: WebKit should not include WebKitLegacy headers! #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #import #if ENABLE(DRAG_SUPPORT) #import #import #import #import #endif #if HAVE(LOOKUP_GESTURE_RECOGNIZER) #import #endif #if ENABLE(ATTACHMENT_ELEMENT) #import "APIAttachment.h" #endif #if ENABLE(INPUT_TYPE_COLOR) #import "WKFormColorControl.h" #endif #if HAVE(PEPPER_UI_CORE) #import "PepperUICoreSPI.h" #endif #if HAVE(AVKIT) #import #endif #import #import #import #if HAVE(LINK_PREVIEW) && USE(UICONTEXTMENU) static NSString * const webkitShowLinkPreviewsPreferenceKey = @"WebKitShowLinkPreviews"; #endif #if HAVE(UI_POINTER_INTERACTION) static NSString * const pointerRegionIdentifier = @"WKPointerRegion"; static NSString * const editablePointerRegionIdentifier = @"WKEditablePointerRegion"; @interface WKContentView (WKUIPointerInteractionDelegate) @end #endif #if HAVE(PENCILKIT_TEXT_INPUT) @interface WKContentView (WKUIIndirectScribbleInteractionDelegate) @end #endif #if HAVE(PEPPER_UI_CORE) #if HAVE(QUICKBOARD_CONTROLLER) @interface WKContentView (QuickboardControllerSupport) @end #endif // HAVE(QUICKBOARD_CONTROLLER) @interface WKContentView (WatchSupport) @end #endif // HAVE(PEPPER_UI_CORE) static void *WKContentViewKVOTransformContext = &WKContentViewKVOTransformContext; #if ENABLE(IMAGE_ANALYSIS) @interface WKContentView (ImageAnalysis) @end #if USE(QUICK_LOOK) @interface WKContentView (ImageAnalysisPreview) @end #endif // USE(QUICK_LOOK) #endif // ENABLE(IMAGE_ANALYSIS) #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) @interface WKContentView (ImageAnalysisInteraction) @end #endif #if USE(APPLE_INTERNAL_SDK) #import #else #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) static bool shouldEnableAlternativeMouseGestureRecognizers(WKMouseGestureRecognizer *mouseRecognizer, WKMouseGestureRecognizer *alternativeMouseRecognizer) { UNUSED_PARAM(mouseRecognizer); UNUSED_PARAM(alternativeMouseRecognizer); return false; } #endif // HAVE(UIKIT_WITH_MOUSE_SUPPORT) #endif #if ENABLE(IMAGE_ANALYSIS) static bool canAttemptTextRecognitionForNonImageElements(const WebKit::InteractionInformationAtPosition& information, const WebKit::WebPreferences& preferences) { return preferences.textRecognitionInVideosEnabled() && information.isPausedVideo; } #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) @interface AVPlayerViewController (Staging_86237428) - (void)setImageAnalysis:(CocoaImageAnalysis *)analysis; @end #endif // ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) #endif // ENABLE(IMAGE_ANALYSIS) namespace WebKit { using namespace WebCore; using namespace WebKit; WKSelectionDrawingInfo::WKSelectionDrawingInfo() : type(SelectionType::None) { } WKSelectionDrawingInfo::WKSelectionDrawingInfo(const EditorState& editorState) { if (editorState.selectionIsNone) { type = SelectionType::None; return; } if (editorState.isInPlugin) { type = SelectionType::Plugin; return; } type = SelectionType::Range; auto& postLayoutData = editorState.postLayoutData(); caretRect = postLayoutData.caretRectAtEnd; caretColor = postLayoutData.caretColor; selectionGeometries = postLayoutData.selectionGeometries; selectionClipRect = postLayoutData.selectionClipRect; } inline bool operator==(const WKSelectionDrawingInfo& a, const WKSelectionDrawingInfo& b) { if (a.type != b.type) return false; if (a.type == WKSelectionDrawingInfo::SelectionType::Range) { if (a.caretRect != b.caretRect) return false; if (a.caretColor != b.caretColor) return false; if (a.selectionGeometries.size() != b.selectionGeometries.size()) return false; for (unsigned i = 0; i < a.selectionGeometries.size(); ++i) { auto& aGeometry = a.selectionGeometries[i]; auto& bGeometry = b.selectionGeometries[i]; auto behavior = aGeometry.behavior(); if (behavior != bGeometry.behavior()) return false; if (behavior == WebCore::SelectionRenderingBehavior::CoalesceBoundingRects && aGeometry.rect() != bGeometry.rect()) return false; if (behavior == WebCore::SelectionRenderingBehavior::UseIndividualQuads && aGeometry.quad() != bGeometry.quad()) return false; } } if (a.type != WKSelectionDrawingInfo::SelectionType::None && a.selectionClipRect != b.selectionClipRect) return false; return true; } inline bool operator!=(const WKSelectionDrawingInfo& a, const WKSelectionDrawingInfo& b) { return !(a == b); } static TextStream& operator<<(TextStream& stream, WKSelectionDrawingInfo::SelectionType type) { switch (type) { case WKSelectionDrawingInfo::SelectionType::None: stream << "none"; break; case WKSelectionDrawingInfo::SelectionType::Plugin: stream << "plugin"; break; case WKSelectionDrawingInfo::SelectionType::Range: stream << "range"; break; } return stream; } TextStream& operator<<(TextStream& stream, const WKSelectionDrawingInfo& info) { TextStream::GroupScope group(stream); stream.dumpProperty("type", info.type); stream.dumpProperty("caret rect", info.caretRect); stream.dumpProperty("caret color", info.caretColor); stream.dumpProperty("selection geometries", info.selectionGeometries); stream.dumpProperty("selection clip rect", info.selectionClipRect); return stream; } #if ENABLE(IMAGE_ANALYSIS) class ImageAnalysisGestureDeferralToken final : public RefCounted { public: static RefPtr create(WKContentView *view) { return adoptRef(*new ImageAnalysisGestureDeferralToken(view)); } ~ImageAnalysisGestureDeferralToken() { if (auto view = m_view.get()) [view _endImageAnalysisGestureDeferral:m_shouldPreventTextSelection ? WebKit::ShouldPreventGestures::Yes : WebKit::ShouldPreventGestures::No]; } void setShouldPreventTextSelection() { m_shouldPreventTextSelection = true; } private: ImageAnalysisGestureDeferralToken(WKContentView *view) : m_view(view) { } WeakObjCPtr m_view; bool m_shouldPreventTextSelection { false }; }; #endif // ENABLE(IMAGE_ANALYSIS) } // namespace WebKit constexpr float highlightDelay = 0.12; constexpr float tapAndHoldDelay = 0.75; constexpr CGFloat minimumTapHighlightRadius = 2.0; constexpr double fasterTapSignificantZoomThreshold = 0.8; @interface WKTextRange : UITextRange { CGRect _startRect; CGRect _endRect; BOOL _isNone; BOOL _isRange; BOOL _isEditable; NSArray*_selectionRects; NSUInteger _selectedTextLength; } @property (nonatomic) CGRect startRect; @property (nonatomic) CGRect endRect; @property (nonatomic) BOOL isNone; @property (nonatomic) BOOL isRange; @property (nonatomic) BOOL isEditable; @property (nonatomic) NSUInteger selectedTextLength; @property (copy, nonatomic) NSArray *selectionRects; + (WKTextRange *)textRangeWithState:(BOOL)isNone isRange:(BOOL)isRange isEditable:(BOOL)isEditable startRect:(CGRect)startRect endRect:(CGRect)endRect selectionRects:(NSArray *)selectionRects selectedTextLength:(NSUInteger)selectedTextLength; @end @interface WKTextPosition : UITextPosition { CGRect _positionRect; } @property (nonatomic) CGRect positionRect; + (WKTextPosition *)textPositionWithRect:(CGRect)positionRect; @end #if HAVE(UIFINDINTERACTION) @interface WKFoundTextRange : UITextRange @property (nonatomic) NSUInteger location; @property (nonatomic) NSUInteger length; @property (nonatomic, copy) NSString *frameIdentifier; @property (nonatomic) NSUInteger order; + (WKFoundTextRange *)foundTextRangeWithWebFoundTextRange:(WebKit::WebFoundTextRange)range; - (WebKit::WebFoundTextRange)webFoundTextRange; @end @interface WKFoundTextPosition : UITextPosition @property (nonatomic) NSUInteger offset; @property (nonatomic) NSUInteger order; + (WKFoundTextPosition *)textPositionWithOffset:(NSUInteger)offset order:(NSUInteger)order; @end #endif @interface WKAutocorrectionRects : UIWKAutocorrectionRects + (WKAutocorrectionRects *)autocorrectionRectsWithFirstCGRect:(CGRect)firstRect lastCGRect:(CGRect)lastRect; @end @interface WKAutocorrectionContext : UIWKAutocorrectionContext + (WKAutocorrectionContext *)emptyAutocorrectionContext; + (WKAutocorrectionContext *)autocorrectionContextWithWebContext:(const WebKit::WebAutocorrectionContext&)context; @end @interface UITextInteractionAssistant (UITextInteractionAssistant_Internal) // FIXME: this needs to be moved from the internal header to the private. - (id)initWithView:(UIResponder *)view; - (void)selectWord; @end @interface UITextInteractionAssistant (WebKit) @property (nonatomic, readonly) BOOL _wk_hasFloatingCursor; @end @interface UIView (UIViewInternalHack) + (BOOL)_addCompletion:(void(^)(BOOL))completion; @end @protocol UISelectionInteractionAssistant; @interface WKFocusedElementInfo : NSObject <_WKFocusedElementInfo> - (instancetype)initWithFocusedElementInformation:(const WebKit::FocusedElementInformation&)information isUserInitiated:(BOOL)isUserInitiated userObject:(NSObject *)userObject; @end @implementation UITextInteractionAssistant (WebKit) - (BOOL)_wk_hasFloatingCursor { return self.inGesture && !self.interactions.inGesture; } @end @implementation WKFormInputSession { WeakObjCPtr _contentView; RetainPtr _focusedElementInfo; RetainPtr _customInputView; RetainPtr _customInputAccessoryView; RetainPtr> _suggestions; BOOL _accessoryViewShouldNotShow; BOOL _forceSecureTextEntry; BOOL _requiresStrongPasswordAssistance; } - (instancetype)initWithContentView:(WKContentView *)view focusedElementInfo:(WKFocusedElementInfo *)elementInfo requiresStrongPasswordAssistance:(BOOL)requiresStrongPasswordAssistance { if (!(self = [super init])) return nil; _contentView = view; _focusedElementInfo = elementInfo; _requiresStrongPasswordAssistance = requiresStrongPasswordAssistance; return self; } - (id <_WKFocusedElementInfo>)focusedElementInfo { return _focusedElementInfo.get(); } - (NSObject *)userObject { return [_focusedElementInfo userObject]; } - (BOOL)isValid { return !!_contentView; } - (NSString *)accessoryViewCustomButtonTitle { return [[[_contentView formAccessoryView] _autofill] title]; } - (void)setAccessoryViewCustomButtonTitle:(NSString *)title { if (title.length) [[_contentView formAccessoryView] showAutoFillButtonWithTitle:title]; else [[_contentView formAccessoryView] hideAutoFillButton]; if (!WebKit::currentUserInterfaceIdiomIsSmallScreen()) [_contentView reloadInputViews]; } - (BOOL)accessoryViewShouldNotShow { return _accessoryViewShouldNotShow; } - (void)setAccessoryViewShouldNotShow:(BOOL)accessoryViewShouldNotShow { if (_accessoryViewShouldNotShow == accessoryViewShouldNotShow) return; _accessoryViewShouldNotShow = accessoryViewShouldNotShow; [_contentView reloadInputViews]; } - (BOOL)forceSecureTextEntry { return _forceSecureTextEntry; } - (void)setForceSecureTextEntry:(BOOL)forceSecureTextEntry { if (_forceSecureTextEntry == forceSecureTextEntry) return; _forceSecureTextEntry = forceSecureTextEntry; [_contentView reloadInputViews]; } - (UIView *)customInputView { return _customInputView.get(); } - (void)setCustomInputView:(UIView *)customInputView { if (customInputView == _customInputView) return; _customInputView = customInputView; [_contentView reloadInputViews]; } - (UIView *)customInputAccessoryView { return _customInputAccessoryView.get(); } - (void)setCustomInputAccessoryView:(UIView *)customInputAccessoryView { if (_customInputAccessoryView == customInputAccessoryView) return; _customInputAccessoryView = customInputAccessoryView; [_contentView reloadInputViews]; } - (void)endEditing { if ([_customInputView conformsToProtocol:@protocol(WKFormControl)]) [(id)_customInputView.get() controlEndEditing]; } - (NSArray *)suggestions { return _suggestions.get(); } - (void)setSuggestions:(NSArray *)suggestions { if (suggestions == _suggestions || [suggestions isEqualToArray:_suggestions.get()]) return; _suggestions = adoptNS([suggestions copy]); [_contentView updateTextSuggestionsForInputDelegate]; } - (BOOL)requiresStrongPasswordAssistance { return _requiresStrongPasswordAssistance; } - (void)invalidate { id suggestionDelegate = (id )[_contentView inputDelegate]; [suggestionDelegate setSuggestions:nil]; _contentView = nil; } - (void)reloadFocusedElementContextView { [_contentView reloadContextViewForPresentedListViewController]; } @end @implementation WKFocusedElementInfo { WKInputType _type; RetainPtr _value; BOOL _isUserInitiated; RetainPtr> _userObject; RetainPtr _placeholder; RetainPtr _label; } - (instancetype)initWithFocusedElementInformation:(const WebKit::FocusedElementInformation&)information isUserInitiated:(BOOL)isUserInitiated userObject:(NSObject *)userObject { if (!(self = [super init])) return nil; switch (information.elementType) { case WebKit::InputType::ContentEditable: _type = WKInputTypeContentEditable; break; case WebKit::InputType::Text: _type = WKInputTypeText; break; case WebKit::InputType::Password: _type = WKInputTypePassword; break; case WebKit::InputType::TextArea: _type = WKInputTypeTextArea; break; case WebKit::InputType::Search: _type = WKInputTypeSearch; break; case WebKit::InputType::Email: _type = WKInputTypeEmail; break; case WebKit::InputType::URL: _type = WKInputTypeURL; break; case WebKit::InputType::Phone: _type = WKInputTypePhone; break; case WebKit::InputType::Number: _type = WKInputTypeNumber; break; case WebKit::InputType::NumberPad: _type = WKInputTypeNumberPad; break; case WebKit::InputType::Date: _type = WKInputTypeDate; break; case WebKit::InputType::DateTimeLocal: _type = WKInputTypeDateTimeLocal; break; case WebKit::InputType::Month: _type = WKInputTypeMonth; break; case WebKit::InputType::Week: _type = WKInputTypeWeek; break; case WebKit::InputType::Time: _type = WKInputTypeTime; break; case WebKit::InputType::Select: _type = WKInputTypeSelect; break; case WebKit::InputType::Drawing: _type = WKInputTypeDrawing; break; #if ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Color: _type = WKInputTypeColor; break; #endif case WebKit::InputType::None: _type = WKInputTypeNone; break; } _value = information.value; _isUserInitiated = isUserInitiated; _userObject = userObject; _placeholder = information.placeholder; _label = information.label; return self; } - (WKInputType)type { return _type; } - (NSString *)value { return _value.get(); } - (BOOL)isUserInitiated { return _isUserInitiated; } - (NSObject *)userObject { return _userObject.get(); } - (NSString *)label { return _label.get(); } - (NSString *)placeholder { return _placeholder.get(); } @end #if ENABLE(DRAG_SUPPORT) @interface WKDragSessionContext : NSObject - (void)addTemporaryDirectory:(NSString *)temporaryDirectory; - (void)cleanUpTemporaryDirectories; @end @implementation WKDragSessionContext { RetainPtr _temporaryDirectories; } - (void)addTemporaryDirectory:(NSString *)temporaryDirectory { if (!_temporaryDirectories) _temporaryDirectories = adoptNS([NSMutableArray new]); [_temporaryDirectories addObject:temporaryDirectory]; } - (void)cleanUpTemporaryDirectories { for (NSString *directory in _temporaryDirectories.get()) { NSError *error = nil; [[NSFileManager defaultManager] removeItemAtPath:directory error:&error]; RELEASE_LOG(DragAndDrop, "Removed temporary download directory: %@ with error: %@", directory, error); } _temporaryDirectories = nil; } @end static WKDragSessionContext *existingLocalDragSessionContext(id session) { return [session.localContext isKindOfClass:[WKDragSessionContext class]] ? (WKDragSessionContext *)session.localContext : nil; } static WKDragSessionContext *ensureLocalDragSessionContext(id session) { if (WKDragSessionContext *existingContext = existingLocalDragSessionContext(session)) return existingContext; if (session.localContext) { RELEASE_LOG(DragAndDrop, "Overriding existing local context: %@ on session: %@", session.localContext, session); ASSERT_NOT_REACHED(); } session.localContext = adoptNS([[WKDragSessionContext alloc] init]).get(); return (WKDragSessionContext *)session.localContext; } #endif // ENABLE(DRAG_SUPPORT) @implementation UIGestureRecognizer (WKContentViewHelpers) - (void)_wk_cancel { if (!self.enabled) return; [self setEnabled:NO]; [self setEnabled:YES]; } - (BOOL)_wk_isTapAndAHalf { static dispatch_once_t onceToken; static Class tapAndAHalfGestureRecognizerClass; dispatch_once(&onceToken, ^{ tapAndAHalfGestureRecognizerClass = NSClassFromString(@"UITapAndAHalfRecognizer"); }); return [self isKindOfClass:tapAndAHalfGestureRecognizerClass]; } @end @interface WKTargetedPreviewContainer : UIView - (instancetype)initWithContentView:(WKContentView *)contentView NS_DESIGNATED_INITIALIZER; - (instancetype)initWithFrame:(CGRect)frame NS_UNAVAILABLE; - (instancetype)initWithCoder:(NSCoder *)coder NS_UNAVAILABLE; @end @implementation WKTargetedPreviewContainer { __weak WKContentView *_contentView; } - (instancetype)initWithContentView:(WKContentView *)contentView { if (!(self = [super initWithFrame:CGRectZero])) return nil; _contentView = contentView; return self; } - (void)_didRemoveSubview:(UIView *)subview { [super _didRemoveSubview:subview]; if (self.subviews.count) return; #if USE(UICONTEXTMENU) [_contentView _targetedPreviewContainerDidRemoveLastSubview:self]; #endif } @end @interface WKContentView (WKInteractionPrivate) - (void)accessibilitySpeakSelectionSetContent:(NSString *)string; - (NSArray *)webSelectionRectsForSelectionGeometries:(const Vector&)selectionRects; - (void)_accessibilityDidGetSelectionRects:(NSArray *)selectionRects withGranularity:(UITextGranularity)granularity atOffset:(NSInteger)offset; @end @implementation WKContentView (WKInteraction) - (BOOL)preventsPanningInXAxis { return _preventsPanningInXAxis; } - (BOOL)preventsPanningInYAxis { return _preventsPanningInYAxis; } - (WKFormInputSession *)_formInputSession { return _formInputSession.get(); } - (void)_createAndConfigureDoubleTapGestureRecognizer { if (_doubleTapGestureRecognizer) { [self removeGestureRecognizer:_doubleTapGestureRecognizer.get()]; [_doubleTapGestureRecognizer setDelegate:nil]; [_doubleTapGestureRecognizer setGestureFailedTarget:nil action:nil]; } _doubleTapGestureRecognizer = adoptNS([[WKSyntheticTapGestureRecognizer alloc] initWithTarget:self action:@selector(_doubleTapRecognized:)]); [_doubleTapGestureRecognizer setGestureFailedTarget:self action:@selector(_doubleTapDidFail:)]; [_doubleTapGestureRecognizer setNumberOfTapsRequired:2]; [_doubleTapGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_doubleTapGestureRecognizer.get()]; [_singleTapGestureRecognizer requireGestureRecognizerToFail:_doubleTapGestureRecognizer.get()]; } - (void)_createAndConfigureHighlightLongPressGestureRecognizer { _highlightLongPressGestureRecognizer = adoptNS([[WKHighlightLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_highlightLongPressRecognized:)]); [_highlightLongPressGestureRecognizer setDelay:highlightDelay]; [_highlightLongPressGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_highlightLongPressGestureRecognizer.get()]; } - (void)_createAndConfigureLongPressGestureRecognizer { _longPressGestureRecognizer = adoptNS([[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_longPressRecognized:)]); [_longPressGestureRecognizer setDelay:tapAndHoldDelay]; [_longPressGestureRecognizer setDelegate:self]; [_longPressGestureRecognizer _setRequiresQuietImpulse:YES]; [self addGestureRecognizer:_longPressGestureRecognizer.get()]; } - (void)setUpInteraction { // If the page is not valid yet then delay interaction setup until the process is launched/relaunched. if (!_page->hasRunningProcess()) return; if (_hasSetUpInteractions) return; if (!_interactionViewsContainerView) { _interactionViewsContainerView = adoptNS([[UIView alloc] init]); [_interactionViewsContainerView layer].name = @"InteractionViewsContainer"; [_interactionViewsContainerView setOpaque:NO]; [_interactionViewsContainerView layer].anchorPoint = CGPointZero; [self.superview addSubview:_interactionViewsContainerView.get()]; } _keyboardScrollingAnimator = adoptNS([[WKKeyboardScrollViewAnimator alloc] initWithScrollView:self.webView.scrollView]); [_keyboardScrollingAnimator setDelegate:self]; [self.layer addObserver:self forKeyPath:@"transform" options:NSKeyValueObservingOptionInitial context:WKContentViewKVOTransformContext]; _touchActionLeftSwipeGestureRecognizer = adoptNS([[UISwipeGestureRecognizer alloc] initWithTarget:nil action:nil]); [_touchActionLeftSwipeGestureRecognizer setDirection:UISwipeGestureRecognizerDirectionLeft]; [_touchActionLeftSwipeGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_touchActionLeftSwipeGestureRecognizer.get()]; _touchActionRightSwipeGestureRecognizer = adoptNS([[UISwipeGestureRecognizer alloc] initWithTarget:nil action:nil]); [_touchActionRightSwipeGestureRecognizer setDirection:UISwipeGestureRecognizerDirectionRight]; [_touchActionRightSwipeGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_touchActionRightSwipeGestureRecognizer.get()]; _touchActionUpSwipeGestureRecognizer = adoptNS([[UISwipeGestureRecognizer alloc] initWithTarget:nil action:nil]); [_touchActionUpSwipeGestureRecognizer setDirection:UISwipeGestureRecognizerDirectionUp]; [_touchActionUpSwipeGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_touchActionUpSwipeGestureRecognizer.get()]; _touchActionDownSwipeGestureRecognizer = adoptNS([[UISwipeGestureRecognizer alloc] initWithTarget:nil action:nil]); [_touchActionDownSwipeGestureRecognizer setDirection:UISwipeGestureRecognizerDirectionDown]; [_touchActionDownSwipeGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_touchActionDownSwipeGestureRecognizer.get()]; _touchStartDeferringGestureRecognizerForImmediatelyResettableGestures = adoptNS([[WKDeferringGestureRecognizer alloc] initWithDeferringGestureDelegate:self]); [_touchStartDeferringGestureRecognizerForImmediatelyResettableGestures setName:@"Deferrer for touch start (immediate reset)"]; _touchStartDeferringGestureRecognizerForDelayedResettableGestures = adoptNS([[WKDeferringGestureRecognizer alloc] initWithDeferringGestureDelegate:self]); [_touchStartDeferringGestureRecognizerForDelayedResettableGestures setName:@"Deferrer for touch start (delayed reset)"]; _touchStartDeferringGestureRecognizerForSyntheticTapGestures = adoptNS([[WKDeferringGestureRecognizer alloc] initWithDeferringGestureDelegate:self]); [_touchStartDeferringGestureRecognizerForSyntheticTapGestures setName:@"Deferrer for touch start (synthetic tap)"]; _touchEndDeferringGestureRecognizerForImmediatelyResettableGestures = adoptNS([[WKDeferringGestureRecognizer alloc] initWithDeferringGestureDelegate:self]); [_touchEndDeferringGestureRecognizerForImmediatelyResettableGestures setName:@"Deferrer for touch end (immediate reset)"]; _touchEndDeferringGestureRecognizerForDelayedResettableGestures = adoptNS([[WKDeferringGestureRecognizer alloc] initWithDeferringGestureDelegate:self]); [_touchEndDeferringGestureRecognizerForDelayedResettableGestures setName:@"Deferrer for touch end (delayed reset)"]; _touchEndDeferringGestureRecognizerForSyntheticTapGestures = adoptNS([[WKDeferringGestureRecognizer alloc] initWithDeferringGestureDelegate:self]); [_touchEndDeferringGestureRecognizerForSyntheticTapGestures setName:@"Deferrer for touch end (synthetic tap)"]; #if ENABLE(IMAGE_ANALYSIS) _imageAnalysisDeferringGestureRecognizer = adoptNS([[WKDeferringGestureRecognizer alloc] initWithDeferringGestureDelegate:self]); [_imageAnalysisDeferringGestureRecognizer setName:@"Deferrer for image analysis"]; [_imageAnalysisDeferringGestureRecognizer setImmediatelyFailsAfterTouchEnd:YES]; [_imageAnalysisDeferringGestureRecognizer setEnabled:WebKit::isLiveTextAvailableAndEnabled()]; #endif for (WKDeferringGestureRecognizer *gesture in self.deferringGestures) { gesture.delegate = self; [self addGestureRecognizer:gesture]; } if (_gestureRecognizerConsistencyEnforcer) _gestureRecognizerConsistencyEnforcer->reset(); _touchEventGestureRecognizer = adoptNS([[UIWebTouchEventsGestureRecognizer alloc] initWithTarget:self action:@selector(_webTouchEventsRecognized:) touchDelegate:self]); [_touchEventGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_touchEventGestureRecognizer.get()]; #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) [self setUpMouseGestureRecognizer]; #endif #if HAVE(LOOKUP_GESTURE_RECOGNIZER) _lookupGestureRecognizer = adoptNS([[_UILookupGestureRecognizer alloc] initWithTarget:self action:@selector(_lookupGestureRecognized:)]); [_lookupGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_lookupGestureRecognizer.get()]; #endif _singleTapGestureRecognizer = adoptNS([[WKSyntheticTapGestureRecognizer alloc] initWithTarget:self action:@selector(_singleTapRecognized:)]); [_singleTapGestureRecognizer setDelegate:self]; [_singleTapGestureRecognizer setGestureIdentifiedTarget:self action:@selector(_singleTapIdentified:)]; [_singleTapGestureRecognizer setResetTarget:self action:@selector(_singleTapDidReset:)]; [_singleTapGestureRecognizer setSupportingWebTouchEventsGestureRecognizer:_touchEventGestureRecognizer.get()]; [self addGestureRecognizer:_singleTapGestureRecognizer.get()]; _nonBlockingDoubleTapGestureRecognizer = adoptNS([[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_nonBlockingDoubleTapRecognized:)]); [_nonBlockingDoubleTapGestureRecognizer setNumberOfTapsRequired:2]; [_nonBlockingDoubleTapGestureRecognizer setDelegate:self]; [_nonBlockingDoubleTapGestureRecognizer setEnabled:NO]; [self addGestureRecognizer:_nonBlockingDoubleTapGestureRecognizer.get()]; _doubleTapGestureRecognizerForDoubleClick = adoptNS([[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_doubleTapRecognizedForDoubleClick:)]); [_doubleTapGestureRecognizerForDoubleClick setNumberOfTapsRequired:2]; [_doubleTapGestureRecognizerForDoubleClick setDelegate:self]; [self addGestureRecognizer:_doubleTapGestureRecognizerForDoubleClick.get()]; [self _createAndConfigureDoubleTapGestureRecognizer]; _twoFingerDoubleTapGestureRecognizer = adoptNS([[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_twoFingerDoubleTapRecognized:)]); [_twoFingerDoubleTapGestureRecognizer setNumberOfTapsRequired:2]; [_twoFingerDoubleTapGestureRecognizer setNumberOfTouchesRequired:2]; [_twoFingerDoubleTapGestureRecognizer setDelegate:self]; [self addGestureRecognizer:_twoFingerDoubleTapGestureRecognizer.get()]; [self _createAndConfigureHighlightLongPressGestureRecognizer]; [self _createAndConfigureLongPressGestureRecognizer]; #if HAVE(LINK_PREVIEW) [self _updateLongPressAndHighlightLongPressGestures]; #endif #if ENABLE(DRAG_SUPPORT) [self setUpDragAndDropInteractions]; #endif #if HAVE(UI_POINTER_INTERACTION) [self setUpPointerInteraction]; #endif #if HAVE(PENCILKIT_TEXT_INPUT) [self setUpScribbleInteraction]; #endif _twoFingerSingleTapGestureRecognizer = adoptNS([[UITapGestureRecognizer alloc] initWithTarget:self action:@selector(_twoFingerSingleTapGestureRecognized:)]); [_twoFingerSingleTapGestureRecognizer setAllowableMovement:60]; [_twoFingerSingleTapGestureRecognizer _setAllowableSeparation:150]; [_twoFingerSingleTapGestureRecognizer setNumberOfTapsRequired:1]; [_twoFingerSingleTapGestureRecognizer setNumberOfTouchesRequired:2]; [_twoFingerSingleTapGestureRecognizer setDelaysTouchesEnded:NO]; [_twoFingerSingleTapGestureRecognizer setDelegate:self]; [_twoFingerSingleTapGestureRecognizer setEnabled:!self.webView._editable]; [self addGestureRecognizer:_twoFingerSingleTapGestureRecognizer.get()]; _touchActionGestureRecognizer = adoptNS([[WKTouchActionGestureRecognizer alloc] initWithTouchActionDelegate:self]); [self addGestureRecognizer:_touchActionGestureRecognizer.get()]; #if HAVE(LINK_PREVIEW) [self _registerPreview]; #endif NSNotificationCenter *center = [NSNotificationCenter defaultCenter]; ALLOW_DEPRECATED_DECLARATIONS_BEGIN [center addObserver:self selector:@selector(_willHideMenu:) name:UIMenuControllerWillHideMenuNotification object:nil]; [center addObserver:self selector:@selector(_didHideMenu:) name:UIMenuControllerDidHideMenuNotification object:nil]; ALLOW_DEPRECATED_DECLARATIONS_END [center addObserver:self selector:@selector(_keyboardDidRequestDismissal:) name:UIKeyboardPrivateDidRequestDismissalNotification object:nil]; _showingTextStyleOptions = NO; // FIXME: This should be called when we get notified that loading has completed. [self setUpTextSelectionAssistant]; _actionSheetAssistant = adoptNS([[WKActionSheetAssistant alloc] initWithView:self]); [_actionSheetAssistant setDelegate:self]; _smartMagnificationController = makeUnique(self); _touchEventsCanPreventNativeGestures = YES; _isExpectingFastSingleTapCommit = NO; _potentialTapInProgress = NO; _isDoubleTapPending = NO; _showDebugTapHighlightsForFastClicking = [[NSUserDefaults standardUserDefaults] boolForKey:@"WebKitShowFastClickDebugTapHighlights"]; _needsDeferredEndScrollingSelectionUpdate = NO; _isChangingFocus = NO; _isBlurringFocusedElement = NO; #if USE(UICONTEXTMENU) _isDisplayingContextMenuWithAnimation = NO; #endif #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS) _contextMenuWasTriggeredByImageAnalysisTimeout = NO; #endif #if ENABLE(DATALIST_ELEMENT) _dataListTextSuggestionsInputView = nil; _dataListTextSuggestions = nil; #endif #if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING) _textCheckingController = makeUnique(*_page); #endif _autocorrectionContextNeedsUpdate = YES; _page->process().updateTextCheckerState(); _page->setScreenIsBeingCaptured([[[self window] screen] isCaptured]); #if ENABLE(IMAGE_ANALYSIS) [self _setUpImageAnalysis]; #endif _hasSetUpInteractions = YES; } - (void)cleanUpInteraction { if (!_hasSetUpInteractions) return; _textInteractionAssistant = nil; [_actionSheetAssistant cleanupSheet]; _actionSheetAssistant = nil; _smartMagnificationController = nil; _didAccessoryTabInitiateFocus = NO; _isChangingFocusUsingAccessoryTab = NO; _isExpectingFastSingleTapCommit = NO; _needsDeferredEndScrollingSelectionUpdate = NO; [_formInputSession invalidate]; _formInputSession = nil; [_highlightView removeFromSuperview]; _lastOutstandingPositionInformationRequest = std::nullopt; _isWaitingOnPositionInformation = NO; _focusRequiresStrongPasswordAssistance = NO; _additionalContextForStrongPasswordAssistance = nil; _waitingForEditDragSnapshot = NO; _autocorrectionContextNeedsUpdate = YES; _lastAutocorrectionContext = { }; _candidateViewNeedsUpdate = NO; _seenHardwareKeyDownInNonEditableElement = NO; _textInteractionDidChangeFocusedElement = NO; _activeTextInteractionCount = 0; _treatAsContentEditableUntilNextEditorStateUpdate = NO; #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS) _contextMenuWasTriggeredByImageAnalysisTimeout = NO; #endif if (_interactionViewsContainerView) { [self.layer removeObserver:self forKeyPath:@"transform" context:WKContentViewKVOTransformContext]; [_interactionViewsContainerView removeFromSuperview]; _interactionViewsContainerView = nil; } _waitingForKeyboardToStartAnimatingInAfterElementFocus = NO; _lastInsertedCharacterToOverrideCharacterBeforeSelection = std::nullopt; [_touchEventGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_touchEventGestureRecognizer.get()]; for (WKDeferringGestureRecognizer *gesture in self.deferringGestures) { gesture.delegate = nil; [self removeGestureRecognizer:gesture]; } if (_gestureRecognizerConsistencyEnforcer) _gestureRecognizerConsistencyEnforcer->reset(); #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) [_mouseGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_mouseGestureRecognizer.get()]; [_alternateMouseGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_alternateMouseGestureRecognizer.get()]; #endif #if HAVE(LOOKUP_GESTURE_RECOGNIZER) [_lookupGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_lookupGestureRecognizer.get()]; #endif [_singleTapGestureRecognizer setDelegate:nil]; [_singleTapGestureRecognizer setGestureIdentifiedTarget:nil action:nil]; [_singleTapGestureRecognizer setResetTarget:nil action:nil]; [_singleTapGestureRecognizer setSupportingWebTouchEventsGestureRecognizer:nil]; [self removeGestureRecognizer:_singleTapGestureRecognizer.get()]; [_highlightLongPressGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_highlightLongPressGestureRecognizer.get()]; [_longPressGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_longPressGestureRecognizer.get()]; [_doubleTapGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_doubleTapGestureRecognizer.get()]; [_nonBlockingDoubleTapGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_nonBlockingDoubleTapGestureRecognizer.get()]; [_doubleTapGestureRecognizerForDoubleClick setDelegate:nil]; [self removeGestureRecognizer:_doubleTapGestureRecognizerForDoubleClick.get()]; [_twoFingerDoubleTapGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_twoFingerDoubleTapGestureRecognizer.get()]; [_twoFingerSingleTapGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_twoFingerSingleTapGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionLeftSwipeGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionRightSwipeGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionUpSwipeGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionDownSwipeGestureRecognizer.get()]; _layerTreeTransactionIdAtLastInteractionStart = { }; #if USE(UICONTEXTMENU) [self _removeContextMenuHintContainerIfPossible]; #endif // USE(UICONTEXTMENU) #if ENABLE(DRAG_SUPPORT) [existingLocalDragSessionContext(_dragDropInteractionState.dragSession()) cleanUpTemporaryDirectories]; [self teardownDragAndDropInteractions]; #endif #if HAVE(UI_POINTER_INTERACTION) [self removeInteraction:_pointerInteraction.get()]; _pointerInteraction = nil; #endif [self resetShouldZoomToFocusRectAfterShowingKeyboard]; #if HAVE(PENCILKIT_TEXT_INPUT) [self cleanUpScribbleInteraction]; #endif _inspectorNodeSearchEnabled = NO; if (_inspectorNodeSearchGestureRecognizer) { [_inspectorNodeSearchGestureRecognizer setDelegate:nil]; [self removeGestureRecognizer:_inspectorNodeSearchGestureRecognizer.get()]; _inspectorNodeSearchGestureRecognizer = nil; } #if HAVE(LINK_PREVIEW) [self _unregisterPreview]; #endif if (_fileUploadPanel) { [_fileUploadPanel setDelegate:nil]; [_fileUploadPanel dismiss]; _fileUploadPanel = nil; } #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) if (_shareSheet) { [_shareSheet setDelegate:nil]; [_shareSheet dismiss]; _shareSheet = nil; } #endif [self _resetInputViewDeferral]; _focusedElementInformation = { }; [_keyboardScrollingAnimator invalidate]; _keyboardScrollingAnimator = nil; #if ENABLE(DATALIST_ELEMENT) _dataListTextSuggestionsInputView = nil; _dataListTextSuggestions = nil; #endif #if ENABLE(IMAGE_ANALYSIS) [self _tearDownImageAnalysis]; #endif [self _removeContainerForContextMenuHintPreviews]; [self _removeContainerForDragPreviews]; [self _removeContainerForDropPreviews]; [self unsuppressSoftwareKeyboardUsingLastAutocorrectionContextIfNeeded]; _hasSetUpInteractions = NO; _suppressSelectionAssistantReasons = { }; [self _resetPanningPreventionFlags]; [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture]; [self _cancelPendingKeyEventHandler]; _cachedSelectedTextRange = nil; } - (void)cleanUpInteractionPreviewContainers { [self _removeContainerForContextMenuHintPreviews]; } - (void)_cancelPendingKeyEventHandler { if (!_page) return; ASSERT_IMPLIES(_keyWebEventHandler, _page->hasQueuedKeyEvent()); if (!_page->hasQueuedKeyEvent()) return; if (auto keyEventHandler = std::exchange(_keyWebEventHandler, nil)) keyEventHandler(_page->firstQueuedKeyEvent().nativeEvent(), NO); } - (void)_removeDefaultGestureRecognizers { for (WKDeferringGestureRecognizer *gesture in self.deferringGestures) [self removeGestureRecognizer:gesture]; [self removeGestureRecognizer:_touchEventGestureRecognizer.get()]; [self removeGestureRecognizer:_singleTapGestureRecognizer.get()]; [self removeGestureRecognizer:_highlightLongPressGestureRecognizer.get()]; [self removeGestureRecognizer:_doubleTapGestureRecognizer.get()]; [self removeGestureRecognizer:_nonBlockingDoubleTapGestureRecognizer.get()]; [self removeGestureRecognizer:_doubleTapGestureRecognizerForDoubleClick.get()]; [self removeGestureRecognizer:_twoFingerDoubleTapGestureRecognizer.get()]; [self removeGestureRecognizer:_twoFingerSingleTapGestureRecognizer.get()]; #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) [self removeGestureRecognizer:_mouseGestureRecognizer.get()]; [self removeGestureRecognizer:_alternateMouseGestureRecognizer.get()]; #endif #if HAVE(LOOKUP_GESTURE_RECOGNIZER) [self removeGestureRecognizer:_lookupGestureRecognizer.get()]; #endif [self removeGestureRecognizer:_touchActionGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionLeftSwipeGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionRightSwipeGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionUpSwipeGestureRecognizer.get()]; [self removeGestureRecognizer:_touchActionDownSwipeGestureRecognizer.get()]; } - (void)_addDefaultGestureRecognizers { for (WKDeferringGestureRecognizer *gesture in self.deferringGestures) [self addGestureRecognizer:gesture]; [self addGestureRecognizer:_touchEventGestureRecognizer.get()]; [self addGestureRecognizer:_singleTapGestureRecognizer.get()]; [self addGestureRecognizer:_highlightLongPressGestureRecognizer.get()]; [self addGestureRecognizer:_doubleTapGestureRecognizer.get()]; [self addGestureRecognizer:_nonBlockingDoubleTapGestureRecognizer.get()]; [self addGestureRecognizer:_doubleTapGestureRecognizerForDoubleClick.get()]; [self addGestureRecognizer:_twoFingerDoubleTapGestureRecognizer.get()]; [self addGestureRecognizer:_twoFingerSingleTapGestureRecognizer.get()]; #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) [self addGestureRecognizer:_mouseGestureRecognizer.get()]; [self addGestureRecognizer:_alternateMouseGestureRecognizer.get()]; #endif #if HAVE(LOOKUP_GESTURE_RECOGNIZER) [self addGestureRecognizer:_lookupGestureRecognizer.get()]; #endif [self addGestureRecognizer:_touchActionGestureRecognizer.get()]; [self addGestureRecognizer:_touchActionLeftSwipeGestureRecognizer.get()]; [self addGestureRecognizer:_touchActionRightSwipeGestureRecognizer.get()]; [self addGestureRecognizer:_touchActionUpSwipeGestureRecognizer.get()]; [self addGestureRecognizer:_touchActionDownSwipeGestureRecognizer.get()]; } - (void)_didChangeLinkPreviewAvailability { [self _updateLongPressAndHighlightLongPressGestures]; } - (void)_updateLongPressAndHighlightLongPressGestures { // We only disable the highlight long press gesture in the case where UIContextMenu is available and we // also allow link previews, since the context menu interaction's gestures need to take precedence over // highlight long press gestures. [_highlightLongPressGestureRecognizer setEnabled:!self._shouldUseContextMenus || !self.webView.allowsLinkPreview]; // We only enable the long press gesture in the case where the app is linked on iOS 12 or earlier (and // therefore prefers the legacy action sheet over context menus), and link previews are also enabled. [_longPressGestureRecognizer setEnabled:!self._shouldUseContextMenus && self.webView.allowsLinkPreview]; } - (UIView *)unscaledView { return _interactionViewsContainerView.get(); } - (CGFloat)inverseScale { return 1 / [[self layer] transform].m11; } - (UIScrollView *)_scroller { return [_webView scrollView]; } - (CGRect)unobscuredContentRect { return _page->unobscuredContentRect(); } #pragma mark - UITextAutoscrolling - (void)startAutoscroll:(CGPoint)pointInDocument { _page->startAutoscrollAtPosition(pointInDocument); } - (void)cancelAutoscroll { _page->cancelAutoscroll(); } - (void)scrollSelectionToVisible:(BOOL)animated { // Used to scroll selection on keyboard up; we already scroll to visible. } - (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context { if (context != WKContentViewKVOTransformContext) { [super observeValueForKeyPath:keyPath ofObject:object change:change context:context]; return; } ASSERT([keyPath isEqualToString:@"transform"]); ASSERT(object == self.layer); if ([UIView _isInAnimationBlock] && _page->editorState().selectionIsNone) { // If the utility views are not already visible, we don't want them to become visible during the animation since // they could not start from a reasonable state. // This is not perfect since views could also get updated during the animation, in practice this is rare and the end state // remains correct. [self _cancelInteraction]; [_interactionViewsContainerView setHidden:YES]; [UIView _addCompletion:^(BOOL){ [_interactionViewsContainerView setHidden:NO]; }]; } [self _updateTapHighlight]; _selectionNeedsUpdate = YES; [self _updateChangedSelection:YES]; } - (void)_enableInspectorNodeSearch { _inspectorNodeSearchEnabled = YES; [self _cancelInteraction]; [self _removeDefaultGestureRecognizers]; _inspectorNodeSearchGestureRecognizer = adoptNS([[WKInspectorNodeSearchGestureRecognizer alloc] initWithTarget:self action:@selector(_inspectorNodeSearchRecognized:)]); [self addGestureRecognizer:_inspectorNodeSearchGestureRecognizer.get()]; } - (void)_disableInspectorNodeSearch { _inspectorNodeSearchEnabled = NO; [self _addDefaultGestureRecognizers]; [self removeGestureRecognizer:_inspectorNodeSearchGestureRecognizer.get()]; _inspectorNodeSearchGestureRecognizer = nil; } - (UIView *)hitTest:(CGPoint)point withEvent:(::UIEvent *)event { for (UIView *subView in [_interactionViewsContainerView.get() subviews]) { UIView *hitView = [subView hitTest:[subView convertPoint:point fromView:self] withEvent:event]; if (hitView) { LOG_WITH_STREAM(UIHitTesting, stream << self << "hitTest at " << WebCore::FloatPoint(point) << " found interaction view " << hitView); return hitView; } } LOG_WITH_STREAM(UIHitTesting, stream << "hit-testing WKContentView subviews " << [[self recursiveDescription] UTF8String]); UIView* hitView = [super hitTest:point withEvent:event]; LOG_WITH_STREAM(UIHitTesting, stream << " found view " << [hitView class] << " " << (void*)hitView); return hitView; } - (const WebKit::InteractionInformationAtPosition&)positionInformation { return _positionInformation; } - (void)setInputDelegate:(id )inputDelegate { _inputDelegate = inputDelegate; } - (id )inputDelegate { return _inputDelegate.getAutoreleased(); } - (CGPoint)lastInteractionLocation { return _lastInteractionLocation; } - (BOOL)shouldHideSelectionWhenScrolling { if (_isEditable) return _focusedElementInformation.insideFixedPosition; auto& editorState = _page->editorState(); return !editorState.isMissingPostLayoutData && editorState.selectionIsRange && editorState.postLayoutData().insideFixedPosition; } - (BOOL)isEditable { return _isEditable; } - (BOOL)setIsEditable:(BOOL)isEditable { if (isEditable == _isEditable) return NO; _isEditable = isEditable; return YES; } - (void)_endEditing { [_inputPeripheral endEditing]; [_formInputSession endEditing]; #if ENABLE(DATALIST_ELEMENT) [_dataListTextSuggestionsInputView controlEndEditing]; #endif } - (void)_cancelPreviousResetInputViewDeferralRequest { [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_resetInputViewDeferral) object:nil]; } - (void)_scheduleResetInputViewDeferralAfterBecomingFirstResponder { [self _cancelPreviousResetInputViewDeferralRequest]; const NSTimeInterval inputViewDeferralWatchdogTimerDuration = 0.5; [self performSelector:@selector(_resetInputViewDeferral) withObject:self afterDelay:inputViewDeferralWatchdogTimerDuration]; } - (void)_resetInputViewDeferral { [self _cancelPreviousResetInputViewDeferralRequest]; _inputViewUpdateDeferrer = nullptr; } - (BOOL)canBecomeFirstResponder { return _becomingFirstResponder; } - (BOOL)canBecomeFirstResponderForWebView { if (_resigningFirstResponder) return NO; // We might want to return something else // if we decide to enable/disable interaction programmatically. return YES; } - (BOOL)becomeFirstResponder { return [_webView becomeFirstResponder]; } - (BOOL)becomeFirstResponderForWebView { if (_resigningFirstResponder) return NO; if (!_inputViewUpdateDeferrer) _inputViewUpdateDeferrer = makeUnique(self); BOOL didBecomeFirstResponder; { SetForScope becomingFirstResponder { _becomingFirstResponder, YES }; didBecomeFirstResponder = [super becomeFirstResponder]; } if (didBecomeFirstResponder) { _page->installActivityStateChangeCompletionHandler([weakSelf = WeakObjCPtr(self)] { if (!weakSelf) return; auto strongSelf = weakSelf.get(); [strongSelf _resetInputViewDeferral]; }); _page->activityStateDidChange(WebCore::ActivityState::IsFocused, WebKit::WebPageProxy::ActivityStateChangeDispatchMode::Immediate); if ([self canShowNonEmptySelectionView] || (!_suppressSelectionAssistantReasons && _activeTextInteractionCount)) [_textInteractionAssistant activateSelection]; [self _scheduleResetInputViewDeferralAfterBecomingFirstResponder]; } else [self _resetInputViewDeferral]; return didBecomeFirstResponder; } - (BOOL)resignFirstResponder { return [_webView resignFirstResponder]; } typedef NS_ENUM(NSInteger, EndEditingReason) { EndEditingReasonAccessoryDone, EndEditingReasonResigningFirstResponder, }; - (void)endEditingAndUpdateFocusAppearanceWithReason:(EndEditingReason)reason { if (!self.webView._retainingActiveFocusedState) { // We need to complete the editing operation before we blur the element. [self _endEditing]; auto shouldBlurFocusedElement = [&] { if (_keyboardDidRequestDismissal) return true; if (self._shouldUseLegacySelectPopoverDismissalBehavior) return true; if (reason == EndEditingReasonAccessoryDone) { if (_focusRequiresStrongPasswordAssistance) return true; if (WebKit::currentUserInterfaceIdiomIsSmallScreen()) return true; } return false; }; if (shouldBlurFocusedElement()) { _page->blurFocusedElement(); // Don't wait for WebPageProxy::blurFocusedElement() to round-trip back to us to hide the keyboard // because we know that the user explicitly requested us to do so. [self _elementDidBlur]; } } [self _cancelInteraction]; [_textInteractionAssistant deactivateSelection]; [self _resetInputViewDeferral]; } - (BOOL)resignFirstResponderForWebView { // FIXME: Maybe we should call resignFirstResponder on the superclass // and do nothing if the return value is NO. SetForScope resigningFirstResponderScope { _resigningFirstResponder, YES }; [self endEditingAndUpdateFocusAppearanceWithReason:EndEditingReasonResigningFirstResponder]; // If the user explicitly dismissed the keyboard then we will lose first responder // status only to gain it back again. Just don't resign in that case. if (_keyboardDidRequestDismissal) { _keyboardDidRequestDismissal = NO; return NO; } bool superDidResign = [super resignFirstResponder]; if (superDidResign) { [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture]; _page->activityStateDidChange(WebCore::ActivityState::IsFocused, WebKit::WebPageProxy::ActivityStateChangeDispatchMode::Immediate); if (_keyWebEventHandler) { RunLoop::main().dispatch([weakHandler = WeakObjCPtr(_keyWebEventHandler.get()), weakSelf = WeakObjCPtr(self)] { auto strongSelf = weakSelf.get(); if (!strongSelf || [strongSelf isFirstResponder]) return; auto strongHandler = weakHandler.get(); if (!strongHandler) return; if (strongSelf->_keyWebEventHandler.get() != strongHandler.get()) return; auto keyEventHandler = std::exchange(strongSelf->_keyWebEventHandler, nil); auto page = strongSelf->_page; if (!page) return; if (!page->hasQueuedKeyEvent()) return; keyEventHandler(page->firstQueuedKeyEvent().nativeEvent(), YES); }); } } return superDidResign; } - (void)cancelPointersForGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { NSMapTable *activeTouches = [_touchEventGestureRecognizer activeTouchesByIdentifier]; for (NSNumber *touchIdentifier in activeTouches) { UITouch *touch = [activeTouches objectForKey:touchIdentifier]; if ([touch.gestureRecognizers containsObject:gestureRecognizer]) _page->cancelPointer([touchIdentifier unsignedIntValue], WebCore::roundedIntPoint([touch locationInView:self])); } } - (std::optional)activeTouchIdentifierForGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer { NSMapTable *activeTouches = [_touchEventGestureRecognizer activeTouchesByIdentifier]; for (NSNumber *touchIdentifier in activeTouches) { UITouch *touch = [activeTouches objectForKey:touchIdentifier]; if ([touch.gestureRecognizers containsObject:gestureRecognizer]) return [touchIdentifier unsignedIntValue]; } return std::nullopt; } - (BOOL)_touchEventsMustRequireGestureRecognizerToFail:(UIGestureRecognizer *)gestureRecognizer { auto webView = self.webView; if ([webView _isNavigationSwipeGestureRecognizer:gestureRecognizer]) return YES; id uiDelegate = static_cast>([webView UIDelegate]); return gestureRecognizer.view == webView && [uiDelegate respondsToSelector:@selector(_webView:touchEventsMustRequireGestureRecognizerToFail:)] && [uiDelegate _webView:webView touchEventsMustRequireGestureRecognizerToFail:gestureRecognizer]; } - (void)_webTouchEventsRecognized:(UIWebTouchEventsGestureRecognizer *)gestureRecognizer { if (!_page->hasRunningProcess()) return; const _UIWebTouchEvent* lastTouchEvent = gestureRecognizer.lastTouchEvent; _lastInteractionLocation = lastTouchEvent->locationInDocumentCoordinates; if (lastTouchEvent->type == UIWebTouchEventTouchBegin) { if (!_failedTouchStartDeferringGestures) _failedTouchStartDeferringGestures = { { } }; [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture]; _layerTreeTransactionIdAtLastInteractionStart = downcast(*_page->drawingArea()).lastCommittedLayerTreeTransactionID(); #if ENABLE(TOUCH_EVENTS) _page->resetPotentialTapSecurityOrigin(); #endif WebKit::InteractionInformationRequest positionInformationRequest { WebCore::IntPoint(_lastInteractionLocation) }; [self doAfterPositionInformationUpdate:[assistant = WeakObjCPtr(_actionSheetAssistant.get())] (WebKit::InteractionInformationAtPosition information) { [assistant interactionDidStartWithPositionInformation:information]; } forRequest:positionInformationRequest]; } #if ENABLE(TOUCH_EVENTS) WebKit::NativeWebTouchEvent nativeWebTouchEvent { lastTouchEvent, gestureRecognizer.modifierFlags }; nativeWebTouchEvent.setCanPreventNativeGestures(_touchEventsCanPreventNativeGestures || [gestureRecognizer isDefaultPrevented]); [self _handleTouchActionsForTouchEvent:nativeWebTouchEvent]; if (_touchEventsCanPreventNativeGestures) _page->handlePreventableTouchEvent(nativeWebTouchEvent); else _page->handleUnpreventableTouchEvent(nativeWebTouchEvent); if (nativeWebTouchEvent.allTouchPointsAreReleased()) { _touchEventsCanPreventNativeGestures = YES; if (!_page->isScrollingOrZooming()) [self _resetPanningPreventionFlags]; if (nativeWebTouchEvent.isPotentialTap() && self.hasHiddenContentEditable && self._hasFocusedElement && !self.window.keyWindow) [self.window makeKeyWindow]; auto stopDeferringNativeGesturesIfNeeded = [] (NSArray *deferringGestures) { for (WKDeferringGestureRecognizer *gesture in deferringGestures) { if (gesture.state == UIGestureRecognizerStatePossible) gesture.state = UIGestureRecognizerStateFailed; } }; if (!_page->isHandlingPreventableTouchStart()) stopDeferringNativeGesturesIfNeeded(self._touchStartDeferringGestures); if (!_page->isHandlingPreventableTouchEnd()) stopDeferringNativeGesturesIfNeeded(self._touchEndDeferringGestures); _failedTouchStartDeferringGestures = std::nullopt; } #endif // ENABLE(TOUCH_EVENTS) } #if ENABLE(TOUCH_EVENTS) - (void)_handleTouchActionsForTouchEvent:(const WebKit::NativeWebTouchEvent&)touchEvent { auto* scrollingCoordinator = _page->scrollingCoordinatorProxy(); if (!scrollingCoordinator) return; for (const auto& touchPoint : touchEvent.touchPoints()) { auto phase = touchPoint.phase(); if (phase == WebKit::WebPlatformTouchPoint::TouchPressed) { auto touchActions = WebKit::touchActionsForPoint(self, touchPoint.location()); LOG_WITH_STREAM(UIHitTesting, stream << "touchActionsForPoint " << touchPoint.location() << " found " << touchActions); if (!touchActions || touchActions.contains(WebCore::TouchAction::Auto)) continue; [_touchActionGestureRecognizer setTouchActions:touchActions forTouchIdentifier:touchPoint.identifier()]; scrollingCoordinator->setTouchActionsForTouchIdentifier(touchActions, touchPoint.identifier()); _preventsPanningInXAxis = !touchActions.containsAny({ WebCore::TouchAction::PanX, WebCore::TouchAction::Manipulation }); _preventsPanningInYAxis = !touchActions.containsAny({ WebCore::TouchAction::PanY, WebCore::TouchAction::Manipulation }); } else if (phase == WebKit::WebPlatformTouchPoint::TouchReleased || phase == WebKit::WebPlatformTouchPoint::TouchCancelled) { [_touchActionGestureRecognizer clearTouchActionsForTouchIdentifier:touchPoint.identifier()]; scrollingCoordinator->clearTouchActionsForTouchIdentifier(touchPoint.identifier()); } } } #endif // ENABLE(TOUCH_EVENTS) - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceiveTouch:(UITouch *)touch { #if HAVE(LOOKUP_GESTURE_RECOGNIZER) if (gestureRecognizer == _lookupGestureRecognizer) return YES; #endif #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) if (gestureRecognizer != _mouseGestureRecognizer && [_mouseGestureRecognizer mouseTouch] == touch && touch._isPointerTouch) return NO; if (gestureRecognizer != _alternateMouseGestureRecognizer && [_alternateMouseGestureRecognizer mouseTouch] == touch) return NO; if (gestureRecognizer == _doubleTapGestureRecognizer || gestureRecognizer == _nonBlockingDoubleTapGestureRecognizer) return touch.type != UITouchTypeIndirectPointer; #endif if (gestureRecognizer == _touchActionLeftSwipeGestureRecognizer || gestureRecognizer == _touchActionRightSwipeGestureRecognizer || gestureRecognizer == _touchActionUpSwipeGestureRecognizer || gestureRecognizer == _touchActionDownSwipeGestureRecognizer) { // We update the enabled state of the various swipe gesture recognizers such that if we have a unidirectional touch-action // specified (only pan-x or only pan-y) we enable the two recognizers in the opposite axis to prevent scrolling from starting // if the initial gesture is such a swipe. Since the recognizers are specified to use a single finger for recognition, we don't // need to worry about the case where there may be more than a single touch for a given UIScrollView. auto touchLocation = [touch locationInView:self]; auto touchActions = WebKit::touchActionsForPoint(self, WebCore::roundedIntPoint(touchLocation)); LOG_WITH_STREAM(UIHitTesting, stream << "gestureRecognizer:shouldReceiveTouch: at " << WebCore::FloatPoint(touchLocation) << " - touch actions " << touchActions); if (gestureRecognizer == _touchActionLeftSwipeGestureRecognizer || gestureRecognizer == _touchActionRightSwipeGestureRecognizer) return touchActions == WebCore::TouchAction::PanY; return touchActions == WebCore::TouchAction::PanX; } return YES; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldReceivePress:(UIPress *)press { return YES; } #pragma mark - WKTouchActionGestureRecognizerDelegate implementation - (BOOL)gestureRecognizerMayPanWebView:(UIGestureRecognizer *)gestureRecognizer { // The gesture recognizer is the main UIScrollView's UIPanGestureRecognizer. if (gestureRecognizer == [_webView scrollView].panGestureRecognizer) return YES; // The gesture recognizer is a child UIScrollView's UIPanGestureRecognizer created by WebKit. if (gestureRecognizer.view && [gestureRecognizer.view isKindOfClass:[WKChildScrollView class]]) return YES; return NO; } - (BOOL)gestureRecognizerMayPinchToZoomWebView:(UIGestureRecognizer *)gestureRecognizer { // The gesture recognizer is the main UIScrollView's UIPinchGestureRecognizer. if (gestureRecognizer == [_webView scrollView].pinchGestureRecognizer) return YES; // The gesture recognizer is another UIPichGestureRecognizer known to lead to pinch-to-zoom. if ([gestureRecognizer isKindOfClass:[UIPinchGestureRecognizer class]]) { if (auto uiDelegate = static_cast>(self.webView.UIDelegate)) { if ([uiDelegate respondsToSelector:@selector(_webView:gestureRecognizerCouldPinch:)]) return [uiDelegate _webView:self.webView gestureRecognizerCouldPinch:gestureRecognizer]; } } return NO; } - (BOOL)gestureRecognizerMayDoubleTapToZoomWebView:(UIGestureRecognizer *)gestureRecognizer { return gestureRecognizer == _doubleTapGestureRecognizer || gestureRecognizer == _twoFingerDoubleTapGestureRecognizer; } - (NSMapTable *)touchActionActiveTouches { return [_touchEventGestureRecognizer activeTouchesByIdentifier]; } - (void)_resetPanningPreventionFlags { _preventsPanningInXAxis = NO; _preventsPanningInYAxis = NO; } - (void)_inspectorNodeSearchRecognized:(UIGestureRecognizer *)gestureRecognizer { ASSERT(_inspectorNodeSearchEnabled); [self _resetIsDoubleTapPending]; CGPoint point = [gestureRecognizer locationInView:self]; switch (gestureRecognizer.state) { case UIGestureRecognizerStateBegan: case UIGestureRecognizerStateChanged: _page->inspectorNodeSearchMovedToPosition(point); break; case UIGestureRecognizerStateEnded: case UIGestureRecognizerStateCancelled: default: // To ensure we turn off node search. _page->inspectorNodeSearchEndedAtPosition(point); break; } } static WebCore::FloatQuad inflateQuad(const WebCore::FloatQuad& quad, float inflateSize) { // We sort the output points like this (as expected by the highlight view): // p2------p3 // | | // p1------p4 // 1) Sort the points horizontally. WebCore::FloatPoint points[4] = { quad.p1(), quad.p4(), quad.p2(), quad.p3() }; if (points[0].x() > points[1].x()) std::swap(points[0], points[1]); if (points[2].x() > points[3].x()) std::swap(points[2], points[3]); if (points[0].x() > points[2].x()) std::swap(points[0], points[2]); if (points[1].x() > points[3].x()) std::swap(points[1], points[3]); if (points[1].x() > points[2].x()) std::swap(points[1], points[2]); // 2) Swap them vertically to have the output points [p2, p1, p3, p4]. if (points[1].y() < points[0].y()) std::swap(points[0], points[1]); if (points[3].y() < points[2].y()) std::swap(points[2], points[3]); // 3) Adjust the positions. points[0].move(-inflateSize, -inflateSize); points[1].move(-inflateSize, inflateSize); points[2].move(inflateSize, -inflateSize); points[3].move(inflateSize, inflateSize); return WebCore::FloatQuad(points[1], points[0], points[2], points[3]); } #if ENABLE(TOUCH_EVENTS) - (void)_webTouchEvent:(const WebKit::NativeWebTouchEvent&)touchEvent preventsNativeGestures:(BOOL)preventsNativeGesture { if (!preventsNativeGesture || ![_touchEventGestureRecognizer isDispatchingTouchEvents]) return; _longPressCanClick = NO; _touchEventsCanPreventNativeGestures = NO; [_touchEventGestureRecognizer setDefaultPrevented:YES]; } #endif - (UIWebTouchEventsGestureRecognizer *)touchEventGestureRecognizer { return _touchEventGestureRecognizer.get(); } - (WebKit::GestureRecognizerConsistencyEnforcer&)gestureRecognizerConsistencyEnforcer { if (!_gestureRecognizerConsistencyEnforcer) _gestureRecognizerConsistencyEnforcer = makeUnique(self); return *_gestureRecognizerConsistencyEnforcer; } - (NSArray *)deferringGestures { auto gestures = [NSMutableArray arrayWithCapacity:7]; [gestures addObjectsFromArray:self._touchStartDeferringGestures]; [gestures addObjectsFromArray:self._touchEndDeferringGestures]; #if ENABLE(IMAGE_ANALYSIS) [gestures addObject:_imageAnalysisDeferringGestureRecognizer.get()]; #endif return gestures; } - (NSArray *)_touchStartDeferringGestures { WKDeferringGestureRecognizer *recognizers[3]; NSUInteger count = 0; auto add = [&] (const RetainPtr& recognizer) { if (recognizer) recognizers[count++] = recognizer.get(); }; add(_touchStartDeferringGestureRecognizerForImmediatelyResettableGestures); add(_touchStartDeferringGestureRecognizerForDelayedResettableGestures); add(_touchStartDeferringGestureRecognizerForSyntheticTapGestures); return [NSArray arrayWithObjects:recognizers count:count]; } - (NSArray *)_touchEndDeferringGestures { WKDeferringGestureRecognizer *recognizers[3]; NSUInteger count = 0; auto add = [&] (const RetainPtr& recognizer) { if (recognizer) recognizers[count++] = recognizer.get(); }; add(_touchEndDeferringGestureRecognizerForImmediatelyResettableGestures); add(_touchEndDeferringGestureRecognizerForDelayedResettableGestures); add(_touchEndDeferringGestureRecognizerForSyntheticTapGestures); return [NSArray arrayWithObjects:recognizers count:count]; } - (void)_doneDeferringTouchStart:(BOOL)preventNativeGestures { for (WKDeferringGestureRecognizer *gestureRecognizer in self._touchStartDeferringGestures) { [gestureRecognizer endDeferral:preventNativeGestures ? WebKit::ShouldPreventGestures::Yes : WebKit::ShouldPreventGestures::No]; if (_failedTouchStartDeferringGestures && !preventNativeGestures) _failedTouchStartDeferringGestures->add(gestureRecognizer); } } - (void)_doneDeferringTouchEnd:(BOOL)preventNativeGestures { for (WKDeferringGestureRecognizer *gesture in self._touchEndDeferringGestures) [gesture endDeferral:preventNativeGestures ? WebKit::ShouldPreventGestures::Yes : WebKit::ShouldPreventGestures::No]; } - (BOOL)_isTouchStartDeferringGesture:(WKDeferringGestureRecognizer *)gesture { return gesture == _touchStartDeferringGestureRecognizerForSyntheticTapGestures || gesture == _touchStartDeferringGestureRecognizerForImmediatelyResettableGestures || gesture == _touchStartDeferringGestureRecognizerForDelayedResettableGestures; } - (BOOL)_isTouchEndDeferringGesture:(WKDeferringGestureRecognizer *)gesture { return gesture == _touchEndDeferringGestureRecognizerForSyntheticTapGestures || gesture == _touchEndDeferringGestureRecognizerForImmediatelyResettableGestures || gesture == _touchEndDeferringGestureRecognizerForDelayedResettableGestures; } static NSValue *nsSizeForTapHighlightBorderRadius(WebCore::IntSize borderRadius, CGFloat borderRadiusScale) { return [NSValue valueWithCGSize:CGSizeMake((borderRadius.width() * borderRadiusScale) + minimumTapHighlightRadius, (borderRadius.height() * borderRadiusScale) + minimumTapHighlightRadius)]; } - (void)_updateTapHighlight { if (![_highlightView superview]) return; [_highlightView setColor:cocoaColor(_tapHighlightInformation.color).get()]; auto& highlightedQuads = _tapHighlightInformation.quads; bool allRectilinear = true; for (auto& quad : highlightedQuads) { if (!quad.isRectilinear()) { allRectilinear = false; break; } } auto selfScale = self.layer.transform.m11; if (allRectilinear) { float deviceScaleFactor = _page->deviceScaleFactor(); auto rects = createNSArray(highlightedQuads, [&] (auto& quad) { auto boundingBox = quad.boundingBox(); boundingBox.scale(selfScale); boundingBox.inflate(minimumTapHighlightRadius); return [NSValue valueWithCGRect:encloseRectToDevicePixels(boundingBox, deviceScaleFactor)]; }); [_highlightView setFrames:rects.get() boundaryRect:_page->exposedContentRect()]; } else { auto quads = adoptNS([[NSMutableArray alloc] initWithCapacity:highlightedQuads.size() * 4]); for (auto quad : highlightedQuads) { quad.scale(selfScale); auto extendedQuad = inflateQuad(quad, minimumTapHighlightRadius); [quads addObject:[NSValue valueWithCGPoint:extendedQuad.p1()]]; [quads addObject:[NSValue valueWithCGPoint:extendedQuad.p2()]]; [quads addObject:[NSValue valueWithCGPoint:extendedQuad.p3()]]; [quads addObject:[NSValue valueWithCGPoint:extendedQuad.p4()]]; } [_highlightView setQuads:quads.get() boundaryRect:_page->exposedContentRect()]; } [_highlightView setCornerRadii:@[ nsSizeForTapHighlightBorderRadius(_tapHighlightInformation.topLeftRadius, selfScale), nsSizeForTapHighlightBorderRadius(_tapHighlightInformation.topRightRadius, selfScale), nsSizeForTapHighlightBorderRadius(_tapHighlightInformation.bottomLeftRadius, selfScale), nsSizeForTapHighlightBorderRadius(_tapHighlightInformation.bottomRightRadius, selfScale), ]]; } - (CGRect)tapHighlightViewRect { return [_highlightView frame]; } - (UIGestureRecognizer *)imageAnalysisGestureRecognizer { #if ENABLE(IMAGE_ANALYSIS) return _imageAnalysisGestureRecognizer.get(); #else return nil; #endif } - (void)_showTapHighlight { auto shouldPaintTapHighlight = [&](const WebCore::FloatRect& rect) { #if PLATFORM(MACCATALYST) UNUSED_PARAM(rect); return NO; #else if (_tapHighlightInformation.nodeHasBuiltInClickHandling) return true; static const float highlightPaintThreshold = 0.3; // 30% float highlightArea = 0; for (auto highlightQuad : _tapHighlightInformation.quads) { auto boundingBox = highlightQuad.boundingBox(); highlightArea += boundingBox.area(); if (boundingBox.width() > (rect.width() * highlightPaintThreshold) || boundingBox.height() > (rect.height() * highlightPaintThreshold)) return false; } return highlightArea < rect.area() * highlightPaintThreshold; #endif }; if (!shouldPaintTapHighlight(_page->unobscuredContentRect()) && !_showDebugTapHighlightsForFastClicking) return; if (!_highlightView) { _highlightView = adoptNS([[_UIHighlightView alloc] initWithFrame:CGRectZero]); [_highlightView setUserInteractionEnabled:NO]; [_highlightView setOpaque:NO]; [_highlightView setCornerRadius:minimumTapHighlightRadius]; } [_highlightView layer].opacity = 1; [_interactionViewsContainerView addSubview:_highlightView.get()]; [self _updateTapHighlight]; } - (void)_didGetTapHighlightForRequest:(WebKit::TapIdentifier)requestID color:(const WebCore::Color&)color quads:(const Vector&)highlightedQuads topLeftRadius:(const WebCore::IntSize&)topLeftRadius topRightRadius:(const WebCore::IntSize&)topRightRadius bottomLeftRadius:(const WebCore::IntSize&)bottomLeftRadius bottomRightRadius:(const WebCore::IntSize&)bottomRightRadius nodeHasBuiltInClickHandling:(BOOL)nodeHasBuiltInClickHandling { if (!_isTapHighlightIDValid || _latestTapID != requestID) return; if (self._hasFocusedElement && _positionInformation.elementContext && _positionInformation.elementContext->isSameElement(_focusedElementInformation.elementContext)) return; _isTapHighlightIDValid = NO; _tapHighlightInformation.quads = highlightedQuads; _tapHighlightInformation.topLeftRadius = topLeftRadius; _tapHighlightInformation.topRightRadius = topRightRadius; _tapHighlightInformation.bottomLeftRadius = bottomLeftRadius; _tapHighlightInformation.bottomRightRadius = bottomRightRadius; _tapHighlightInformation.nodeHasBuiltInClickHandling = nodeHasBuiltInClickHandling; if (_showDebugTapHighlightsForFastClicking) _tapHighlightInformation.color = [self _tapHighlightColorForFastClick:![_doubleTapGestureRecognizer isEnabled]]; else _tapHighlightInformation.color = color; if (_potentialTapInProgress) { _hasTapHighlightForPotentialTap = YES; return; } [self _showTapHighlight]; if (_isExpectingFastSingleTapCommit) { [self _finishInteraction]; if (!_potentialTapInProgress) _isExpectingFastSingleTapCommit = NO; } } - (BOOL)_mayDisableDoubleTapGesturesDuringSingleTap { return _potentialTapInProgress; } - (void)_disableDoubleTapGesturesDuringTapIfNecessary:(WebKit::TapIdentifier)requestID { if (_latestTapID != requestID) return; [self _setDoubleTapGesturesEnabled:NO]; } - (void)_handleSmartMagnificationInformationForPotentialTap:(WebKit::TapIdentifier)requestID renderRect:(const WebCore::FloatRect&)renderRect fitEntireRect:(BOOL)fitEntireRect viewportMinimumScale:(double)viewportMinimumScale viewportMaximumScale:(double)viewportMaximumScale nodeIsRootLevel:(BOOL)nodeIsRootLevel { const auto& preferences = _page->preferences(); ASSERT(preferences.fasterClicksEnabled()); if (!_potentialTapInProgress) return; // We check both the system preference and the page preference, because we only want this // to apply in "desktop" mode. if (preferences.preferFasterClickOverDoubleTap() && _page->preferFasterClickOverDoubleTap()) { RELEASE_LOG(ViewGestures, "Potential tap found an element and fast taps are preferred. Trigger click. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); if (preferences.zoomOnDoubleTapWhenRoot() && nodeIsRootLevel) { RELEASE_LOG(ViewGestures, "The click handler was on a root-level element, so don't disable double-tap. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); return; } if (preferences.alwaysZoomOnDoubleTap()) { RELEASE_LOG(ViewGestures, "DTTZ is forced on, so don't disable double-tap. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); return; } RELEASE_LOG(ViewGestures, "Give preference to click by disabling double-tap. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); [self _setDoubleTapGesturesEnabled:NO]; return; } auto currentScale = self._contentZoomScale; auto targetScale = _smartMagnificationController->zoomFactorForTargetRect(renderRect, fitEntireRect, viewportMinimumScale, viewportMaximumScale); if (std::min(targetScale, currentScale) / std::max(targetScale, currentScale) > fasterTapSignificantZoomThreshold) { RELEASE_LOG(ViewGestures, "Potential tap would not cause a significant zoom. Trigger click. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); [self _setDoubleTapGesturesEnabled:NO]; return; } RELEASE_LOG(ViewGestures, "Potential tap may cause significant zoom. Wait. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); } - (void)_cancelLongPressGestureRecognizer { [_highlightLongPressGestureRecognizer cancel]; } - (void)_cancelTouchEventGestureRecognizer { #if HAVE(CANCEL_WEB_TOUCH_EVENTS_GESTURE) [_touchEventGestureRecognizer cancel]; #endif } - (void)_didScroll { [self _updateFrameOfContainerForContextMenuHintPreviewsIfNeeded]; [self _cancelLongPressGestureRecognizer]; [self _cancelInteraction]; } - (void)_scrollingNodeScrollingWillBegin:(WebCore::ScrollingNodeID)scrollingNodeID { [_textInteractionAssistant willStartScrollingOverflow]; } - (void)_scrollingNodeScrollingDidEnd:(WebCore::ScrollingNodeID)scrollingNodeID { // If scrolling ends before we've received a selection update, // we postpone showing the selection until the update is received. if (!_selectionNeedsUpdate) { _shouldRestoreSelection = YES; return; } [self _updateChangedSelection]; [_textInteractionAssistant didEndScrollingOverflow]; UIScrollView *targetScrollView = nil; if (auto* scrollingCoordinator = _page->scrollingCoordinatorProxy()) targetScrollView = scrollingCoordinator->scrollViewForScrollingNodeID(scrollingNodeID); [_webView _didFinishScrolling:targetScrollView]; } - (BOOL)shouldShowAutomaticKeyboardUI { if (_focusedElementInformation.inputMode == WebCore::InputMode::None) return NO; if (_focusedElementInformation.isFocusingWithDataListDropdown && !UIKeyboard.isInHardwareKeyboardMode) return NO; return [self _shouldShowAutomaticKeyboardUIIgnoringInputMode]; } - (BOOL)_shouldShowAutomaticKeyboardUIIgnoringInputMode { if (_focusedElementInformation.isReadOnly) return NO; switch (_focusedElementInformation.elementType) { case WebKit::InputType::None: #if ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Color: #endif case WebKit::InputType::Drawing: case WebKit::InputType::Date: case WebKit::InputType::Month: case WebKit::InputType::DateTimeLocal: case WebKit::InputType::Time: return NO; case WebKit::InputType::Select: { #if ENABLE(IOS_FORM_CONTROL_REFRESH) if (self._shouldUseContextMenusForFormControls) return NO; #endif return WebKit::currentUserInterfaceIdiomIsSmallScreen(); } default: return YES; } return NO; } - (BOOL)_disableAutomaticKeyboardUI { // Always enable automatic keyboard UI if we are not the first responder to avoid // interfering with other focused views (e.g. Find-in-page). return [self isFirstResponder] && ![self shouldShowAutomaticKeyboardUI]; } - (BOOL)_requiresKeyboardWhenFirstResponder { if ([_webView _isEditable] && !self._disableAutomaticKeyboardUI) return YES; // FIXME: We should add the logic to handle keyboard visibility during focus redirects. return [self _shouldShowAutomaticKeyboardUIIgnoringInputMode] || _seenHardwareKeyDownInNonEditableElement; } - (BOOL)_requiresKeyboardResetOnReload { return YES; } - (WebCore::FloatRect)rectToRevealWhenZoomingToFocusedElement { WebCore::IntRect elementInteractionRect; if (_focusedElementInformation.interactionRect.contains(_focusedElementInformation.lastInteractionLocation)) elementInteractionRect = { _focusedElementInformation.lastInteractionLocation, { 1, 1 } }; if (!mayContainSelectableText(_focusedElementInformation.elementType)) return elementInteractionRect; if (_page->editorState().isMissingPostLayoutData) return elementInteractionRect; auto boundingRect = _page->selectionBoundingRectInRootViewCoordinates(); boundingRect.intersect(_focusedElementInformation.interactionRect); return boundingRect; } - (void)_keyboardWillShow { _waitingForKeyboardToStartAnimatingInAfterElementFocus = NO; } - (void)_keyboardDidShow { [self _zoomToFocusRectAfterShowingKeyboardIfNeeded]; #if USE(UICONTEXTMENU) [_fileUploadPanel repositionContextMenuIfNeeded]; #endif } - (void)_zoomToFocusRectAfterShowingKeyboardIfNeeded { if (!_shouldZoomToFocusRectAfterShowingKeyboard) return; // FIXME: This deferred call to -_zoomToRevealFocusedElement works around the fact that Mail compose // disables automatic content inset adjustment using the keyboard height, and instead has logic to // explicitly set WKScrollView's contentScrollInset after receiving UIKeyboardDidShowNotification. // This means that if we -_zoomToRevealFocusedElement immediately after focusing the body field in // Mail, we won't take the keyboard height into account when scrolling. // Mitigate this by deferring the call to -_zoomToRevealFocusedElement in this case until after the // keyboard has finished animating. We can revert this once rdar://87733414 is fixed. [self resetShouldZoomToFocusRectAfterShowingKeyboard]; [self performSelector:@selector(_zoomToRevealFocusedElement) withObject:nil afterDelay:0]; } - (void)_zoomToRevealFocusedElement { if (_focusedElementInformation.preventScroll) return; if (_suppressSelectionAssistantReasons || _activeTextInteractionCount) return; if (!self._scroller.firstResponderKeyboardAvoidanceEnabled && (_page->isKeyboardAnimatingIn() || _waitingForKeyboardToStartAnimatingInAfterElementFocus)) { _shouldZoomToFocusRectAfterShowingKeyboard = YES; return; } // In case user scaling is force enabled, do not use that scaling when zooming in with an input field. // Zooming above the page's default scale factor should only happen when the user performs it. [self _zoomToFocusRect:_focusedElementInformation.interactionRect selectionRect:_didAccessoryTabInitiateFocus ? WebCore::FloatRect() : self.rectToRevealWhenZoomingToFocusedElement fontSize:_focusedElementInformation.nodeFontSize minimumScale:_focusedElementInformation.minimumScaleFactor maximumScale:_focusedElementInformation.maximumScaleFactorIgnoringAlwaysScalable allowScaling:_focusedElementInformation.allowsUserScalingIgnoringAlwaysScalable && WebKit::currentUserInterfaceIdiomIsSmallScreen() forceScroll:[self requiresAccessoryView]]; } - (void)resetShouldZoomToFocusRectAfterShowingKeyboard { _shouldZoomToFocusRectAfterShowingKeyboard = NO; [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(_zoomToRevealFocusedElement) object:nil]; } - (UIView *)inputView { return [_webView inputView]; } - (UIView *)inputViewForWebView { if (!self._hasFocusedElement) return nil; if (_inputPeripheral) { // FIXME: UIKit may invoke -[WKContentView inputView] at any time when WKContentView is the first responder; // as such, it doesn't make sense to change the enclosing scroll view's zoom scale and content offset to reveal // the focused element here. It seems this behavior was added to match logic in legacy WebKit (refer to // UIWebBrowserView). Instead, we should find the places where we currently assume that UIKit (or other clients) // invoke -inputView to zoom to the focused element, and either surface SPI for clients to zoom to the focused // element, or explicitly trigger the zoom from WebKit. // For instance, one use case that currently relies on this detail is adjusting the zoom scale and viewport upon // rotation, when a select element is focused. See for more information. [self _zoomToRevealFocusedElement]; [self _updateAccessory]; } if (UIView *customInputView = [_formInputSession customInputView]) return customInputView; #if ENABLE(DATALIST_ELEMENT) if (_dataListTextSuggestionsInputView) return _dataListTextSuggestionsInputView.get(); #endif return [_inputPeripheral assistantView]; } - (CGRect)_selectionClipRect { if (_page->waitingForPostLayoutEditorStateUpdateAfterFocusingElement()) return _focusedElementInformation.interactionRect; if (!_page->editorState().postLayoutData().selectionClipRect.isEmpty()) return _page->editorState().postLayoutData().selectionClipRect; return CGRectNull; } static BOOL isBuiltInScrollViewGestureRecognizer(UIGestureRecognizer *recognizer) { return ([recognizer isKindOfClass:NSClassFromString(@"UIScrollViewPanGestureRecognizer")] || [recognizer isKindOfClass:NSClassFromString(@"UIScrollViewPinchGestureRecognizer")] || [recognizer isKindOfClass:NSClassFromString(@"UIScrollViewKnobLongPressGestureRecognizer")] ); } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer canPreventGestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer { // A long-press gesture can not be recognized while panning, but a pan can be recognized // during a long-press gesture. BOOL shouldNotPreventScrollViewGestures = preventingGestureRecognizer == _highlightLongPressGestureRecognizer || preventingGestureRecognizer == _longPressGestureRecognizer; return !(shouldNotPreventScrollViewGestures && isBuiltInScrollViewGestureRecognizer(preventedGestureRecognizer)); } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)preventedGestureRecognizer canBePreventedByGestureRecognizer:(UIGestureRecognizer *)preventingGestureRecognizer { // Don't allow the highlight to be prevented by a selection gesture. Press-and-hold on a link should highlight the link, not select it. bool isForcePressGesture = NO; // FIXME: Likely we can remove this special case for watchOS and tvOS. #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) isForcePressGesture = (preventingGestureRecognizer == _textInteractionAssistant.get().forcePressGesture); #endif #if PLATFORM(MACCATALYST) if ((preventingGestureRecognizer == _textInteractionAssistant.get().loupeGesture) && (preventedGestureRecognizer == _highlightLongPressGestureRecognizer || preventedGestureRecognizer == _longPressGestureRecognizer || preventedGestureRecognizer == _textInteractionAssistant.get().forcePressGesture)) return YES; #endif if ((preventingGestureRecognizer == _textInteractionAssistant.get().loupeGesture || isForcePressGesture) && (preventedGestureRecognizer == _highlightLongPressGestureRecognizer || preventedGestureRecognizer == _longPressGestureRecognizer)) return NO; return YES; } static inline bool isSamePair(UIGestureRecognizer *a, UIGestureRecognizer *b, UIGestureRecognizer *x, UIGestureRecognizer *y) { return (a == x && b == y) || (b == x && a == y); } #if USE(APPLE_INTERNAL_SDK) && __has_include() #import #else - (BOOL)_shouldAdditionallyRecognizeGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer simultaneouslyWithGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { return NO; } #endif - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRecognizeSimultaneouslyWithGestureRecognizer:(UIGestureRecognizer*)otherGestureRecognizer { for (WKDeferringGestureRecognizer *gesture in self.deferringGestures) { if (isSamePair(gestureRecognizer, otherGestureRecognizer, _touchEventGestureRecognizer.get(), gesture)) return YES; } if ([gestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class] && [otherGestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class]) return YES; #if ENABLE(IMAGE_ANALYSIS) if (gestureRecognizer == _imageAnalysisDeferringGestureRecognizer) return ![self shouldDeferGestureDueToImageAnalysis:otherGestureRecognizer]; if (otherGestureRecognizer == _imageAnalysisDeferringGestureRecognizer) return ![self shouldDeferGestureDueToImageAnalysis:gestureRecognizer]; #endif if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _longPressGestureRecognizer.get())) return YES; #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) if ([gestureRecognizer isKindOfClass:[WKMouseGestureRecognizer class]] || [otherGestureRecognizer isKindOfClass:[WKMouseGestureRecognizer class]]) return YES; #endif #if PLATFORM(MACCATALYST) if (isSamePair(gestureRecognizer, otherGestureRecognizer, [_textInteractionAssistant loupeGesture], [_textInteractionAssistant forcePressGesture])) return YES; if (isSamePair(gestureRecognizer, otherGestureRecognizer, _singleTapGestureRecognizer.get(), [_textInteractionAssistant loupeGesture])) return YES; if (([gestureRecognizer isKindOfClass:[_UILookupGestureRecognizer class]] && [otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]) || ([otherGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]] && [gestureRecognizer isKindOfClass:[_UILookupGestureRecognizer class]])) return YES; #endif // PLATFORM(MACCATALYST) if (gestureRecognizer == _highlightLongPressGestureRecognizer.get() || otherGestureRecognizer == _highlightLongPressGestureRecognizer.get()) { auto forcePressGesture = [_textInteractionAssistant forcePressGesture]; if (gestureRecognizer == forcePressGesture || otherGestureRecognizer == forcePressGesture) return YES; auto loupeGesture = [_textInteractionAssistant loupeGesture]; if (gestureRecognizer == loupeGesture || otherGestureRecognizer == loupeGesture) return YES; if (gestureRecognizer._wk_isTapAndAHalf || otherGestureRecognizer._wk_isTapAndAHalf) return YES; } if (isSamePair(gestureRecognizer, otherGestureRecognizer, _singleTapGestureRecognizer.get(), [_textInteractionAssistant singleTapGesture])) return YES; if (isSamePair(gestureRecognizer, otherGestureRecognizer, _singleTapGestureRecognizer.get(), _nonBlockingDoubleTapGestureRecognizer.get())) return YES; if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _nonBlockingDoubleTapGestureRecognizer.get())) return YES; if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _previewSecondaryGestureRecognizer.get())) return YES; if (isSamePair(gestureRecognizer, otherGestureRecognizer, _highlightLongPressGestureRecognizer.get(), _previewGestureRecognizer.get())) return YES; if (isSamePair(gestureRecognizer, otherGestureRecognizer, _nonBlockingDoubleTapGestureRecognizer.get(), _doubleTapGestureRecognizerForDoubleClick.get())) return YES; if (isSamePair(gestureRecognizer, otherGestureRecognizer, _doubleTapGestureRecognizer.get(), _doubleTapGestureRecognizerForDoubleClick.get())) return YES; #if ENABLE(IMAGE_ANALYSIS) if (gestureRecognizer == _imageAnalysisGestureRecognizer || gestureRecognizer == _imageAnalysisTimeoutGestureRecognizer) return YES; #endif if ([self _shouldAdditionallyRecognizeGestureRecognizer:gestureRecognizer simultaneouslyWithGestureRecognizer:otherGestureRecognizer]) return YES; return NO; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldRequireFailureOfGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if (gestureRecognizer == _touchEventGestureRecognizer && [self _touchEventsMustRequireGestureRecognizerToFail:otherGestureRecognizer]) return YES; if ([otherGestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class]) return [(WKDeferringGestureRecognizer *)otherGestureRecognizer shouldDeferGestureRecognizer:gestureRecognizer]; #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS) if (gestureRecognizer == _imageAnalysisTimeoutGestureRecognizer && otherGestureRecognizer == [_contextMenuInteraction gestureRecognizerForFailureRelationships]) return YES; #endif return NO; } - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer shouldBeRequiredToFailByGestureRecognizer:(UIGestureRecognizer *)otherGestureRecognizer { if ([gestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class]) return [(WKDeferringGestureRecognizer *)gestureRecognizer shouldDeferGestureRecognizer:otherGestureRecognizer]; #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS) if (gestureRecognizer == [_contextMenuInteraction gestureRecognizerForFailureRelationships] && otherGestureRecognizer == _imageAnalysisTimeoutGestureRecognizer) return YES; #endif return NO; } - (void)_showImageSheet { [_actionSheetAssistant showImageSheet]; } - (void)_showAttachmentSheet { id uiDelegate = static_cast>([_webView UIDelegate]); if (![uiDelegate respondsToSelector:@selector(_webView:showCustomSheetForElement:)]) return; auto element = adoptNS([[_WKActivatedElementInfo alloc] _initWithType:_WKActivatedElementTypeAttachment URL:(NSURL *)_positionInformation.url imageURL:(NSURL *)_positionInformation.imageURL location:_positionInformation.request.point title:_positionInformation.title ID:_positionInformation.idAttribute rect:_positionInformation.bounds image:nil imageMIMEType:_positionInformation.imageMIMEType]); ALLOW_DEPRECATED_DECLARATIONS_BEGIN [uiDelegate _webView:self.webView showCustomSheetForElement:element.get()]; ALLOW_DEPRECATED_DECLARATIONS_END } - (void)_showLinkSheet { [_actionSheetAssistant showLinkSheet]; } - (void)_showDataDetectorsUI { [self _showDataDetectorsUIForPositionInformation:_positionInformation]; } - (void)_showDataDetectorsUIForPositionInformation:(const WebKit::InteractionInformationAtPosition&)positionInformation { [_actionSheetAssistant showDataDetectorsUIForPositionInformation:positionInformation]; } - (SEL)_actionForLongPressFromPositionInformation:(const WebKit::InteractionInformationAtPosition&)positionInformation { if (!self.webView.configuration._longPressActionsEnabled) return nil; if (!positionInformation.touchCalloutEnabled) return nil; if (positionInformation.isImage) return @selector(_showImageSheet); if (positionInformation.isLink) { #if ENABLE(DATA_DETECTION) if (WebCore::DataDetection::canBePresentedByDataDetectors(positionInformation.url)) return @selector(_showDataDetectorsUI); #endif return @selector(_showLinkSheet); } if (positionInformation.isAttachment) return @selector(_showAttachmentSheet); return nil; } - (SEL)_actionForLongPress { return [self _actionForLongPressFromPositionInformation:_positionInformation]; } - (void)doAfterPositionInformationUpdate:(void (^)(WebKit::InteractionInformationAtPosition))action forRequest:(WebKit::InteractionInformationRequest)request { if ([self _currentPositionInformationIsValidForRequest:request]) { // If the most recent position information is already valid, invoke the given action block immediately. action(_positionInformation); return; } _pendingPositionInformationHandlers.append(InteractionInformationRequestAndCallback(request, action)); if (![self _hasValidOutstandingPositionInformationRequest:request]) [self requestAsynchronousPositionInformationUpdate:request]; } - (BOOL)ensurePositionInformationIsUpToDate:(WebKit::InteractionInformationRequest)request { if ([self _currentPositionInformationIsValidForRequest:request]) return YES; if (!_page->hasRunningProcess()) return NO; auto* connection = _page->process().connection(); if (!connection) return NO; if (_isWaitingOnPositionInformation) return NO; _isWaitingOnPositionInformation = YES; if (![self _hasValidOutstandingPositionInformationRequest:request]) [self requestAsynchronousPositionInformationUpdate:request]; bool receivedResponse = connection->waitForAndDispatchImmediately(_page->webPageID(), 1_s, IPC::WaitForOption::InterruptWaitingIfSyncMessageArrives); _hasValidPositionInformation = receivedResponse && _positionInformation.canBeValid; return _hasValidPositionInformation; } - (void)requestAsynchronousPositionInformationUpdate:(WebKit::InteractionInformationRequest)request { if ([self _currentPositionInformationIsValidForRequest:request]) return; _lastOutstandingPositionInformationRequest = request; _page->requestPositionInformation(request); } - (BOOL)_currentPositionInformationIsValidForRequest:(const WebKit::InteractionInformationRequest&)request { return _hasValidPositionInformation && _positionInformation.request.isValidForRequest(request); } - (BOOL)_hasValidOutstandingPositionInformationRequest:(const WebKit::InteractionInformationRequest&)request { return _lastOutstandingPositionInformationRequest && _lastOutstandingPositionInformationRequest->isValidForRequest(request); } - (BOOL)_currentPositionInformationIsApproximatelyValidForRequest:(const WebKit::InteractionInformationRequest&)request radiusForApproximation:(int)radius { return _hasValidPositionInformation && _positionInformation.request.isApproximatelyValidForRequest(request, radius); } - (void)_invokeAndRemovePendingHandlersValidForCurrentPositionInformation { ASSERT(_hasValidPositionInformation); // FIXME: We need to clean up these handlers in the event that we are not able to collect data, or if the WebProcess crashes. ++_positionInformationCallbackDepth; auto updatedPositionInformation = _positionInformation; for (size_t index = 0; index < _pendingPositionInformationHandlers.size(); ++index) { auto requestAndHandler = _pendingPositionInformationHandlers[index]; if (!requestAndHandler) continue; if (![self _currentPositionInformationIsValidForRequest:requestAndHandler->first]) continue; _pendingPositionInformationHandlers[index] = std::nullopt; if (requestAndHandler->second) requestAndHandler->second(updatedPositionInformation); } if (--_positionInformationCallbackDepth) return; for (int index = _pendingPositionInformationHandlers.size() - 1; index >= 0; --index) { if (!_pendingPositionInformationHandlers[index]) _pendingPositionInformationHandlers.remove(index); } } #if ENABLE(DATA_DETECTION) - (NSArray *)_dataDetectionResults { return _page->dataDetectionResults(); } #endif - (NSArray *)_uiTextSelectionRects { NSMutableArray *textSelectionRects = [NSMutableArray array]; if (_textInteractionAssistant) { for (WKTextSelectionRect *selectionRect in [_textInteractionAssistant valueForKeyPath:@"selectionView.selection.selectionRects"]) [textSelectionRects addObject:[NSValue valueWithCGRect:selectionRect.rect]]; } return textSelectionRects; } - (BOOL)_pointIsInsideSelectionRect:(CGPoint)point outBoundingRect:(WebCore::FloatRect *)outBoundingRect { BOOL pointIsInSelectionRect = NO; for (auto& rectInfo : _lastSelectionDrawingInfo.selectionGeometries) { auto rect = rectInfo.rect(); if (rect.isEmpty()) continue; pointIsInSelectionRect |= rect.contains(WebCore::roundedIntPoint(point)); if (outBoundingRect) outBoundingRect->unite(rect); } return pointIsInSelectionRect; } - (BOOL)_shouldToggleSelectionCommandsAfterTapAt:(CGPoint)point { if (_lastSelectionDrawingInfo.selectionGeometries.isEmpty()) return NO; WebCore::FloatRect selectionBoundingRect; BOOL pointIsInSelectionRect = [self _pointIsInsideSelectionRect:point outBoundingRect:&selectionBoundingRect]; WebCore::FloatRect unobscuredContentRect = self.unobscuredContentRect; selectionBoundingRect.intersect(unobscuredContentRect); float unobscuredArea = unobscuredContentRect.area(); float ratioForConsideringSelectionRectToCoverVastMajorityOfContent = 0.75; if (!unobscuredArea || selectionBoundingRect.area() / unobscuredArea > ratioForConsideringSelectionRectToCoverVastMajorityOfContent) return NO; return pointIsInSelectionRect; } - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer *)gestureRecognizer { CGPoint point = [gestureRecognizer locationInView:self]; auto isInterruptingDecelerationForScrollViewOrAncestor = [&] (UIScrollView *scrollView) { UIScrollView *mainScroller = self.webView.scrollView; UIView *view = scrollView ?: mainScroller; while (view) { if ([view isKindOfClass:UIScrollView.class] && [(UIScrollView *)view _isInterruptingDeceleration]) return YES; if (mainScroller == view) break; view = view.superview; } return NO; }; if (gestureRecognizer == _singleTapGestureRecognizer) { if ([self _shouldToggleSelectionCommandsAfterTapAt:point]) return NO; return !isInterruptingDecelerationForScrollViewOrAncestor([_singleTapGestureRecognizer lastTouchedScrollView]); } if (gestureRecognizer == _doubleTapGestureRecognizerForDoubleClick) { // Do not start the double-tap-for-double-click gesture recognizer unless we've got a dblclick event handler on the node at the tap location. WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(point)); if (![self _currentPositionInformationIsApproximatelyValidForRequest:request radiusForApproximation:[_doubleTapGestureRecognizerForDoubleClick allowableMovement]]) { if (![self ensurePositionInformationIsUpToDate:request]) return NO; } return _positionInformation.nodeAtPositionHasDoubleClickHandler.value_or(false); } if (gestureRecognizer == _highlightLongPressGestureRecognizer || gestureRecognizer == _doubleTapGestureRecognizer || gestureRecognizer == _nonBlockingDoubleTapGestureRecognizer || gestureRecognizer == _twoFingerDoubleTapGestureRecognizer) { if (self._hasFocusedElement) { // Request information about the position with sync message. // If the focused element is the same, prevent the gesture. if (![self ensurePositionInformationIsUpToDate:WebKit::InteractionInformationRequest(WebCore::roundedIntPoint(point))]) return NO; if (_positionInformation.elementContext && _positionInformation.elementContext->isSameElement(_focusedElementInformation.elementContext)) return NO; } } if (gestureRecognizer == _highlightLongPressGestureRecognizer) { if (isInterruptingDecelerationForScrollViewOrAncestor([_highlightLongPressGestureRecognizer lastTouchedScrollView])) return NO; if (self._hasFocusedElement) { // This is a different element than the focused one. // Prevent the gesture if there is no node. // Allow the gesture if it is a node that wants highlight or if there is an action for it. if (!_positionInformation.isElement) return NO; return [self _actionForLongPress] != nil; } // We still have no idea about what is at the location. // Send an async message to find out. _hasValidPositionInformation = NO; WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(point)); // If 3D Touch is enabled, asynchronously collect snapshots in the hopes that // they'll arrive before we have to synchronously request them in // _interactionShouldBeginFromPreviewItemController. if (self.traitCollection.forceTouchCapability == UIForceTouchCapabilityAvailable) { request.includeSnapshot = true; request.includeLinkIndicator = true; request.linkIndicatorShouldHaveLegacyMargins = !self._shouldUseContextMenus; } [self requestAsynchronousPositionInformationUpdate:request]; return YES; } if (gestureRecognizer == _longPressGestureRecognizer) { // Use the information retrieved with one of the previous calls // to gestureRecognizerShouldBegin. // Force a sync call if not ready yet. WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(point)); if (![self ensurePositionInformationIsUpToDate:request]) return NO; if (self._hasFocusedElement) { // Prevent the gesture if it is the same node. if (_positionInformation.elementContext && _positionInformation.elementContext->isSameElement(_focusedElementInformation.elementContext)) return NO; } else { // Prevent the gesture if there is no action for the node. return [self _actionForLongPress] != nil; } } return YES; } - (void)_cancelInteraction { _isTapHighlightIDValid = NO; [_highlightView removeFromSuperview]; } - (void)_finishInteraction { _isTapHighlightIDValid = NO; [self _fadeTapHighlightViewIfNeeded]; } - (void)_fadeTapHighlightViewIfNeeded { if (![_highlightView superview] || _isTapHighlightFading) return; _isTapHighlightFading = YES; CGFloat tapHighlightFadeDuration = _showDebugTapHighlightsForFastClicking ? 0.25 : 0.1; [UIView animateWithDuration:tapHighlightFadeDuration animations:^{ [_highlightView layer].opacity = 0; } completion:^(BOOL finished) { if (finished) [_highlightView removeFromSuperview]; _isTapHighlightFading = NO; }]; } - (BOOL)canShowNonEmptySelectionView { if (_suppressSelectionAssistantReasons) return NO; auto& state = _page->editorState(); return !state.isMissingPostLayoutData && !state.selectionIsNone; } - (BOOL)hasSelectablePositionAtPoint:(CGPoint)point { #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) if ([_imageAnalysisInteraction interactableItemExistsAtPoint:point]) return NO; #endif ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (!self.webView.configuration._textInteractionGesturesEnabled) return NO; ALLOW_DEPRECATED_DECLARATIONS_END if (!_page->preferences().textInteractionEnabled()) return NO; if (_suppressSelectionAssistantReasons) return NO; if (_inspectorNodeSearchEnabled) return NO; WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(point)); if (![self ensurePositionInformationIsUpToDate:request]) return NO; #if ENABLE(IMAGE_ANALYSIS) if (_elementPendingImageAnalysis && _positionInformation.hostImageOrVideoElementContext == _elementPendingImageAnalysis) return YES; #endif return _positionInformation.isSelectable(); } - (BOOL)pointIsNearMarkedText:(CGPoint)point { #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) if ([_imageAnalysisInteraction interactableItemExistsAtPoint:point]) return NO; #endif ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (!self.webView.configuration._textInteractionGesturesEnabled) return NO; ALLOW_DEPRECATED_DECLARATIONS_END if (!_page->preferences().textInteractionEnabled()) return NO; if (_suppressSelectionAssistantReasons) return NO; WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(point)); if (![self ensurePositionInformationIsUpToDate:request]) return NO; return _positionInformation.isNearMarkedText; } - (BOOL)textInteractionGesture:(UIWKGestureType)gesture shouldBeginAtPoint:(CGPoint)point { #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) if ([_imageAnalysisInteraction interactableItemExistsAtPoint:point]) return NO; #endif ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (!self.webView.configuration._textInteractionGesturesEnabled) return NO; ALLOW_DEPRECATED_DECLARATIONS_END if (!_page->preferences().textInteractionEnabled()) return NO; if (_domPasteRequestHandler) return NO; if (_suppressSelectionAssistantReasons) return NO; if (!self.isFocusingElement) { if (gesture == UIWKGestureDoubleTap) { // Don't allow double tap text gestures in noneditable content. return NO; } if (gesture == UIWKGestureOneFingerTap) { ASSERT(_suppressNonEditableSingleTapTextInteractionCount >= 0); if (_suppressNonEditableSingleTapTextInteractionCount > 0) return NO; switch ([_textInteractionAssistant loupeGesture].state) { case UIGestureRecognizerStateBegan: case UIGestureRecognizerStateChanged: case UIGestureRecognizerStateEnded: { // Avoid handling one-finger taps while the web process is processing certain selection changes. // This works around a scenario where UIKeyboardImpl blocks the main thread while handling a one- // finger tap, which subsequently prevents the UI process from handling any incoming IPC messages. return NO; } default: return _page->editorState().selectionIsRange; } } } WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(point)); if (![self ensurePositionInformationIsUpToDate:request]) return NO; if (gesture == UIWKGestureLoupe && _positionInformation.selectability == WebKit::InteractionInformationAtPosition::Selectability::UnselectableDueToUserSelectNone) return NO; #if ENABLE(DATALIST_ELEMENT) if (_positionInformation.preventTextInteraction) return NO; #endif #if ENABLE(IMAGE_ANALYSIS) if (_elementPendingImageAnalysis && _positionInformation.hostImageOrVideoElementContext == _elementPendingImageAnalysis) return YES; #endif // If we're currently focusing an editable element, only allow the selection to move within that focused element. if (self.isFocusingElement) return _positionInformation.elementContext && _positionInformation.elementContext->isSameElement(_focusedElementInformation.elementContext); if (_positionInformation.prefersDraggingOverTextSelection) return NO; // If we're selecting something, don't activate highlight. if (gesture == UIWKGestureLoupe && [self hasSelectablePositionAtPoint:point]) [self _cancelLongPressGestureRecognizer]; // Otherwise, if we're using a text interaction assistant outside of editing purposes (e.g. the selection mode // is character granularity) then allow text selection. return YES; } - (NSArray *)webSelectionRectsForSelectionGeometries:(const Vector&)selectionGeometries { if (selectionGeometries.isEmpty()) return nil; return createNSArray(selectionGeometries, [] (auto& geometry) { auto webRect = [WebSelectionRect selectionRect]; webRect.rect = geometry.rect(); webRect.writingDirection = geometry.direction() == WebCore::TextDirection::LTR ? WKWritingDirectionLeftToRight : WKWritingDirectionRightToLeft; webRect.isLineBreak = geometry.isLineBreak(); webRect.isFirstOnLine = geometry.isFirstOnLine(); webRect.isLastOnLine = geometry.isLastOnLine(); webRect.containsStart = geometry.containsStart(); webRect.containsEnd = geometry.containsEnd(); webRect.isInFixedPosition = geometry.isInFixedPosition(); webRect.isHorizontal = geometry.isHorizontal(); return webRect; }).autorelease(); } - (NSArray *)webSelectionRects { if (_page->editorState().isMissingPostLayoutData || _page->editorState().selectionIsNone) return nil; const auto& selectionGeometries = _page->editorState().postLayoutData().selectionGeometries; return [self webSelectionRectsForSelectionGeometries:selectionGeometries]; } - (WebKit::TapIdentifier)nextTapIdentifier { _latestTapID = WebKit::TapIdentifier::generate(); return _latestTapID; } - (void)_highlightLongPressRecognized:(UILongPressGestureRecognizer *)gestureRecognizer { ASSERT(gestureRecognizer == _highlightLongPressGestureRecognizer); [self _resetIsDoubleTapPending]; auto startPoint = [gestureRecognizer startPoint]; _lastInteractionLocation = startPoint; switch ([gestureRecognizer state]) { case UIGestureRecognizerStateBegan: _longPressCanClick = YES; cancelPotentialTapIfNecessary(self); _page->tapHighlightAtPosition(startPoint, [self nextTapIdentifier]); _isTapHighlightIDValid = YES; break; case UIGestureRecognizerStateEnded: if (_longPressCanClick && _positionInformation.isElement) { [self _attemptSyntheticClickAtLocation:startPoint modifierFlags:gestureRecognizer.modifierFlags]; [self _finishInteraction]; } else [self _cancelInteraction]; _longPressCanClick = NO; break; case UIGestureRecognizerStateCancelled: [self _cancelInteraction]; _longPressCanClick = NO; break; default: break; } } - (void)_doubleTapRecognizedForDoubleClick:(UITapGestureRecognizer *)gestureRecognizer { _page->handleDoubleTapForDoubleClickAtPoint(WebCore::IntPoint([gestureRecognizer locationInView:self]), WebKit::webEventModifierFlags(gestureRecognizer.modifierFlags), _layerTreeTransactionIdAtLastInteractionStart); } - (void)_twoFingerSingleTapGestureRecognized:(UITapGestureRecognizer *)gestureRecognizer { _isTapHighlightIDValid = YES; _isExpectingFastSingleTapCommit = YES; _page->handleTwoFingerTapAtPoint(WebCore::roundedIntPoint([gestureRecognizer locationInView:self]), WebKit::webEventModifierFlags(gestureRecognizer.modifierFlags | UIKeyModifierCommand), [self nextTapIdentifier]); } - (void)_longPressRecognized:(UILongPressGestureRecognizer *)gestureRecognizer { ASSERT(gestureRecognizer == _longPressGestureRecognizer); [self _resetIsDoubleTapPending]; [self _cancelTouchEventGestureRecognizer]; _page->didRecognizeLongPress(); _lastInteractionLocation = [gestureRecognizer startPoint]; if ([gestureRecognizer state] == UIGestureRecognizerStateBegan) { SEL action = [self _actionForLongPress]; if (action) { [self performSelector:action]; [self _cancelLongPressGestureRecognizer]; } } } - (void)_endPotentialTapAndEnableDoubleTapGesturesIfNecessary { if (self.webView._allowsDoubleTapGestures) { RELEASE_LOG(ViewGestures, "ending potential tap - double taps are back. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); [self _setDoubleTapGesturesEnabled:YES]; } RELEASE_LOG(ViewGestures, "Ending potential tap. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); _potentialTapInProgress = NO; } - (void)_singleTapIdentified:(UITapGestureRecognizer *)gestureRecognizer { auto position = [gestureRecognizer locationInView:self]; #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) if ([self _handleTapOverImageAnalysisInteractionButton:position]) return; #endif ASSERT(gestureRecognizer == _singleTapGestureRecognizer); ASSERT(!_potentialTapInProgress); [self _resetIsDoubleTapPending]; [_inputPeripheral setSingleTapShouldEndEditing:[_inputPeripheral isEditing]]; bool shouldRequestMagnificationInformation = _page->preferences().fasterClicksEnabled(); if (shouldRequestMagnificationInformation) RELEASE_LOG(ViewGestures, "Single tap identified. Request details on potential zoom. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); _page->potentialTapAtPosition(position, shouldRequestMagnificationInformation, [self nextTapIdentifier]); _potentialTapInProgress = YES; _isTapHighlightIDValid = YES; _isExpectingFastSingleTapCommit = !_doubleTapGestureRecognizer.get().enabled; } static void cancelPotentialTapIfNecessary(WKContentView* contentView) { if (contentView->_potentialTapInProgress) { [contentView _endPotentialTapAndEnableDoubleTapGesturesIfNecessary]; [contentView _cancelInteraction]; contentView->_page->cancelPotentialTap(); } } - (void)_singleTapDidReset:(UITapGestureRecognizer *)gestureRecognizer { ASSERT(gestureRecognizer == _singleTapGestureRecognizer); cancelPotentialTapIfNecessary(self); if (auto* singleTapTouchIdentifier = [_singleTapGestureRecognizer lastActiveTouchIdentifier]) { WebCore::PointerID pointerId = [singleTapTouchIdentifier unsignedIntValue]; if (_commitPotentialTapPointerId != pointerId) _page->touchWithIdentifierWasRemoved(pointerId); } if (!_isTapHighlightIDValid) [self _fadeTapHighlightViewIfNeeded]; } - (void)_doubleTapDidFail:(UITapGestureRecognizer *)gestureRecognizer { RELEASE_LOG(ViewGestures, "Double tap was not recognized. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); ASSERT(gestureRecognizer == _doubleTapGestureRecognizer); } - (void)_commitPotentialTapFailed { _page->touchWithIdentifierWasRemoved(_commitPotentialTapPointerId); _commitPotentialTapPointerId = 0; [self _cancelInteraction]; [self _resetInputViewDeferral]; } - (void)_didNotHandleTapAsClick:(const WebCore::IntPoint&)point { [self _resetInputViewDeferral]; if (!_isDoubleTapPending) return; _smartMagnificationController->handleSmartMagnificationGesture(_lastInteractionLocation); _isDoubleTapPending = NO; } - (void)_didCompleteSyntheticClick { _page->touchWithIdentifierWasRemoved(_commitPotentialTapPointerId); _commitPotentialTapPointerId = 0; RELEASE_LOG(ViewGestures, "Synthetic click completed. (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); [self _resetInputViewDeferral]; } - (void)_singleTapRecognized:(UITapGestureRecognizer *)gestureRecognizer { ASSERT(gestureRecognizer == _singleTapGestureRecognizer); if (![self isFirstResponder]) [self becomeFirstResponder]; ASSERT(_potentialTapInProgress); _lastInteractionLocation = [gestureRecognizer locationInView:self]; [self _endPotentialTapAndEnableDoubleTapGesturesIfNecessary]; if (_hasTapHighlightForPotentialTap) { [self _showTapHighlight]; _hasTapHighlightForPotentialTap = NO; } if ([_inputPeripheral singleTapShouldEndEditing]) [_inputPeripheral endEditing]; RELEASE_LOG(ViewGestures, "Single tap recognized - commit potential tap (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); WebCore::PointerID pointerId = WebCore::mousePointerID; if (auto* singleTapTouchIdentifier = [_singleTapGestureRecognizer lastActiveTouchIdentifier]) { pointerId = [singleTapTouchIdentifier unsignedIntValue]; _commitPotentialTapPointerId = pointerId; } _page->commitPotentialTap(WebKit::webEventModifierFlags(gestureRecognizer.modifierFlags), _layerTreeTransactionIdAtLastInteractionStart, pointerId); if (!_isExpectingFastSingleTapCommit) [self _finishInteraction]; #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) if (![_imageAnalysisInteraction interactableItemExistsAtPoint:_lastInteractionLocation]) [_imageAnalysisInteraction resetSelection]; #endif } - (void)_doubleTapRecognized:(UITapGestureRecognizer *)gestureRecognizer { RELEASE_LOG(ViewGestures, "Identified a double tap (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); [self _resetIsDoubleTapPending]; auto location = [gestureRecognizer locationInView:self]; _lastInteractionLocation = location; _smartMagnificationController->handleSmartMagnificationGesture(location); } - (void)_resetIsDoubleTapPending { _isDoubleTapPending = NO; } - (void)_nonBlockingDoubleTapRecognized:(UITapGestureRecognizer *)gestureRecognizer { _lastInteractionLocation = [gestureRecognizer locationInView:self]; _isDoubleTapPending = YES; } - (void)_twoFingerDoubleTapRecognized:(UITapGestureRecognizer *)gestureRecognizer { [self _resetIsDoubleTapPending]; auto location = [gestureRecognizer locationInView:self]; _lastInteractionLocation = location; _smartMagnificationController->handleResetMagnificationGesture(location); } - (void)_attemptSyntheticClickAtLocation:(CGPoint)location modifierFlags:(UIKeyModifierFlags)modifierFlags { if (![self isFirstResponder]) [self becomeFirstResponder]; [_inputPeripheral endEditing]; _page->attemptSyntheticClick(location, WebKit::webEventModifierFlags(modifierFlags), _layerTreeTransactionIdAtLastInteractionStart); } - (void)setUpTextSelectionAssistant { if (!_textInteractionAssistant) _textInteractionAssistant = adoptNS([[UIWKTextInteractionAssistant alloc] initWithView:self]); else { // Reset the gesture recognizers in case editability has changed. [_textInteractionAssistant setGestureRecognizers]; } } - (void)pasteWithCompletionHandler:(void (^)(void))completionHandler { _page->executeEditCommand("Paste"_s, { }, [completion = makeBlockPtr(completionHandler)] { if (completion) completion(); }); } - (void)clearSelection { [self _elementDidBlur]; _page->clearSelection(); } - (void)_invalidateCurrentPositionInformation { _hasValidPositionInformation = NO; _positionInformation = { }; } - (void)_positionInformationDidChange:(const WebKit::InteractionInformationAtPosition&)info { if (_lastOutstandingPositionInformationRequest && info.request.isValidForRequest(*_lastOutstandingPositionInformationRequest)) _lastOutstandingPositionInformationRequest = std::nullopt; _isWaitingOnPositionInformation = NO; WebKit::InteractionInformationAtPosition newInfo = info; newInfo.mergeCompatibleOptionalInformation(_positionInformation); _positionInformation = newInfo; _hasValidPositionInformation = _positionInformation.canBeValid; if (_actionSheetAssistant) [_actionSheetAssistant updateSheetPosition]; [self _invokeAndRemovePendingHandlersValidForCurrentPositionInformation]; } - (void)_willStartScrollingOrZooming { [_textInteractionAssistant willStartScrollingOrZooming]; _page->setIsScrollingOrZooming(true); #if HAVE(PEPPER_UI_CORE) [_focusedFormControlView disengageFocusedFormControlNavigation]; #endif } - (void)scrollViewWillStartPanOrPinchGesture { _page->hideValidationMessage(); [_keyboardScrollingAnimator willStartInteractiveScroll]; _touchEventsCanPreventNativeGestures = NO; } - (void)_didEndScrollingOrZooming { if (!_needsDeferredEndScrollingSelectionUpdate) [_textInteractionAssistant didEndScrollingOrZooming]; _page->setIsScrollingOrZooming(false); [self _resetPanningPreventionFlags]; #if HAVE(PEPPER_UI_CORE) [_focusedFormControlView engageFocusedFormControlNavigation]; #endif } - (BOOL)_elementTypeRequiresAccessoryView:(WebKit::InputType)type { switch (type) { case WebKit::InputType::None: #if ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Color: #endif case WebKit::InputType::Drawing: case WebKit::InputType::Date: case WebKit::InputType::DateTimeLocal: case WebKit::InputType::Month: case WebKit::InputType::Time: return NO; case WebKit::InputType::Select: { #if ENABLE(IOS_FORM_CONTROL_REFRESH) if (self._shouldUseContextMenusForFormControls) return NO; #endif return WebKit::currentUserInterfaceIdiomIsSmallScreen(); } case WebKit::InputType::Text: case WebKit::InputType::Password: case WebKit::InputType::Search: case WebKit::InputType::Email: case WebKit::InputType::URL: case WebKit::InputType::Phone: case WebKit::InputType::Number: case WebKit::InputType::NumberPad: case WebKit::InputType::ContentEditable: case WebKit::InputType::TextArea: case WebKit::InputType::Week: return WebKit::currentUserInterfaceIdiomIsSmallScreen(); } } - (BOOL)requiresAccessoryView { if ([_formInputSession accessoryViewShouldNotShow]) return NO; if ([_formInputSession customInputAccessoryView]) return YES; return [self _elementTypeRequiresAccessoryView:_focusedElementInformation.elementType]; } - (UITextInputAssistantItem *)inputAssistantItem { return [_webView inputAssistantItem]; } - (UITextInputAssistantItem *)inputAssistantItemForWebView { return [super inputAssistantItem]; } - (UIView *)inputAccessoryView { return [_webView inputAccessoryView]; } - (UIView *)inputAccessoryViewForWebView { if (![self requiresAccessoryView]) return nil; return [_formInputSession customInputAccessoryView] ?: self.formAccessoryView; } - (NSArray *)supportedPasteboardTypesForCurrentSelection { if (_page->editorState().selectionIsNone) return nil; static NeverDestroyed plainTextTypes = [] { auto plainTextTypes = adoptNS([[NSMutableArray alloc] init]); ALLOW_DEPRECATED_DECLARATIONS_BEGIN [plainTextTypes addObject:(id)kUTTypeURL]; ALLOW_DEPRECATED_DECLARATIONS_END [plainTextTypes addObjectsFromArray:UIPasteboardTypeListString]; return plainTextTypes; }(); static NeverDestroyed richTypes = [] { auto richTypes = adoptNS([[NSMutableArray alloc] init]); [richTypes addObject:WebCore::WebArchivePboardType]; [richTypes addObjectsFromArray:UIPasteboardTypeListImage]; [richTypes addObjectsFromArray:plainTextTypes.get().get()]; return richTypes; }(); return (_page->editorState().isContentRichlyEditable) ? richTypes.get().get() : plainTextTypes.get().get(); } #define FORWARD_ACTION_TO_WKWEBVIEW(_action) \ - (void)_action:(id)sender \ { \ SEL action = @selector(_action:);\ [self _willPerformAction:action sender:sender];\ [_webView _action:sender]; \ [self _didPerformAction:action sender:sender];\ } FOR_EACH_WKCONTENTVIEW_ACTION(FORWARD_ACTION_TO_WKWEBVIEW) FOR_EACH_PRIVATE_WKCONTENTVIEW_ACTION(FORWARD_ACTION_TO_WKWEBVIEW) #undef FORWARD_ACTION_TO_WKWEBVIEW - (void)_lookupForWebView:(id)sender { _page->getSelectionContext([view = retainPtr(self)](const String& selectedText, const String& textBefore, const String& textAfter) { if (!selectedText) return; auto& editorState = view->_page->editorState(); auto& postLayoutData = editorState.postLayoutData(); CGRect presentationRect; if (editorState.selectionIsRange && !postLayoutData.selectionGeometries.isEmpty()) presentationRect = view->_page->selectionBoundingRectInRootViewCoordinates(); else presentationRect = postLayoutData.caretRectAtStart; String selectionContext = textBefore + selectedText + textAfter; NSRange selectedRangeInContext = NSMakeRange(textBefore.length(), selectedText.length()); if (auto textSelectionAssistant = view->_textInteractionAssistant) [textSelectionAssistant lookup:selectionContext withRange:selectedRangeInContext fromRect:presentationRect]; }); } - (void)_shareForWebView:(id)sender { RetainPtr view = self; _page->getSelectionOrContentsAsString([view](const String& string) { if (!view->_textInteractionAssistant || !string || view->_page->editorState().isMissingPostLayoutData) return; auto& selectionGeometries = view->_page->editorState().postLayoutData().selectionGeometries; if (selectionGeometries.isEmpty()) return; [view->_textInteractionAssistant showShareSheetFor:string fromRect:selectionGeometries.first().rect()]; }); } - (void)_translateForWebView:(id)sender { _page->getSelectionOrContentsAsString([weakSelf = WeakObjCPtr(self)] (const String& string) { if (!weakSelf) return; if (string.isEmpty()) return; auto strongSelf = weakSelf.get(); if (strongSelf->_page->editorState().isMissingPostLayoutData) return; if (strongSelf->_page->editorState().postLayoutData().selectionGeometries.isEmpty()) return; if ([strongSelf->_textInteractionAssistant respondsToSelector:@selector(translate:fromRect:)]) [strongSelf->_textInteractionAssistant translate:string fromRect:strongSelf->_page->selectionBoundingRectInRootViewCoordinates()]; }); } - (void)_addShortcutForWebView:(id)sender { [_textInteractionAssistant showTextServiceFor:[self selectedText] fromRect:_page->editorState().postLayoutData().selectionGeometries[0].rect()]; } - (NSString *)selectedText { return (NSString *)_page->editorState().postLayoutData().wordAtSelection; } - (NSArray *)alternativesForSelectedText { auto& dictationContextsForSelection = _page->editorState().postLayoutData().dictationContextsForSelection; return createNSArray(dictationContextsForSelection, [&] (auto& dictationContext) { return _page->platformDictationAlternatives(dictationContext); }).autorelease(); } - (void)makeTextWritingDirectionNaturalForWebView:(id)sender { // Match platform behavior on iOS as well as legacy WebKit behavior by modifying the // base (paragraph) writing direction rather than the inline direction. _page->setBaseWritingDirection(WebCore::WritingDirection::Natural); } - (void)makeTextWritingDirectionLeftToRightForWebView:(id)sender { _page->setBaseWritingDirection(WebCore::WritingDirection::LeftToRight); } - (void)makeTextWritingDirectionRightToLeftForWebView:(id)sender { _page->setBaseWritingDirection(WebCore::WritingDirection::RightToLeft); } - (BOOL)isReplaceAllowed { return _page->editorState().postLayoutData().isReplaceAllowed; } - (void)replaceText:(NSString *)text withText:(NSString *)word { _autocorrectionContextNeedsUpdate = YES; _page->replaceSelectedText(text, word); } - (void)selectWordBackward { _autocorrectionContextNeedsUpdate = YES; _page->selectWordBackward(); } - (void)_promptForReplaceForWebView:(id)sender { const auto& wordAtSelection = _page->editorState().postLayoutData().wordAtSelection; if (wordAtSelection.isEmpty()) return; [_textInteractionAssistant scheduleReplacementsForText:wordAtSelection]; } - (void)_transliterateChineseForWebView:(id)sender { [_textInteractionAssistant scheduleChineseTransliterationForText:_page->editorState().postLayoutData().wordAtSelection]; } - (void)replaceForWebView:(id)sender { [[UIKeyboardImpl sharedInstance] replaceText:sender]; } #define WEBCORE_COMMAND_FOR_WEBVIEW(command) \ - (void)_ ## command ## ForWebView:(id)sender { _page->executeEditCommand(#command ## _s); } \ - (void)command ## ForWebView:(id)sender { [self _ ## command ## ForWebView:sender]; } WEBCORE_COMMAND_FOR_WEBVIEW(insertOrderedList); WEBCORE_COMMAND_FOR_WEBVIEW(insertUnorderedList); WEBCORE_COMMAND_FOR_WEBVIEW(insertNestedOrderedList); WEBCORE_COMMAND_FOR_WEBVIEW(insertNestedUnorderedList); WEBCORE_COMMAND_FOR_WEBVIEW(indent); WEBCORE_COMMAND_FOR_WEBVIEW(outdent); WEBCORE_COMMAND_FOR_WEBVIEW(alignLeft); WEBCORE_COMMAND_FOR_WEBVIEW(alignRight); WEBCORE_COMMAND_FOR_WEBVIEW(alignCenter); WEBCORE_COMMAND_FOR_WEBVIEW(alignJustified); WEBCORE_COMMAND_FOR_WEBVIEW(pasteAndMatchStyle); #undef WEBCORE_COMMAND_FOR_WEBVIEW - (void)_increaseListLevelForWebView:(id)sender { _page->increaseListLevel(); } - (void)_decreaseListLevelForWebView:(id)sender { _page->decreaseListLevel(); } - (void)_changeListTypeForWebView:(id)sender { _page->changeListType(); } - (void)_toggleStrikeThroughForWebView:(id)sender { _page->executeEditCommand("StrikeThrough"_s); } - (void)increaseSizeForWebView:(id)sender { _page->executeEditCommand("FontSizeDelta"_s, "1"_s); } - (void)decreaseSizeForWebView:(id)sender { _page->executeEditCommand("FontSizeDelta"_s, "-1"_s); } - (void)_setFontForWebView:(UIFont *)font sender:(id)sender { WebCore::FontChanges changes; changes.setFontFamily(font.familyName); changes.setFontName(font.fontName); changes.setFontSize(font.pointSize); changes.setBold(font.traits & UIFontTraitBold); changes.setItalic(font.traits & UIFontTraitItalic); _page->changeFont(WTFMove(changes)); } - (void)_setFontSizeForWebView:(CGFloat)fontSize sender:(id)sender { WebCore::FontChanges changes; changes.setFontSize(fontSize); _page->changeFont(WTFMove(changes)); } - (void)_setTextColorForWebView:(UIColor *)color sender:(id)sender { WebCore::Color textColor(WebCore::roundAndClampToSRGBALossy(color.CGColor)); _page->executeEditCommand("ForeColor"_s, WebCore::serializationForHTML(textColor)); } - (void)toggleStrikeThroughForWebView:(id)sender { [self _toggleStrikeThroughForWebView:sender]; } - (NSDictionary *)textStylingAtPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction { if (!position || !_page->editorState().isContentRichlyEditable) return nil; NSMutableDictionary* result = [NSMutableDictionary dictionary]; auto typingAttributes = _page->editorState().postLayoutData().typingAttributes; CTFontSymbolicTraits symbolicTraits = 0; if (typingAttributes & WebKit::AttributeBold) symbolicTraits |= kCTFontBoldTrait; if (typingAttributes & WebKit::AttributeItalics) symbolicTraits |= kCTFontTraitItalic; // We chose a random font family and size. // What matters are the traits but the caller expects a font object // in the dictionary for NSFontAttributeName. RetainPtr fontDescriptor = adoptCF(CTFontDescriptorCreateWithNameAndSize(CFSTR("Helvetica"), 10)); if (symbolicTraits) fontDescriptor = adoptCF(CTFontDescriptorCreateCopyWithSymbolicTraits(fontDescriptor.get(), symbolicTraits, symbolicTraits)); RetainPtr font = adoptCF(CTFontCreateWithFontDescriptor(fontDescriptor.get(), 10, nullptr)); if (font) [result setObject:(id)font.get() forKey:NSFontAttributeName]; if (typingAttributes & WebKit::AttributeUnderline) [result setObject:@(NSUnderlineStyleSingle) forKey:NSUnderlineStyleAttributeName]; return result; } - (UIColor *)insertionPointColor { // On macCatalyst we need to explicitly return the color we have calculated, rather than rely on textTraits, as on macCatalyst, UIKit ignores text traits. #if PLATFORM(MACCATALYST) return [self _cascadeInteractionTintColor]; #else return [self.textInputTraits insertionPointColor]; #endif } - (UIColor *)selectionBarColor { return [self.textInputTraits selectionBarColor]; } - (UIColor *)selectionHighlightColor { return [self.textInputTraits selectionHighlightColor]; } - (UIColor *)_cascadeInteractionTintColor { ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (!self.webView.configuration._textInteractionGesturesEnabled) return [UIColor clearColor]; ALLOW_DEPRECATED_DECLARATIONS_END if (!_page->preferences().textInteractionEnabled()) return [UIColor clearColor]; if (!_page->editorState().isMissingPostLayoutData) { if (auto caretColor = _page->editorState().postLayoutData().caretColor; caretColor.isValid()) return cocoaColor(caretColor).autorelease(); } return [self _inheritedInteractionTintColor]; } - (void)_updateInteractionTintColor:(UITextInputTraits *)traits { [traits _setColorsToMatchTintColor:[self _cascadeInteractionTintColor]]; } - (void)tintColorDidChange { [super tintColorDidChange]; BOOL shouldUpdateTextSelection = self.isFirstResponder && [self canShowNonEmptySelectionView]; if (shouldUpdateTextSelection) [_textInteractionAssistant deactivateSelection]; [self _updateInteractionTintColor:_traits.get()]; if (shouldUpdateTextSelection) [_textInteractionAssistant activateSelection]; } #if ENABLE(APP_HIGHLIGHTS) - (BOOL)shouldAllowAppHighlightCreation { auto editorState = _page->editorState(); return editorState.selectionIsRange && !editorState.isContentEditable && !editorState.selectionIsRangeInsideImageOverlay; } #endif // ENABLE(APP_HIGHLIGHTS) - (BOOL)canPerformAction:(SEL)action withSender:(id)sender { if (_domPasteRequestHandler) return action == @selector(paste:); // These are UIKit IPI selectors. We don't want to forward them to the web view. auto editorState = _page->editorState(); if (action == @selector(_moveDown:withHistory:) || action == @selector(_moveLeft:withHistory:) || action == @selector(_moveRight:withHistory:) || action == @selector(_moveToEndOfDocument:withHistory:) || action == @selector(_moveToEndOfLine:withHistory:) || action == @selector(_moveToEndOfParagraph:withHistory:) || action == @selector(_moveToEndOfWord:withHistory:) || action == @selector(_moveToStartOfDocument:withHistory:) || action == @selector(_moveToStartOfLine:withHistory:) || action == @selector(_moveToStartOfParagraph:withHistory:) || action == @selector(_moveToStartOfWord:withHistory:) || action == @selector(_moveUp:withHistory:)) return !editorState.selectionIsNone; if (action == @selector(_deleteByWord) || action == @selector(_deleteForwardByWord) || action == @selector(_deleteForwardAndNotify:) || action == @selector(_deleteToEndOfParagraph) || action == @selector(_deleteToStartOfLine) || action == @selector(_transpose)) return editorState.isContentEditable; return [_webView canPerformAction:action withSender:sender]; } - (BOOL)canPerformActionForWebView:(SEL)action withSender:(id)sender { if (_domPasteRequestHandler) return action == @selector(paste:); if (action == @selector(_nextAccessoryTab:)) return self._hasFocusedElement && _focusedElementInformation.hasNextNode; if (action == @selector(_previousAccessoryTab:)) return self._hasFocusedElement && _focusedElementInformation.hasPreviousNode; auto editorState = _page->editorState(); if (action == @selector(_showTextStyleOptions:)) return editorState.isContentRichlyEditable && editorState.selectionIsRange && !_showingTextStyleOptions; if (_showingTextStyleOptions) return (action == @selector(toggleBoldface:) || action == @selector(toggleItalics:) || action == @selector(toggleUnderline:)); // FIXME: Some of the following checks should be removed once internal clients move to the underscore-prefixed versions. if (action == @selector(toggleBoldface:) || action == @selector(toggleItalics:) || action == @selector(toggleUnderline:) || action == @selector(_toggleStrikeThrough:) || action == @selector(_alignLeft:) || action == @selector(_alignRight:) || action == @selector(_alignCenter:) || action == @selector(_alignJustified:) || action == @selector(_setTextColor:sender:) || action == @selector(_setFont:sender:) || action == @selector(_setFontSize:sender:) || action == @selector(_insertOrderedList:) || action == @selector(_insertUnorderedList:) || action == @selector(_insertNestedOrderedList:) || action == @selector(_insertNestedUnorderedList:) || action == @selector(_increaseListLevel:) || action == @selector(_decreaseListLevel:) || action == @selector(_changeListType:) || action == @selector(_indent:) || action == @selector(_outdent:) || action == @selector(increaseSize:) || action == @selector(decreaseSize:) || action == @selector(makeTextWritingDirectionNatural:)) { // FIXME: This should be more nuanced in the future, rather than returning YES for all richly editable areas. For instance, outdent: should be disabled when the selection is already // at the outermost indentation level. return editorState.isContentRichlyEditable; } if (action == @selector(cut:)) return !editorState.isInPasswordField && editorState.isContentEditable && editorState.selectionIsRange; if (action == @selector(paste:) || action == @selector(_pasteAsQuotation:) || action == @selector(_pasteAndMatchStyle:) || action == @selector(pasteAndMatchStyle:)) { if (editorState.selectionIsNone || !editorState.isContentEditable) return NO; UIPasteboard *pasteboard = [UIPasteboard generalPasteboard]; NSArray *types = [self supportedPasteboardTypesForCurrentSelection]; NSIndexSet *indices = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [pasteboard numberOfItems])]; if ([pasteboard containsPasteboardTypes:types inItemSet:indices]) return YES; #if PLATFORM(IOS) if (editorState.isContentRichlyEditable && self.webView.configuration._attachmentElementEnabled) { for (NSItemProvider *itemProvider in pasteboard.itemProviders) { auto preferredPresentationStyle = itemProvider.preferredPresentationStyle; if (preferredPresentationStyle == UIPreferredPresentationStyleInline) continue; if (preferredPresentationStyle == UIPreferredPresentationStyleUnspecified && !itemProvider.suggestedName.length) continue; if (itemProvider.web_fileUploadContentTypes.count) return YES; } } #endif // PLATFORM(IOS) auto focusedDocumentOrigin = editorState.originIdentifierForPasteboard; if (focusedDocumentOrigin.isEmpty()) return NO; NSArray *allCustomPasteboardData = [pasteboard dataForPasteboardType:@(WebCore::PasteboardCustomData::cocoaType().characters()) inItemSet:indices]; for (NSData *data in allCustomPasteboardData) { auto buffer = WebCore::SharedBuffer::create(data); if (WebCore::PasteboardCustomData::fromSharedBuffer(buffer.get()).origin() == focusedDocumentOrigin) return YES; } return NO; } if (action == @selector(copy:)) { if (editorState.isInPasswordField) return NO; return editorState.selectionIsRange; } if (action == @selector(_define:)) { if (editorState.isInPasswordField || !editorState.selectionIsRange) return NO; NSUInteger textLength = editorState.postLayoutData().selectedTextLength; // FIXME: We should be calling UIReferenceLibraryViewController to check if the length is // acceptable, but the interface takes a string. // if (!textLength || textLength > 200) return NO; #if !PLATFORM(MACCATALYST) if ([[PAL::getMCProfileConnectionClass() sharedConnection] effectiveBoolValueForSetting:PAL::get_ManagedConfiguration_MCFeatureDefinitionLookupAllowed()] == MCRestrictedBoolExplicitNo) return NO; #endif return YES; } if (action == @selector(_lookup:)) { if (editorState.isInPasswordField) return NO; #if !PLATFORM(MACCATALYST) if ([[PAL::getMCProfileConnectionClass() sharedConnection] effectiveBoolValueForSetting:PAL::get_ManagedConfiguration_MCFeatureDefinitionLookupAllowed()] == MCRestrictedBoolExplicitNo) return NO; #endif return editorState.selectionIsRange; } if (action == @selector(_share:)) { if (editorState.isInPasswordField || !editorState.selectionIsRange) return NO; return editorState.postLayoutData().selectedTextLength > 0; } if (action == @selector(_addShortcut:)) { if (editorState.isInPasswordField || !editorState.selectionIsRange) return NO; NSString *selectedText = [self selectedText]; if (![selectedText length]) return NO; if (!UIKeyboardEnabledInputModesAllowOneToManyShortcuts()) return NO; if (![selectedText _containsCJScripts]) return NO; return YES; } if (action == @selector(_promptForReplace:)) { if (!editorState.selectionIsRange || !editorState.postLayoutData().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled]) return NO; if ([[self selectedText] _containsCJScriptsOnly]) return NO; return YES; } if (action == @selector(_transliterateChinese:)) { if (!editorState.selectionIsRange || !editorState.postLayoutData().isReplaceAllowed || ![[UIKeyboardImpl activeInstance] autocorrectSpellingEnabled]) return NO; return UIKeyboardEnabledInputModesAllowChineseTransliterationForText([self selectedText]); } if (action == @selector(_translate:)) return !editorState.isInPasswordField && editorState.selectionIsRange; if (action == @selector(select:)) { // Disable select in password fields so that you can't see word boundaries. return !editorState.isInPasswordField && !editorState.selectionIsRange && self.hasContent; } if (action == @selector(selectAll:)) { ALLOW_DEPRECATED_DECLARATIONS_BEGIN if ([sender isKindOfClass:UIMenuController.class]) { // By platform convention we don't show Select All in the callout menu for a range selection. return !editorState.selectionIsRange && self.hasContent; } ALLOW_DEPRECATED_DECLARATIONS_END return YES; } if (action == @selector(replace:)) return editorState.isContentEditable && !editorState.isInPasswordField; if (action == @selector(makeTextWritingDirectionLeftToRight:) || action == @selector(makeTextWritingDirectionRightToLeft:)) { if (!editorState.isContentEditable) return NO; auto baseWritingDirection = editorState.postLayoutData().baseWritingDirection; if (baseWritingDirection == WebCore::WritingDirection::LeftToRight && !UIKeyboardIsRightToLeftInputModeActive()) { // A keyboard is considered "active" if it is available for the user to switch to. As such, this check prevents // text direction actions from showing up in the case where a user has only added left-to-right keyboards, and // is also not editing right-to-left content. return NO; } if (action == @selector(makeTextWritingDirectionLeftToRight:)) return baseWritingDirection != WebCore::WritingDirection::LeftToRight; return baseWritingDirection != WebCore::WritingDirection::RightToLeft; } #if ENABLE(IMAGE_ANALYSIS) if (action == @selector(captureTextFromCamera:)) { if (!mayContainSelectableText(_focusedElementInformation.elementType) || _focusedElementInformation.isReadOnly) return NO; ALLOW_DEPRECATED_DECLARATIONS_BEGIN if ([sender isKindOfClass:UIMenuController.class] && editorState.selectionIsRange) return NO; ALLOW_DEPRECATED_DECLARATIONS_END } #endif // ENABLE(IMAGE_ANALYSIS) #if HAVE(UIFINDINTERACTION) if (action == @selector(find:) || action == @selector(findNext:) || action == @selector(findPrevious:)) return self.webView._findInteractionEnabled; if (action == @selector(findAndReplace:)) return self.webView._findInteractionEnabled && self.supportsTextReplacement; #endif return [super canPerformAction:action withSender:sender]; } - (id)targetForAction:(SEL)action withSender:(id)sender { return [_webView targetForAction:action withSender:sender]; } - (id)targetForActionForWebView:(SEL)action withSender:(id)sender { return [super targetForAction:action withSender:sender]; } - (void)_willHideMenu:(NSNotification *)notification { [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture]; } - (void)_didHideMenu:(NSNotification *)notification { _showingTextStyleOptions = NO; [_textInteractionAssistant hideTextStyleOptions]; } - (void)_keyboardDidRequestDismissal:(NSNotification *)notification { if (![self isFirstResponder]) return; _keyboardDidRequestDismissal = YES; } - (void)copyForWebView:(id)sender { _page->executeEditCommand("copy"_s); } - (void)cutForWebView:(id)sender { [self executeEditCommandWithCallback:@"cut"]; } - (void)pasteForWebView:(id)sender { ALLOW_DEPRECATED_DECLARATIONS_BEGIN if (sender == UIMenuController.sharedMenuController && [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::GrantedForGesture]) return; ALLOW_DEPRECATED_DECLARATIONS_END _autocorrectionContextNeedsUpdate = YES; _page->executeEditCommand("paste"_s); } - (void)_pasteAsQuotationForWebView:(id)sender { _autocorrectionContextNeedsUpdate = YES; _page->executeEditCommand("PasteAsQuotation"_s); } - (void)selectForWebView:(id)sender { if (!_page->preferences().textInteractionEnabled()) return; _autocorrectionContextNeedsUpdate = YES; [_textInteractionAssistant selectWord]; // We cannot use selectWord command, because we want to be able to select the word even when it is the last in the paragraph. _page->extendSelection(WebCore::TextGranularity::WordGranularity); } - (void)selectAllForWebView:(id)sender { if (!_page->preferences().textInteractionEnabled()) return; _autocorrectionContextNeedsUpdate = YES; [_textInteractionAssistant selectAll:sender]; _page->selectAll(); } - (BOOL)shouldSynthesizeKeyEvents { if (_focusedElementInformation.shouldSynthesizeKeyEventsForEditing && self.hasHiddenContentEditable) return true; return false; } - (void)toggleBoldfaceForWebView:(id)sender { if (!_page->editorState().isContentRichlyEditable) return; [self executeEditCommandWithCallback:@"toggleBold"]; if (self.shouldSynthesizeKeyEvents) _page->generateSyntheticEditingCommand(WebKit::SyntheticEditingCommandType::ToggleBoldface); } - (void)toggleItalicsForWebView:(id)sender { if (!_page->editorState().isContentRichlyEditable) return; [self executeEditCommandWithCallback:@"toggleItalic"]; if (self.shouldSynthesizeKeyEvents) _page->generateSyntheticEditingCommand(WebKit::SyntheticEditingCommandType::ToggleItalic); } - (void)toggleUnderlineForWebView:(id)sender { if (!_page->editorState().isContentRichlyEditable) return; [self executeEditCommandWithCallback:@"toggleUnderline"]; if (self.shouldSynthesizeKeyEvents) _page->generateSyntheticEditingCommand(WebKit::SyntheticEditingCommandType::ToggleUnderline); } - (void)_showTextStyleOptionsForWebView:(id)sender { _showingTextStyleOptions = YES; [_textInteractionAssistant showTextStyleOptions]; } - (void)_showDictionary:(NSString *)text { CGRect presentationRect = _page->editorState().postLayoutData().selectionGeometries[0].rect(); if (_textInteractionAssistant) [_textInteractionAssistant showDictionaryFor:text fromRect:presentationRect]; } - (void)_defineForWebView:(id)sender { [self _lookupForWebView:sender]; } - (void)accessibilityRetrieveSpeakSelectionContent { RetainPtr view = self; RetainPtr webView = _webView.get(); _page->getSelectionOrContentsAsString([view, webView](const String& string) { [webView _accessibilityDidGetSpeakSelectionContent:string]; if ([view respondsToSelector:@selector(accessibilitySpeakSelectionSetContent:)]) [view accessibilitySpeakSelectionSetContent:string]; }); } - (void)_accessibilityRetrieveRectsEnclosingSelectionOffset:(NSInteger)offset withGranularity:(UITextGranularity)granularity { _page->requestRectsForGranularityWithSelectionOffset(toWKTextGranularity(granularity), offset, [view = retainPtr(self), offset, granularity](const Vector& selectionGeometries) { if ([view respondsToSelector:@selector(_accessibilityDidGetSelectionRects:withGranularity:atOffset:)]) [view _accessibilityDidGetSelectionRects:[view webSelectionRectsForSelectionGeometries:selectionGeometries] withGranularity:granularity atOffset:offset]; }); } - (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text { [self _accessibilityRetrieveRectsAtSelectionOffset:offset withText:text completionHandler:nil]; } - (void)_accessibilityRetrieveRectsAtSelectionOffset:(NSInteger)offset withText:(NSString *)text completionHandler:(void (^)(const Vector& geometries))completionHandler { RetainPtr view = self; _page->requestRectsAtSelectionOffsetWithText(offset, text, [view, offset, capturedCompletionHandler = makeBlockPtr(completionHandler)](const Vector& selectionGeometries) { if (capturedCompletionHandler) capturedCompletionHandler(selectionGeometries); if ([view respondsToSelector:@selector(_accessibilityDidGetSelectionRects:withGranularity:atOffset:)]) [view _accessibilityDidGetSelectionRects:[view webSelectionRectsForSelectionGeometries:selectionGeometries] withGranularity:UITextGranularityWord atOffset:offset]; }); } - (void)_accessibilityStoreSelection { _page->storeSelectionForAccessibility(true); } - (void)_accessibilityClearSelection { _page->storeSelectionForAccessibility(false); } static UIPasteboardName pasteboardNameForAccessCategory(WebCore::DOMPasteAccessCategory pasteAccessCategory) { switch (pasteAccessCategory) { case WebCore::DOMPasteAccessCategory::General: case WebCore::DOMPasteAccessCategory::Fonts: return UIPasteboardNameGeneral; } } static UIPasteboard *pasteboardForAccessCategory(WebCore::DOMPasteAccessCategory pasteAccessCategory) { switch (pasteAccessCategory) { case WebCore::DOMPasteAccessCategory::General: case WebCore::DOMPasteAccessCategory::Fonts: return UIPasteboard.generalPasteboard; } } - (BOOL)_handleDOMPasteRequestWithResult:(WebCore::DOMPasteAccessResponse)response { if (auto pasteAccessCategory = std::exchange(_domPasteRequestCategory, std::nullopt)) { if (response == WebCore::DOMPasteAccessResponse::GrantedForCommand || response == WebCore::DOMPasteAccessResponse::GrantedForGesture) _page->grantAccessToCurrentPasteboardData(pasteboardNameForAccessCategory(*pasteAccessCategory)); } if (auto pasteHandler = WTFMove(_domPasteRequestHandler)) { ALLOW_DEPRECATED_DECLARATIONS_BEGIN [UIMenuController.sharedMenuController hideMenuFromView:self]; ALLOW_DEPRECATED_DECLARATIONS_END pasteHandler(response); return YES; } return NO; } - (void)_willPerformAction:(SEL)action sender:(id)sender { if (action != @selector(paste:)) [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture]; } - (void)_didPerformAction:(SEL)action sender:(id)sender { if (action == @selector(paste:)) [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture]; } // UIWKInteractionViewProtocol static inline WebKit::GestureType toGestureType(UIWKGestureType gestureType) { switch (gestureType) { case UIWKGestureLoupe: return WebKit::GestureType::Loupe; case UIWKGestureOneFingerTap: return WebKit::GestureType::OneFingerTap; case UIWKGestureTapAndAHalf: return WebKit::GestureType::TapAndAHalf; case UIWKGestureDoubleTap: return WebKit::GestureType::DoubleTap; case UIWKGestureOneFingerDoubleTap: return WebKit::GestureType::OneFingerDoubleTap; case UIWKGestureOneFingerTripleTap: return WebKit::GestureType::OneFingerTripleTap; case UIWKGestureTwoFingerSingleTap: return WebKit::GestureType::TwoFingerSingleTap; case UIWKGesturePhraseBoundary: return WebKit::GestureType::PhraseBoundary; default: ASSERT_NOT_REACHED(); return WebKit::GestureType::Loupe; } } static inline UIWKGestureType toUIWKGestureType(WebKit::GestureType gestureType) { switch (gestureType) { case WebKit::GestureType::Loupe: return UIWKGestureLoupe; case WebKit::GestureType::OneFingerTap: return UIWKGestureOneFingerTap; case WebKit::GestureType::TapAndAHalf: return UIWKGestureTapAndAHalf; case WebKit::GestureType::DoubleTap: return UIWKGestureDoubleTap; case WebKit::GestureType::OneFingerDoubleTap: return UIWKGestureOneFingerDoubleTap; case WebKit::GestureType::OneFingerTripleTap: return UIWKGestureOneFingerTripleTap; case WebKit::GestureType::TwoFingerSingleTap: return UIWKGestureTwoFingerSingleTap; case WebKit::GestureType::PhraseBoundary: return UIWKGesturePhraseBoundary; } } static inline WebKit::SelectionTouch toSelectionTouch(UIWKSelectionTouch touch) { switch (touch) { case UIWKSelectionTouchStarted: return WebKit::SelectionTouch::Started; case UIWKSelectionTouchMoved: return WebKit::SelectionTouch::Moved; case UIWKSelectionTouchEnded: return WebKit::SelectionTouch::Ended; case UIWKSelectionTouchEndedMovingForward: return WebKit::SelectionTouch::EndedMovingForward; case UIWKSelectionTouchEndedMovingBackward: return WebKit::SelectionTouch::EndedMovingBackward; case UIWKSelectionTouchEndedNotMoving: return WebKit::SelectionTouch::EndedNotMoving; } ASSERT_NOT_REACHED(); return WebKit::SelectionTouch::Ended; } static inline UIWKSelectionTouch toUIWKSelectionTouch(WebKit::SelectionTouch touch) { switch (touch) { case WebKit::SelectionTouch::Started: return UIWKSelectionTouchStarted; case WebKit::SelectionTouch::Moved: return UIWKSelectionTouchMoved; case WebKit::SelectionTouch::Ended: return UIWKSelectionTouchEnded; case WebKit::SelectionTouch::EndedMovingForward: return UIWKSelectionTouchEndedMovingForward; case WebKit::SelectionTouch::EndedMovingBackward: return UIWKSelectionTouchEndedMovingBackward; case WebKit::SelectionTouch::EndedNotMoving: return UIWKSelectionTouchEndedNotMoving; } } static inline WebKit::GestureRecognizerState toGestureRecognizerState(UIGestureRecognizerState state) { switch (state) { case UIGestureRecognizerStatePossible: return WebKit::GestureRecognizerState::Possible; case UIGestureRecognizerStateBegan: return WebKit::GestureRecognizerState::Began; case UIGestureRecognizerStateChanged: return WebKit::GestureRecognizerState::Changed; case UIGestureRecognizerStateCancelled: return WebKit::GestureRecognizerState::Cancelled; case UIGestureRecognizerStateEnded: return WebKit::GestureRecognizerState::Ended; case UIGestureRecognizerStateFailed: return WebKit::GestureRecognizerState::Failed; } } static inline UIGestureRecognizerState toUIGestureRecognizerState(WebKit::GestureRecognizerState state) { switch (state) { case WebKit::GestureRecognizerState::Possible: return UIGestureRecognizerStatePossible; case WebKit::GestureRecognizerState::Began: return UIGestureRecognizerStateBegan; case WebKit::GestureRecognizerState::Changed: return UIGestureRecognizerStateChanged; case WebKit::GestureRecognizerState::Cancelled: return UIGestureRecognizerStateCancelled; case WebKit::GestureRecognizerState::Ended: return UIGestureRecognizerStateEnded; case WebKit::GestureRecognizerState::Failed: return UIGestureRecognizerStateFailed; } } static inline UIWKSelectionFlags toUIWKSelectionFlags(OptionSet flags) { NSInteger uiFlags = UIWKNone; if (flags.contains(WebKit::WordIsNearTap)) uiFlags |= UIWKWordIsNearTap; if (flags.contains(WebKit::SelectionFlipped)) uiFlags |= UIWKSelectionFlipped; if (flags.contains(WebKit::PhraseBoundaryChanged)) uiFlags |= UIWKPhraseBoundaryChanged; return static_cast(uiFlags); } static inline OptionSet toSelectionFlags(UIWKSelectionFlags uiFlags) { OptionSet flags; if (uiFlags & UIWKWordIsNearTap) flags.add(WebKit::WordIsNearTap); if (uiFlags & UIWKSelectionFlipped) flags.add(WebKit::SelectionFlipped); if (uiFlags & UIWKPhraseBoundaryChanged) flags.add(WebKit::PhraseBoundaryChanged); return flags; } static inline WebCore::TextGranularity toWKTextGranularity(UITextGranularity granularity) { switch (granularity) { case UITextGranularityCharacter: return WebCore::TextGranularity::CharacterGranularity; case UITextGranularityWord: return WebCore::TextGranularity::WordGranularity; case UITextGranularitySentence: return WebCore::TextGranularity::SentenceGranularity; case UITextGranularityParagraph: return WebCore::TextGranularity::ParagraphGranularity; case UITextGranularityLine: return WebCore::TextGranularity::LineGranularity; case UITextGranularityDocument: return WebCore::TextGranularity::DocumentGranularity; } } static inline WebCore::SelectionDirection toWKSelectionDirection(UITextDirection direction) { switch (direction) { case UITextLayoutDirectionDown: case UITextLayoutDirectionRight: return WebCore::SelectionDirection::Right; case UITextLayoutDirectionUp: case UITextLayoutDirectionLeft: return WebCore::SelectionDirection::Left; default: // UITextDirection is not an enum, but we only want to accept values from UITextLayoutDirection. ASSERT_NOT_REACHED(); return WebCore::SelectionDirection::Right; } } static void selectionChangedWithGesture(WKContentView *view, const WebCore::IntPoint& point, WebKit::GestureType gestureType, WebKit::GestureRecognizerState gestureState, OptionSet flags) { [(UIWKTextInteractionAssistant *)[view interactionAssistant] selectionChangedWithGestureAt:(CGPoint)point withGesture:toUIWKGestureType(gestureType) withState:toUIGestureRecognizerState(gestureState) withFlags:toUIWKSelectionFlags(flags)]; } static void selectionChangedWithTouch(WKContentView *view, const WebCore::IntPoint& point, WebKit::SelectionTouch touch, OptionSet flags) { [(UIWKTextInteractionAssistant *)[view interactionAssistant] selectionChangedWithTouchAt:(CGPoint)point withSelectionTouch:toUIWKSelectionTouch((WebKit::SelectionTouch)touch) withFlags:toUIWKSelectionFlags(flags)]; } - (BOOL)_hasFocusedElement { return _focusedElementInformation.elementType != WebKit::InputType::None; } - (void)changeSelectionWithGestureAt:(CGPoint)point withGesture:(UIWKGestureType)gestureType withState:(UIGestureRecognizerState)state { [self changeSelectionWithGestureAt:point withGesture:gestureType withState:state withFlags:UIWKNone]; } - (void)changeSelectionWithGestureAt:(CGPoint)point withGesture:(UIWKGestureType)gestureType withState:(UIGestureRecognizerState)state withFlags:(UIWKSelectionFlags)flags { _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; _page->selectWithGesture(WebCore::IntPoint(point), toGestureType(gestureType), toGestureRecognizerState(state), self._hasFocusedElement, [self, strongSelf = retainPtr(self), state, flags](const WebCore::IntPoint& point, WebKit::GestureType gestureType, WebKit::GestureRecognizerState gestureState, OptionSet innerFlags) { selectionChangedWithGesture(self, point, gestureType, gestureState, toSelectionFlags(flags) | innerFlags); if (state == UIGestureRecognizerStateEnded || state == UIGestureRecognizerStateCancelled) _usingGestureForSelection = NO; }); } - (void)changeSelectionWithTouchAt:(CGPoint)point withSelectionTouch:(UIWKSelectionTouch)touch baseIsStart:(BOOL)baseIsStart withFlags:(UIWKSelectionFlags)flags { _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; _page->updateSelectionWithTouches(WebCore::IntPoint(point), toSelectionTouch(touch), baseIsStart, [self, strongSelf = retainPtr(self), flags](const WebCore::IntPoint& point, WebKit::SelectionTouch touch, OptionSet innerFlags) { selectionChangedWithTouch(self, point, touch, toSelectionFlags(flags) | innerFlags); if (toUIWKSelectionTouch(touch) != UIWKSelectionTouchStarted && toUIWKSelectionTouch(touch) != UIWKSelectionTouchMoved) _usingGestureForSelection = NO; }); } - (void)changeSelectionWithTouchesFrom:(CGPoint)from to:(CGPoint)to withGesture:(UIWKGestureType)gestureType withState:(UIGestureRecognizerState)gestureState { _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; _page->selectWithTwoTouches(WebCore::IntPoint(from), WebCore::IntPoint(to), toGestureType(gestureType), toGestureRecognizerState(gestureState), [self, strongSelf = retainPtr(self)](const WebCore::IntPoint& point, WebKit::GestureType gestureType, WebKit::GestureRecognizerState gestureState, OptionSet flags) { selectionChangedWithGesture(self, point, gestureType, gestureState, flags); if (toUIGestureRecognizerState(gestureState) == UIGestureRecognizerStateEnded || toUIGestureRecognizerState(gestureState) == UIGestureRecognizerStateCancelled) _usingGestureForSelection = NO; }); } - (void)moveByOffset:(NSInteger)offset { if (!offset) return; [self beginSelectionChange]; RetainPtr view = self; _page->moveSelectionByOffset(offset, [view] { [view endSelectionChange]; }); } - (const WebKit::WKAutoCorrectionData&)autocorrectionData { return _autocorrectionData; } // The completion handler can pass nil if input does not match the actual text preceding the insertion point. - (void)requestAutocorrectionRectsForString:(NSString *)input withCompletionHandler:(void (^)(UIWKAutocorrectionRects *rectsForInput))completionHandler { if (!completionHandler) { [NSException raise:NSInvalidArgumentException format:@"Expected a nonnull completion handler in %s.", __PRETTY_FUNCTION__]; return; } if (!input || ![input length]) { completionHandler(nil); return; } _page->requestAutocorrectionData(input, [view = retainPtr(self), completion = makeBlockPtr(completionHandler)](auto data) { CGRect firstRect; CGRect lastRect; auto& rects = data.textRects; if (rects.isEmpty()) { firstRect = CGRectZero; lastRect = CGRectZero; } else { firstRect = rects.first(); lastRect = rects.last(); } view->_autocorrectionData.font = data.font; view->_autocorrectionData.textFirstRect = firstRect; view->_autocorrectionData.textLastRect = lastRect; completion(!rects.isEmpty() ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:firstRect lastCGRect:lastRect] : nil); }); } #if HAVE(UI_EDIT_MENU_INTERACTION) - (void)requestPreferredArrowDirectionForEditMenuWithCompletionHandler:(void(^)(UIEditMenuArrowDirection))completion { [self _requestEvasionRectsAboveSelectionIfNeeded:[completion = makeBlockPtr(completion)](const Vector& rects) { completion(rects.isEmpty() ? UIEditMenuArrowDirectionAutomatic : UIEditMenuArrowDirectionUp); }]; } #endif - (void)requestRectsToEvadeForSelectionCommandsWithCompletionHandler:(void(^)(NSArray *rects))completion { [self _requestEvasionRectsAboveSelectionIfNeeded:[completion = makeBlockPtr(completion)](const Vector& rects) { completion(createNSArray(rects).get()); }]; } - (void)_requestEvasionRectsAboveSelectionIfNeeded:(CompletionHandler&)>&&)completion { if ([self _shouldSuppressSelectionCommands]) { completion({ }); return; } auto requestRectsToEvadeIfNeeded = [startTime = ApproximateTime::now(), weakSelf = WeakObjCPtr(self), completion = WTFMove(completion)]() mutable { auto strongSelf = weakSelf.get(); if (!strongSelf) { completion({ }); return; } if ([strongSelf webView]._editable) { completion({ }); return; } auto focusedElementType = strongSelf->_focusedElementInformation.elementType; if (focusedElementType != WebKit::InputType::ContentEditable && focusedElementType != WebKit::InputType::TextArea) { completion({ }); return; } // Give the page some time to present custom editing UI before attempting to detect and evade it. auto delayBeforeShowingCalloutBar = std::max(0_s, 0.25_s - (ApproximateTime::now() - startTime)); WorkQueue::main().dispatchAfter(delayBeforeShowingCalloutBar, [completion = WTFMove(completion), weakSelf]() mutable { auto strongSelf = weakSelf.get(); if (!strongSelf) { completion({ }); return; } if (!strongSelf->_page) { completion({ }); return; } strongSelf->_page->requestEvasionRectsAboveSelection(WTFMove(completion)); }); }; #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) [self doAfterComputingImageAnalysisResultsForBackgroundRemoval:WTFMove(requestRectsToEvadeIfNeeded)]; #else requestRectsToEvadeIfNeeded(); #endif } #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) - (UIMenu *)removeBackgroundMenu { if (!_page || !_page->preferences().removeBackgroundEnabled()) return nil; if (_page->editorState().isMissingPostLayoutData || !_removeBackgroundData) return nil; if (_removeBackgroundData->element != _page->editorState().postLayoutData().selectedEditableImage) return nil; return [self menuWithInlineAction:WebCore::contextMenuItemTitleRemoveBackground() image:[UIImage _systemImageNamed:@"circle.rectangle.filled.pattern.diagonalline"] identifier:@"WKActionRemoveBackground" handler:[](WKContentView *view) { auto data = std::exchange(view->_removeBackgroundData, { }); if (!data) return; auto [elementContext, image, preferredMIMEType] = *data; if (auto [data, type] = WebKit::imageDataForRemoveBackground(image.get(), preferredMIMEType.createCFString().get()); data) view->_page->replaceImageWithMarkupResults(elementContext, { String { type.get() } }, { static_cast([data bytes]), [data length] }); }]; } - (void)doAfterComputingImageAnalysisResultsForBackgroundRemoval:(CompletionHandler&&)completion { if (_page->editorState().isMissingPostLayoutData) { completion(); return; } auto elementToAnalyze = _page->editorState().postLayoutData().selectedEditableImage; if (_removeBackgroundData && _removeBackgroundData->element == elementToAnalyze) { completion(); return; } _removeBackgroundData = std::nullopt; if (!elementToAnalyze) { completion(); return; } _page->shouldAllowRemoveBackground(*elementToAnalyze, [context = *elementToAnalyze, completion = WTFMove(completion), weakSelf = WeakObjCPtr(self)](bool shouldAllow) mutable { auto strongSelf = weakSelf.get(); if (!shouldAllow || !strongSelf) { completion(); return; } strongSelf->_page->requestImageBitmap(context, [context, completion = WTFMove(completion), weakSelf = WTFMove(weakSelf)](auto& imageData, auto& sourceMIMEType) mutable { auto strongSelf = weakSelf.get(); if (!strongSelf) { completion(); return; } auto imageBitmap = WebKit::ShareableBitmap::create(imageData); if (!imageBitmap) { completion(); return; } auto cgImage = imageBitmap->makeCGImage(); if (!cgImage) { completion(); return; } WebKit::requestBackgroundRemoval(cgImage.get(), [sourceMIMEType, context, completion = WTFMove(completion), weakSelf](CGImageRef result, CGRect) mutable { auto strongSelf = weakSelf.get(); if (!strongSelf) { completion(); return; } if (result) strongSelf->_removeBackgroundData = { { context, { result }, sourceMIMEType } }; else strongSelf->_removeBackgroundData = std::nullopt; completion(); }); }); }); } - (BOOL)_handleTapOverImageAnalysisInteractionButton:(CGPoint)position { if (!_imageAnalysisInteraction) return NO; auto *hitButton = dynamic_objc_cast([self hitTest:position withEvent:nil]); UIButton *analysisButton = [_imageAnalysisInteraction analysisButton]; // FIXME: Instead of a class check, this should be a straightforward equality check. // However, rdar://91828384 currently prevents us from doing so. We can simplify this logic // once the fix for that bug has landed. if (analysisButton && hitButton.class == analysisButton.class) { [_imageAnalysisInteraction setHighlightSelectableItems:![_imageAnalysisInteraction highlightSelectableItems]]; return YES; } if ([_imageAnalysisActionButtons containsObject:hitButton]) { [hitButton sendActionsForControlEvents:UIControlEventTouchUpInside]; return YES; } return NO; } #endif // ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) - (void)selectPositionAtPoint:(CGPoint)point completionHandler:(void (^)(void))completionHandler { _autocorrectionContextNeedsUpdate = YES; [self _selectPositionAtPoint:point stayingWithinFocusedElement:self._hasFocusedElement completionHandler:completionHandler]; } - (void)_selectPositionAtPoint:(CGPoint)point stayingWithinFocusedElement:(BOOL)stayingWithinFocusedElement completionHandler:(void (^)(void))completionHandler { _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; _page->selectPositionAtPoint(WebCore::IntPoint(point), stayingWithinFocusedElement, [view = retainPtr(self), completionHandler = makeBlockPtr(completionHandler)]() { completionHandler(); view->_usingGestureForSelection = NO; }); } - (void)selectPositionAtBoundary:(UITextGranularity)granularity inDirection:(UITextDirection)direction fromPoint:(CGPoint)point completionHandler:(void (^)(void))completionHandler { _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; _page->selectPositionAtBoundaryWithDirection(WebCore::IntPoint(point), toWKTextGranularity(granularity), toWKSelectionDirection(direction), self._hasFocusedElement, [view = retainPtr(self), completionHandler = makeBlockPtr(completionHandler)]() { completionHandler(); view->_usingGestureForSelection = NO; }); } - (void)moveSelectionAtBoundary:(UITextGranularity)granularity inDirection:(UITextDirection)direction completionHandler:(void (^)(void))completionHandler { _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; _page->moveSelectionAtBoundaryWithDirection(toWKTextGranularity(granularity), toWKSelectionDirection(direction), [view = retainPtr(self), completionHandler = makeBlockPtr(completionHandler)] { completionHandler(); view->_usingGestureForSelection = NO; }); } - (void)selectTextWithGranularity:(UITextGranularity)granularity atPoint:(CGPoint)point completionHandler:(void (^)(void))completionHandler { _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; ++_suppressNonEditableSingleTapTextInteractionCount; _page->selectTextWithGranularityAtPoint(WebCore::IntPoint(point), toWKTextGranularity(granularity), self._hasFocusedElement, [view = retainPtr(self), selectionHandler = makeBlockPtr(completionHandler)] { selectionHandler(); view->_usingGestureForSelection = NO; --view->_suppressNonEditableSingleTapTextInteractionCount; }); } - (void)beginSelectionInDirection:(UITextDirection)direction completionHandler:(void (^)(BOOL endIsMoving))completionHandler { _autocorrectionContextNeedsUpdate = YES; _page->beginSelectionInDirection(toWKSelectionDirection(direction), [selectionHandler = makeBlockPtr(completionHandler)] (bool endIsMoving) { selectionHandler(endIsMoving); }); } - (void)updateSelectionWithExtentPoint:(CGPoint)point completionHandler:(void (^)(BOOL endIsMoving))completionHandler { _autocorrectionContextNeedsUpdate = YES; auto respectSelectionAnchor = self.interactionAssistant._wk_hasFloatingCursor ? WebKit::RespectSelectionAnchor::Yes : WebKit::RespectSelectionAnchor::No; _page->updateSelectionWithExtentPoint(WebCore::IntPoint(point), self._hasFocusedElement, respectSelectionAnchor, [selectionHandler = makeBlockPtr(completionHandler)](bool endIsMoving) { selectionHandler(endIsMoving); }); } - (void)updateSelectionWithExtentPoint:(CGPoint)point withBoundary:(UITextGranularity)granularity completionHandler:(void (^)(BOOL selectionEndIsMoving))completionHandler { _autocorrectionContextNeedsUpdate = YES; ++_suppressNonEditableSingleTapTextInteractionCount; _page->updateSelectionWithExtentPointAndBoundary(WebCore::IntPoint(point), toWKTextGranularity(granularity), self._hasFocusedElement, [completionHandler = makeBlockPtr(completionHandler), protectedSelf = retainPtr(self)] (bool endIsMoving) { completionHandler(endIsMoving); --protectedSelf->_suppressNonEditableSingleTapTextInteractionCount; }); } - (UTF32Char)_characterInRelationToCaretSelection:(int)amount { if (amount == -1 && _lastInsertedCharacterToOverrideCharacterBeforeSelection) return *_lastInsertedCharacterToOverrideCharacterBeforeSelection; auto& state = _page->editorState(); if (!state.isContentEditable || state.selectionIsNone || state.selectionIsRange) return 0; switch (amount) { case 0: return state.postLayoutData().characterAfterSelection; case -1: return state.postLayoutData().characterBeforeSelection; case -2: return state.postLayoutData().twoCharacterBeforeSelection; default: return 0; } } - (BOOL)_selectionAtDocumentStart { return !_page->editorState().postLayoutData().characterBeforeSelection; } - (CGRect)textFirstRect { auto& editorState = _page->editorState(); if (editorState.hasComposition) { auto& markedTextRects = editorState.postLayoutData().markedTextRects; return markedTextRects.isEmpty() ? CGRectZero : markedTextRects.first().rect(); } return _autocorrectionData.textFirstRect; } - (CGRect)textLastRect { auto& editorState = _page->editorState(); if (editorState.hasComposition) { auto& markedTextRects = editorState.postLayoutData().markedTextRects; return markedTextRects.isEmpty() ? CGRectZero : markedTextRects.last().rect(); } return _autocorrectionData.textLastRect; } - (void)willInsertFinalDictationResult { _page->willInsertFinalDictationResult(); } - (void)didInsertFinalDictationResult { _page->didInsertFinalDictationResult(); } - (void)replaceDictatedText:(NSString*)oldText withText:(NSString *)newText { if (_isHidingKeyboard && _isChangingFocus) return; _autocorrectionContextNeedsUpdate = YES; _page->replaceDictatedText(oldText, newText); } - (void)requestDictationContext:(void (^)(NSString *selectedText, NSString *beforeText, NSString *afterText))completionHandler { _page->requestDictationContext([dictationHandler = makeBlockPtr(completionHandler)](const String& selectedText, const String& beforeText, const String& afterText) { dictationHandler(selectedText, beforeText, afterText); }); } // The completion handler should pass the rect of the correction text after replacing the input text, or nil if the replacement could not be performed. - (void)applyAutocorrection:(NSString *)correction toString:(NSString *)input withCompletionHandler:(void (^)(UIWKAutocorrectionRects *rectsForCorrection))completionHandler { if ([self _disableAutomaticKeyboardUI]) { if (completionHandler) completionHandler(nil); return; } // FIXME: Remove the synchronous call when is fixed. const bool useSyncRequest = true; if (useSyncRequest) { if (completionHandler) completionHandler(_page->applyAutocorrection(correction, input) ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:_autocorrectionData.textFirstRect lastCGRect:_autocorrectionData.textLastRect] : nil); return; } _page->applyAutocorrection(correction, input, [view = retainPtr(self), completion = makeBlockPtr(completionHandler)](auto& string) { if (completion) completion(!string.isNull() ? [WKAutocorrectionRects autocorrectionRectsWithFirstCGRect:view->_autocorrectionData.textFirstRect lastCGRect:view->_autocorrectionData.textLastRect] : nil); }); } - (void)_invokePendingAutocorrectionContextHandler:(WKAutocorrectionContext *)context { if (auto handler = WTFMove(_pendingAutocorrectionContextHandler)) handler(context); } - (void)_cancelPendingAutocorrectionContextHandler { [self _invokePendingAutocorrectionContextHandler:WKAutocorrectionContext.emptyAutocorrectionContext]; } - (void)requestAutocorrectionContextWithCompletionHandler:(void (^)(UIWKAutocorrectionContext *autocorrectionContext))completionHandler { if (!completionHandler) { [NSException raise:NSInvalidArgumentException format:@"Expected a nonnull completion handler in %s.", __PRETTY_FUNCTION__]; return; } if ([self _disableAutomaticKeyboardUI]) { completionHandler(WKAutocorrectionContext.emptyAutocorrectionContext); return; } if (!_page->hasRunningProcess()) { completionHandler(WKAutocorrectionContext.emptyAutocorrectionContext); return; } bool respondWithLastKnownAutocorrectionContext = ([&] { if (_page->isRunningModalJavaScriptDialog()) return true; if (_domPasteRequestHandler) return true; if (_isUnsuppressingSoftwareKeyboardUsingLastAutocorrectionContext) return true; return !_autocorrectionContextNeedsUpdate; })(); if (respondWithLastKnownAutocorrectionContext) { completionHandler([WKAutocorrectionContext autocorrectionContextWithWebContext:_lastAutocorrectionContext]); return; } // FIXME: Remove the synchronous call when is fixed. const bool useSyncRequest = true; if (useSyncRequest && _pendingAutocorrectionContextHandler) { completionHandler(WKAutocorrectionContext.emptyAutocorrectionContext); return; } _pendingAutocorrectionContextHandler = completionHandler; _page->requestAutocorrectionContext(); if (!useSyncRequest) return; if (!_page->process().connection()->waitForAndDispatchImmediately(_page->webPageID(), 1_s, IPC::WaitForOption::DispatchIncomingSyncMessagesWhileWaiting)) RELEASE_LOG(TextInput, "Timed out while waiting for autocorrection context."); [self _cancelPendingAutocorrectionContextHandler]; } - (void)_handleAutocorrectionContext:(const WebKit::WebAutocorrectionContext&)context { _lastAutocorrectionContext = context; _autocorrectionContextNeedsUpdate = NO; [self unsuppressSoftwareKeyboardUsingLastAutocorrectionContextIfNeeded]; [self _invokePendingAutocorrectionContextHandler:[WKAutocorrectionContext autocorrectionContextWithWebContext:context]]; } - (void)updateSoftwareKeyboardSuppressionStateFromWebView { BOOL webViewIsSuppressingSoftwareKeyboard = [_webView _suppressSoftwareKeyboard]; if (webViewIsSuppressingSoftwareKeyboard) { _unsuppressSoftwareKeyboardAfterNextAutocorrectionContextUpdate = NO; self._suppressSoftwareKeyboard = webViewIsSuppressingSoftwareKeyboard; return; } if (self._suppressSoftwareKeyboard == webViewIsSuppressingSoftwareKeyboard) return; if (!std::exchange(_unsuppressSoftwareKeyboardAfterNextAutocorrectionContextUpdate, YES)) _page->requestAutocorrectionContext(); } - (void)unsuppressSoftwareKeyboardUsingLastAutocorrectionContextIfNeeded { if (!std::exchange(_unsuppressSoftwareKeyboardAfterNextAutocorrectionContextUpdate, NO)) return; SetForScope unsuppressSoftwareKeyboardScope { _isUnsuppressingSoftwareKeyboardUsingLastAutocorrectionContext, YES }; self._suppressSoftwareKeyboard = NO; } - (void)runModalJavaScriptDialog:(CompletionHandler&&)callback { if (_isFocusingElementWithKeyboard) _pendingRunModalJavaScriptDialogCallback = WTFMove(callback); else callback(); } - (void)_didStartProvisionalLoadForMainFrame { // Reset the double tap gesture recognizer to prevent any double click that is in the process of being recognized. [_doubleTapGestureRecognizerForDoubleClick _wk_cancel]; // We also need to disable the double-tap gesture recognizers that are enabled for double-tap-to-zoom and which // are enabled when a single tap is first recognized. This avoids tests running in sequence and simulating taps // in the same location to trigger double-tap recognition. [self _setDoubleTapGesturesEnabled:NO]; [_twoFingerDoubleTapGestureRecognizer _wk_cancel]; #if ENABLE(IMAGE_ANALYSIS) [self _cancelImageAnalysis]; #endif } - (void)_didCommitLoadForMainFrame { _seenHardwareKeyDownInNonEditableElement = NO; [self _elementDidBlur]; [self _cancelLongPressGestureRecognizer]; [self _removeContainerForContextMenuHintPreviews]; [self _removeContainerForDragPreviews]; [self _removeContainerForDropPreviews]; [_webView _didCommitLoadForMainFrame]; _textInteractionDidChangeFocusedElement = NO; _activeTextInteractionCount = 0; _treatAsContentEditableUntilNextEditorStateUpdate = NO; [self _invalidateCurrentPositionInformation]; #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) [self uninstallImageAnalysisInteraction]; #endif } - (void)_nextAccessoryTabForWebView:(id)sender { [self accessoryTab:YES]; } - (void)_previousAccessoryTabForWebView:(id)sender { [self accessoryTab:NO]; } - (void)_becomeFirstResponderWithSelectionMovingForward:(BOOL)selectingForward completionHandler:(void (^)(BOOL didBecomeFirstResponder))completionHandler { constexpr bool isKeyboardEventValid = false; _page->setInitialFocus(selectingForward, isKeyboardEventValid, { }, [protectedSelf = retainPtr(self), completionHandler = makeBlockPtr(completionHandler)] { if (completionHandler) completionHandler([protectedSelf becomeFirstResponder]); }); } - (WebCore::Color)_tapHighlightColorForFastClick:(BOOL)forFastClick { ASSERT(_showDebugTapHighlightsForFastClicking); return forFastClick ? WebCore::SRGBA { 0, 225, 0, 127 } : WebCore::SRGBA { 225, 0, 0, 127 }; } - (void)_setDoubleTapGesturesEnabled:(BOOL)enabled { if (enabled && ![_doubleTapGestureRecognizer isEnabled]) { // The first tap recognized after re-enabling double tap gestures will not wait for the // second tap before committing. To fix this, we use a new double tap gesture recognizer. [self _createAndConfigureDoubleTapGestureRecognizer]; } if (_showDebugTapHighlightsForFastClicking && !enabled) _tapHighlightInformation.color = [self _tapHighlightColorForFastClick:YES]; [_doubleTapGestureRecognizer setEnabled:enabled]; [_nonBlockingDoubleTapGestureRecognizer setEnabled:!enabled]; [self _resetIsDoubleTapPending]; } // MARK: UIWebFormAccessoryDelegate protocol and accessory methods - (void)accessoryClear { _page->setFocusedElementValue(_focusedElementInformation.elementContext, { }); } - (void)accessoryDone { [self stopRelinquishingFirstResponderToFocusedElement]; [self endEditingAndUpdateFocusAppearanceWithReason:EndEditingReasonAccessoryDone]; _page->setIsShowingInputViewForFocusedElement(false); } - (void)updateFocusedElementValue:(NSString *)value { _page->setFocusedElementValue(_focusedElementInformation.elementContext, value); _focusedElementInformation.value = value; } - (void)updateFocusedElementValueAsColor:(UIColor *)value { WebCore::Color color(WebCore::roundAndClampToSRGBALossy(value.CGColor)); String valueAsString = WebCore::serializationForHTML(color); _page->setFocusedElementValue(_focusedElementInformation.elementContext, valueAsString); _focusedElementInformation.value = valueAsString; _focusedElementInformation.colorValue = color; } - (void)updateFocusedElementSelectedIndex:(uint32_t)index allowsMultipleSelection:(bool)allowsMultipleSelection { _page->setFocusedElementSelectedIndex(_focusedElementInformation.elementContext, index, allowsMultipleSelection); } - (void)updateFocusedElementFocusedWithDataListDropdown:(BOOL)value { _focusedElementInformation.isFocusingWithDataListDropdown = value; [self reloadInputViews]; } - (void)accessoryTab:(BOOL)isNext { // The input peripheral may need to update the focused DOM node before we switch focus. The UI process does // not maintain a handle to the actual focused DOM node – only the web process has such a handle. So, we need // to end the editing session now before we tell the web process to switch focus. Once the web process tells // us the newly focused element we are no longer are in a position to effect the previously focused element. // See . [self _endEditing]; _inputPeripheral = nil; // Nullify so that we don't tell the input peripheral to end editing again in -_elementDidBlur. _isChangingFocusUsingAccessoryTab = YES; [self beginSelectionChange]; _page->focusNextFocusedElement(isNext, [protectedSelf = retainPtr(self)] { [protectedSelf endSelectionChange]; [protectedSelf reloadInputViews]; protectedSelf->_isChangingFocusUsingAccessoryTab = NO; }); } - (void)accessoryAutoFill { id <_WKInputDelegate> inputDelegate = [_webView _inputDelegate]; if ([inputDelegate respondsToSelector:@selector(_webView:accessoryViewCustomButtonTappedInFormInputSession:)]) [inputDelegate _webView:self.webView accessoryViewCustomButtonTappedInFormInputSession:_formInputSession.get()]; } - (UIWebFormAccessory *)formAccessoryView { if (_formAccessoryView) return _formAccessoryView.get(); _formAccessoryView = adoptNS([[UIWebFormAccessory alloc] initWithInputAssistantItem:self.inputAssistantItem]); [_formAccessoryView setDelegate:self]; return _formAccessoryView.get(); } - (void)accessoryOpen { if (!_inputPeripheral) return; [self _zoomToRevealFocusedElement]; [self _updateAccessory]; [_inputPeripheral beginEditing]; } - (void)_updateAccessory { auto* accessoryView = self.formAccessoryView; // Creates one, if needed. if ([accessoryView respondsToSelector:@selector(setNextPreviousItemsVisible:)]) [accessoryView setNextPreviousItemsVisible:!self.webView._editable]; [accessoryView setNextEnabled:_focusedElementInformation.hasNextNode]; [accessoryView setPreviousEnabled:_focusedElementInformation.hasPreviousNode]; if (!WebKit::currentUserInterfaceIdiomIsSmallScreen()) { [accessoryView setClearVisible:NO]; return; } switch (_focusedElementInformation.elementType) { case WebKit::InputType::Date: case WebKit::InputType::DateTimeLocal: case WebKit::InputType::Month: case WebKit::InputType::Time: [accessoryView setClearVisible:YES]; return; default: [accessoryView setClearVisible:NO]; return; } } // MARK: Keyboard interaction // UITextInput protocol implementation - (BOOL)_allowAnimatedUpdateSelectionRectViews { return NO; } - (void)beginSelectionChange { _autocorrectionContextNeedsUpdate = YES; _selectionChangeNestingLevel++; [self.inputDelegate selectionWillChange:self]; } - (void)endSelectionChange { [self.inputDelegate selectionDidChange:self]; if (_selectionChangeNestingLevel) { // FIXME (228083): Some layout tests end up triggering unbalanced calls to -endSelectionChange. // We should investigate why this happens, (ideally) prevent it from happening, and then assert // that `_selectionChangeNestingLevel` is nonzero when calling -endSelectionChange. _selectionChangeNestingLevel--; } } - (void)willFinishIgnoringCalloutBarFadeAfterPerformingAction { _ignoreSelectionCommandFadeCount++; _page->scheduleFullEditorStateUpdate(); _page->callAfterNextPresentationUpdate([weakSelf = WeakObjCPtr(self)] (auto) { if (auto strongSelf = weakSelf.get()) strongSelf->_ignoreSelectionCommandFadeCount--; }); } - (void)_didChangeWebViewEditability { if ([_formAccessoryView respondsToSelector:@selector(setNextPreviousItemsVisible:)]) [_formAccessoryView setNextPreviousItemsVisible:!self.webView._editable]; [_twoFingerSingleTapGestureRecognizer setEnabled:!self.webView._editable]; } - (void)insertTextSuggestion:(UITextSuggestion *)textSuggestion { _autocorrectionContextNeedsUpdate = YES; // FIXME: Replace NSClassFromString with actual class as soon as UIKit submitted the new class into the iOS SDK. if ([textSuggestion isKindOfClass:[UITextAutofillSuggestion class]]) { _page->autofillLoginCredentials([(UITextAutofillSuggestion *)textSuggestion username], [(UITextAutofillSuggestion *)textSuggestion password]); return; } #if ENABLE(DATALIST_ELEMENT) if ([textSuggestion isKindOfClass:[WKDataListTextSuggestion class]]) { _page->setFocusedElementValue(_focusedElementInformation.elementContext, [textSuggestion inputText]); return; } #endif id <_WKInputDelegate> inputDelegate = [_webView _inputDelegate]; if ([inputDelegate respondsToSelector:@selector(_webView:insertTextSuggestion:inInputSession:)]) [inputDelegate _webView:self.webView insertTextSuggestion:textSuggestion inInputSession:_formInputSession.get()]; } - (NSString *)textInRange:(UITextRange *)range { if (!_page) return nil; auto& editorState = _page->editorState(); if (self.selectedTextRange == range && !editorState.isMissingPostLayoutData && editorState.selectionIsRange) return editorState.postLayoutData().wordAtSelection; return nil; } - (void)replaceRange:(UITextRange *)range withText:(NSString *)text { } static NSArray *textSelectionRects(const Vector& rects, CGFloat scaleFactor) { return createNSArray(rects, [scaleFactor] (auto& rect) { return adoptNS([[WKTextSelectionRect alloc] initWithSelectionGeometry:rect scaleFactor:scaleFactor]); }).autorelease(); } - (WebCore::FloatRect)_scaledCaretRectForSelectionStart:(WebCore::FloatRect)caretRect { // The logical height of the caret is scaled inversely by the view's zoom scale // to achieve the visual effect that the caret is narrow when zoomed in and wide // when zoomed out. double inverseScale = [self inverseScale]; if (bool isHorizontalCaret = caretRect.width() < caretRect.height()) caretRect.setWidth(caretRect.width() * inverseScale); else caretRect.setHeight(caretRect.height() * inverseScale); return caretRect; } - (WebCore::FloatRect)_scaledCaretRectForSelectionEnd:(WebCore::FloatRect)caretRect { // The logical height of the caret is scaled inversely by the view's zoom scale // to achieve the visual effect that the caret is narrow when zoomed in and wide // when zoomed out. double inverseScale = [self inverseScale]; if (bool isHorizontalCaret = caretRect.width() < caretRect.height()) { float originalWidth = caretRect.width(); caretRect.setWidth(originalWidth * inverseScale); caretRect.move(caretRect.width() - originalWidth, 0); } else { float originalHeight = caretRect.height(); caretRect.setHeight(caretRect.height() * inverseScale); caretRect.move(0, caretRect.height() - originalHeight); } return caretRect; } - (UITextRange *)selectedTextRange { auto& editorState = _page->editorState(); auto hasSelection = !editorState.selectionIsNone; if (!hasSelection || editorState.isMissingPostLayoutData) return nil; auto isRange = editorState.selectionIsRange; auto isContentEditable = editorState.isContentEditable; // UIKit does not expect caret selections in non-editable content. if (!isContentEditable && !isRange) return nil; if (_cachedSelectedTextRange) return _cachedSelectedTextRange.get(); auto caretStartRect = [self _scaledCaretRectForSelectionStart:_page->editorState().postLayoutData().caretRectAtStart]; auto caretEndRect = [self _scaledCaretRectForSelectionEnd:_page->editorState().postLayoutData().caretRectAtEnd]; auto selectionRects = textSelectionRects(_page->editorState().postLayoutData().selectionGeometries, self._contentZoomScale); auto selectedTextLength = editorState.postLayoutData().selectedTextLength; _cachedSelectedTextRange = [WKTextRange textRangeWithState:!hasSelection isRange:isRange isEditable:isContentEditable startRect:caretStartRect endRect:caretEndRect selectionRects:selectionRects selectedTextLength:selectedTextLength]; return _cachedSelectedTextRange.get(); } - (CGRect)caretRectForPosition:(UITextPosition *)position { return ((WKTextPosition *)position).positionRect; } - (NSArray *)selectionRectsForRange:(UITextRange *)range { return [(WKTextRange *)range selectionRects]; } - (void)setSelectedTextRange:(UITextRange *)range { if (range) return; #if !PLATFORM(MACCATALYST) if (!self._hasFocusedElement) return; #endif [self clearSelection]; } - (BOOL)hasMarkedText { return [_markedText length]; } - (NSString *)markedText { return _markedText.get(); } - (UITextRange *)markedTextRange { auto& editorState = _page->editorState(); bool hasComposition = editorState.hasComposition; if (!hasComposition || editorState.isMissingPostLayoutData) return nil; auto& postLayoutData = editorState.postLayoutData(); auto unscaledCaretRectAtStart = postLayoutData.markedTextCaretRectAtStart; auto unscaledCaretRectAtEnd = postLayoutData.markedTextCaretRectAtEnd; auto isRange = unscaledCaretRectAtStart != unscaledCaretRectAtEnd; auto isContentEditable = editorState.isContentEditable; auto caretStartRect = [self _scaledCaretRectForSelectionStart:unscaledCaretRectAtStart]; auto caretEndRect = [self _scaledCaretRectForSelectionEnd:unscaledCaretRectAtEnd]; auto selectionRects = textSelectionRects(postLayoutData.markedTextRects, self._contentZoomScale); auto selectedTextLength = postLayoutData.markedText.length(); return [WKTextRange textRangeWithState:!hasComposition isRange:isRange isEditable:isContentEditable startRect:caretStartRect endRect:caretEndRect selectionRects:selectionRects selectedTextLength:selectedTextLength]; } - (NSDictionary *)markedTextStyle { return nil; } - (void)setMarkedTextStyle:(NSDictionary *)styleDictionary { } static Vector compositionHighlights(NSAttributedString *string) { if (!string.length) return { }; Vector highlights; [string enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:[&highlights](NSDictionary *attributes, NSRange range, BOOL *) { if (!attributes[NSMarkedClauseSegmentAttributeName]) return; WebCore::Color highlightColor { WebCore::CompositionHighlight::defaultCompositionFillColor }; if (UIColor *uiColor = attributes[NSBackgroundColorAttributeName]) highlightColor = WebCore::colorFromCocoaColor(uiColor); highlights.append({ static_cast(range.location), static_cast(NSMaxRange(range)), highlightColor }); }]; std::sort(highlights.begin(), highlights.end(), [](auto& a, auto& b) { if (a.startOffset < b.startOffset) return true; if (a.startOffset > b.startOffset) return false; return a.endOffset < b.endOffset; }); Vector mergedHighlights; mergedHighlights.reserveInitialCapacity(highlights.size()); for (auto& highlight : highlights) { if (mergedHighlights.isEmpty() || mergedHighlights.last().color != highlight.color) mergedHighlights.append(highlight); else mergedHighlights.last().endOffset = highlight.endOffset; } return mergedHighlights; } - (void)setAttributedMarkedText:(NSAttributedString *)markedText selectedRange:(NSRange)selectedRange { [self _setMarkedText:markedText.string highlights:compositionHighlights(markedText) selectedRange:selectedRange]; } - (void)setMarkedText:(NSString *)markedText selectedRange:(NSRange)selectedRange { [self _setMarkedText:markedText highlights:Vector { } selectedRange:selectedRange]; } - (void)_setMarkedText:(NSString *)markedText highlights:(const Vector&)highlights selectedRange:(NSRange)selectedRange { _autocorrectionContextNeedsUpdate = YES; _candidateViewNeedsUpdate = !self.hasMarkedText; _markedText = markedText; _page->setCompositionAsync(markedText, { }, highlights, selectedRange, { }); } - (void)unmarkText { _markedText = nil; _page->confirmCompositionAsync(); } - (UITextPosition *)beginningOfDocument { return nil; } - (UITextPosition *)endOfDocument { return nil; } - (UITextRange *)textRangeFromPosition:(UITextPosition *)fromPosition toPosition:(UITextPosition *)toPosition { return nil; } - (UITextPosition *)positionFromPosition:(UITextPosition *)position offset:(NSInteger)offset { return nil; } - (UITextPosition *)positionFromPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction offset:(NSInteger)offset { return nil; } - (NSComparisonResult)comparePosition:(UITextPosition *)position toPosition:(UITextPosition *)other { return NSOrderedSame; } - (NSInteger)offsetFromPosition:(UITextPosition *)from toPosition:(UITextPosition *)to { #if HAVE(UIFINDINTERACTION) if ([from isKindOfClass:[WKFoundTextPosition class]] && [to isKindOfClass:[WKFoundTextPosition class]]) { WKFoundTextPosition *fromPosition = (WKFoundTextPosition *)from; WKFoundTextPosition *toPosition = (WKFoundTextPosition *)to; if (fromPosition.order == toPosition.order) return fromPosition.offset - toPosition.offset; return fromPosition.order - toPosition.order; } #endif return 0; } - (id )tokenizer { return nil; } - (UITextPosition *)positionWithinRange:(UITextRange *)range farthestInDirection:(UITextLayoutDirection)direction { return nil; } - (UITextRange *)characterRangeByExtendingPosition:(UITextPosition *)position inDirection:(UITextLayoutDirection)direction { return nil; } - (NSWritingDirection)baseWritingDirectionForPosition:(UITextPosition *)position inDirection:(UITextStorageDirection)direction { return NSWritingDirectionLeftToRight; } static WebKit::WritingDirection coreWritingDirection(NSWritingDirection direction) { switch (direction) { case NSWritingDirectionNatural: return WebCore::WritingDirection::Natural; case NSWritingDirectionLeftToRight: return WebCore::WritingDirection::LeftToRight; case NSWritingDirectionRightToLeft: return WebCore::WritingDirection::RightToLeft; default: ASSERT_NOT_REACHED(); return WebCore::WritingDirection::Natural; } } - (void)setBaseWritingDirection:(NSWritingDirection)direction forRange:(UITextRange *)range { if (!_page->isEditable()) return; if (range && ![range isEqual:self.selectedTextRange]) { // We currently only support changing the base writing direction at the selection. return; } _page->setBaseWritingDirection(coreWritingDirection(direction)); } - (CGRect)firstRectForRange:(UITextRange *)range { return CGRectZero; } /* Hit testing. */ - (UITextPosition *)closestPositionToPoint:(CGPoint)point { #if PLATFORM(MACCATALYST) WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(point)); [self requestAsynchronousPositionInformationUpdate:request]; if ([self _currentPositionInformationIsApproximatelyValidForRequest:request radiusForApproximation:2] && _positionInformation.isSelectable()) return [WKTextPosition textPositionWithRect:_positionInformation.caretRect]; #endif return nil; } - (UITextPosition *)closestPositionToPoint:(CGPoint)point withinRange:(UITextRange *)range { return nil; } - (UITextRange *)characterRangeAtPoint:(CGPoint)point { return nil; } - (void)deleteBackward { _page->executeEditCommand("deleteBackward"_s); } - (BOOL)_shouldSimulateKeyboardInputOnTextInsertion { #if HAVE(PENCILKIT_TEXT_INPUT) return [_scribbleInteraction isHandlingWriting]; #else return NO; #endif } // Inserts the given string, replacing any selected or marked text. - (void)insertText:(NSString *)aStringValue { if (_isHidingKeyboard && _isChangingFocus) return; auto* keyboard = [UIKeyboardImpl sharedInstance]; WebKit::InsertTextOptions options; options.processingUserGesture = keyboard.isCallingInputDelegate; options.shouldSimulateKeyboardInput = [self _shouldSimulateKeyboardInputOnTextInsertion]; _page->insertTextAsync(aStringValue, WebKit::EditingRange(), WTFMove(options)); if (_focusedElementInformation.autocapitalizeType == WebCore::AutocapitalizeType::Words && aStringValue.length) { _lastInsertedCharacterToOverrideCharacterBeforeSelection = [aStringValue characterAtIndex:aStringValue.length - 1]; _page->scheduleFullEditorStateUpdate(); } _autocorrectionContextNeedsUpdate = YES; } - (void)insertText:(NSString *)aStringValue alternatives:(NSArray *)alternatives style:(UITextAlternativeStyle)style { if (!alternatives.count) { [self insertText:aStringValue]; return; } BOOL isLowConfidence = style == UITextAlternativeStyleLowConfidence; auto textAlternatives = adoptNS([[NSTextAlternatives alloc] initWithPrimaryString:aStringValue alternativeStrings:alternatives isLowConfidence:isLowConfidence]); WebCore::TextAlternativeWithRange textAlternativeWithRange { textAlternatives.get(), NSMakeRange(0, aStringValue.length) }; WebKit::InsertTextOptions options; options.shouldSimulateKeyboardInput = [self _shouldSimulateKeyboardInputOnTextInsertion]; _page->insertDictatedTextAsync(aStringValue, { }, { textAlternativeWithRange }, WTFMove(options)); _autocorrectionContextNeedsUpdate = YES; } - (BOOL)hasText { auto& editorState = _page->editorState(); return !editorState.isMissingPostLayoutData && editorState.postLayoutData().hasPlainText; } - (void)addTextAlternatives:(NSTextAlternatives *)alternatives { _page->addDictationAlternative({ alternatives, NSMakeRange(0, alternatives.primaryString.length) }); } - (void)removeEmojiAlternatives { _page->dictationAlternativesAtSelection([weakSelf = WeakObjCPtr(self)](auto&& contexts) { auto strongSelf = weakSelf.get(); if (!strongSelf || !strongSelf->_page) return; RefPtr page = strongSelf->_page; Vector contextsToRemove; for (auto context : contexts) { auto alternatives = page->platformDictationAlternatives(context); if (!alternatives) continue; auto originalAlternatives = alternatives.alternativeStrings; auto nonEmojiAlternatives = [NSMutableArray arrayWithCapacity:originalAlternatives.count]; for (NSString *alternative in originalAlternatives) { if (!alternative._containsEmojiOnly) [nonEmojiAlternatives addObject:alternative]; } if (nonEmojiAlternatives.count == originalAlternatives.count) continue; RetainPtr replacement; if (nonEmojiAlternatives.count) replacement = adoptNS([[NSTextAlternatives alloc] initWithPrimaryString:alternatives.primaryString alternativeStrings:nonEmojiAlternatives isLowConfidence:alternatives.isLowConfidence]); else contextsToRemove.append(context); page->pageClient().replaceDictationAlternatives(replacement.get(), context); } page->clearDictationAlternatives(WTFMove(contextsToRemove)); }); } // end of UITextInput protocol implementation static UITextAutocapitalizationType toUITextAutocapitalize(WebCore::AutocapitalizeType webkitType) { switch (webkitType) { case WebCore::AutocapitalizeType::Default: return UITextAutocapitalizationTypeSentences; case WebCore::AutocapitalizeType::None: return UITextAutocapitalizationTypeNone; case WebCore::AutocapitalizeType::Words: return UITextAutocapitalizationTypeWords; case WebCore::AutocapitalizeType::Sentences: return UITextAutocapitalizationTypeSentences; case WebCore::AutocapitalizeType::AllCharacters: return UITextAutocapitalizationTypeAllCharacters; } return UITextAutocapitalizationTypeSentences; } static NSString *contentTypeFromFieldName(WebCore::AutofillFieldName fieldName) { switch (fieldName) { case WebCore::AutofillFieldName::Name: return UITextContentTypeName; case WebCore::AutofillFieldName::HonorificPrefix: return UITextContentTypeNamePrefix; case WebCore::AutofillFieldName::GivenName: return UITextContentTypeMiddleName; case WebCore::AutofillFieldName::AdditionalName: return UITextContentTypeMiddleName; case WebCore::AutofillFieldName::FamilyName: return UITextContentTypeFamilyName; case WebCore::AutofillFieldName::HonorificSuffix: return UITextContentTypeNameSuffix; case WebCore::AutofillFieldName::Nickname: return UITextContentTypeNickname; case WebCore::AutofillFieldName::OrganizationTitle: return UITextContentTypeJobTitle; case WebCore::AutofillFieldName::Organization: return UITextContentTypeOrganizationName; case WebCore::AutofillFieldName::StreetAddress: return UITextContentTypeFullStreetAddress; case WebCore::AutofillFieldName::AddressLine1: return UITextContentTypeStreetAddressLine1; case WebCore::AutofillFieldName::AddressLine2: return UITextContentTypeStreetAddressLine2; case WebCore::AutofillFieldName::AddressLevel3: return UITextContentTypeSublocality; case WebCore::AutofillFieldName::AddressLevel2: return UITextContentTypeAddressCity; case WebCore::AutofillFieldName::AddressLevel1: return UITextContentTypeAddressState; case WebCore::AutofillFieldName::CountryName: return UITextContentTypeCountryName; case WebCore::AutofillFieldName::PostalCode: return UITextContentTypePostalCode; case WebCore::AutofillFieldName::Tel: return UITextContentTypeTelephoneNumber; case WebCore::AutofillFieldName::Email: return UITextContentTypeEmailAddress; case WebCore::AutofillFieldName::URL: return UITextContentTypeURL; case WebCore::AutofillFieldName::Username: return UITextContentTypeUsername; case WebCore::AutofillFieldName::None: case WebCore::AutofillFieldName::NewPassword: case WebCore::AutofillFieldName::CurrentPassword: case WebCore::AutofillFieldName::AddressLine3: case WebCore::AutofillFieldName::AddressLevel4: case WebCore::AutofillFieldName::Country: case WebCore::AutofillFieldName::CcName: case WebCore::AutofillFieldName::CcGivenName: case WebCore::AutofillFieldName::CcAdditionalName: case WebCore::AutofillFieldName::CcFamilyName: case WebCore::AutofillFieldName::CcNumber: case WebCore::AutofillFieldName::CcExp: case WebCore::AutofillFieldName::CcExpMonth: case WebCore::AutofillFieldName::CcExpYear: case WebCore::AutofillFieldName::CcCsc: case WebCore::AutofillFieldName::CcType: case WebCore::AutofillFieldName::TransactionCurrency: case WebCore::AutofillFieldName::TransactionAmount: case WebCore::AutofillFieldName::Language: case WebCore::AutofillFieldName::Bday: case WebCore::AutofillFieldName::BdayDay: case WebCore::AutofillFieldName::BdayMonth: case WebCore::AutofillFieldName::BdayYear: case WebCore::AutofillFieldName::Sex: case WebCore::AutofillFieldName::Photo: case WebCore::AutofillFieldName::TelCountryCode: case WebCore::AutofillFieldName::TelNational: case WebCore::AutofillFieldName::TelAreaCode: case WebCore::AutofillFieldName::TelLocal: case WebCore::AutofillFieldName::TelLocalPrefix: case WebCore::AutofillFieldName::TelLocalSuffix: case WebCore::AutofillFieldName::TelExtension: case WebCore::AutofillFieldName::Impp: break; }; return nil; } // UITextInputPrivate protocol // Direct access to the (private) UITextInputTraits object. - (UITextInputTraits *)textInputTraits { if (!_traits) _traits = adoptNS([[UITextInputTraits alloc] init]); // Do not change traits when dismissing the keyboard. if (_isBlurringFocusedElement) return _traits.get(); [self _updateTextInputTraits:_traits.get()]; return _traits.get(); } - (void)_updateTextInputTraits:(id )traits { traits.secureTextEntry = _focusedElementInformation.elementType == WebKit::InputType::Password || [_formInputSession forceSecureTextEntry]; switch (_focusedElementInformation.enterKeyHint) { case WebCore::EnterKeyHint::Enter: traits.returnKeyType = UIReturnKeyDefault; break; case WebCore::EnterKeyHint::Done: traits.returnKeyType = UIReturnKeyDone; break; case WebCore::EnterKeyHint::Go: traits.returnKeyType = UIReturnKeyGo; break; case WebCore::EnterKeyHint::Next: traits.returnKeyType = UIReturnKeyNext; break; case WebCore::EnterKeyHint::Search: traits.returnKeyType = UIReturnKeySearch; break; case WebCore::EnterKeyHint::Send: traits.returnKeyType = UIReturnKeySend; break; default: { if (!_focusedElementInformation.formAction.isEmpty()) traits.returnKeyType = _focusedElementInformation.elementType == WebKit::InputType::Search ? UIReturnKeySearch : UIReturnKeyGo; } } BOOL disableAutocorrectAndAutocapitalize = _focusedElementInformation.elementType == WebKit::InputType::Password || _focusedElementInformation.elementType == WebKit::InputType::Email || _focusedElementInformation.elementType == WebKit::InputType::URL || _focusedElementInformation.formAction.contains("login"_s); if ([traits respondsToSelector:@selector(setAutocapitalizationType:)]) traits.autocapitalizationType = disableAutocorrectAndAutocapitalize ? UITextAutocapitalizationTypeNone : toUITextAutocapitalize(_focusedElementInformation.autocapitalizeType); if ([traits respondsToSelector:@selector(setAutocorrectionType:)]) traits.autocorrectionType = disableAutocorrectAndAutocapitalize ? UITextAutocorrectionTypeNo : (_focusedElementInformation.isAutocorrect ? UITextAutocorrectionTypeYes : UITextAutocorrectionTypeNo); if (!_focusedElementInformation.isSpellCheckingEnabled) { if ([traits respondsToSelector:@selector(setSmartQuotesType:)]) traits.smartQuotesType = UITextSmartQuotesTypeNo; if ([traits respondsToSelector:@selector(setSmartDashesType:)]) traits.smartDashesType = UITextSmartDashesTypeNo; traits.spellCheckingType = UITextSpellCheckingTypeNo; } switch (_focusedElementInformation.inputMode) { case WebCore::InputMode::None: case WebCore::InputMode::Unspecified: switch (_focusedElementInformation.elementType) { case WebKit::InputType::Phone: traits.keyboardType = UIKeyboardTypePhonePad; break; case WebKit::InputType::URL: traits.keyboardType = UIKeyboardTypeURL; break; case WebKit::InputType::Email: traits.keyboardType = UIKeyboardTypeEmailAddress; break; case WebKit::InputType::Number: traits.keyboardType = UIKeyboardTypeNumbersAndPunctuation; break; case WebKit::InputType::NumberPad: traits.keyboardType = UIKeyboardTypeNumberPad; break; case WebKit::InputType::None: case WebKit::InputType::ContentEditable: case WebKit::InputType::Text: case WebKit::InputType::Password: case WebKit::InputType::TextArea: case WebKit::InputType::Search: case WebKit::InputType::Date: case WebKit::InputType::DateTimeLocal: case WebKit::InputType::Month: case WebKit::InputType::Week: case WebKit::InputType::Time: case WebKit::InputType::Select: case WebKit::InputType::Drawing: #if ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Color: #endif traits.keyboardType = UIKeyboardTypeDefault; } break; case WebCore::InputMode::Text: traits.keyboardType = UIKeyboardTypeDefault; break; case WebCore::InputMode::Telephone: traits.keyboardType = UIKeyboardTypePhonePad; break; case WebCore::InputMode::Url: traits.keyboardType = UIKeyboardTypeURL; break; case WebCore::InputMode::Email: traits.keyboardType = UIKeyboardTypeEmailAddress; break; case WebCore::InputMode::Numeric: traits.keyboardType = UIKeyboardTypeNumberPad; break; case WebCore::InputMode::Decimal: traits.keyboardType = UIKeyboardTypeDecimalPad; break; case WebCore::InputMode::Search: traits.keyboardType = UIKeyboardTypeWebSearch; break; } #if HAVE(PEPPER_UI_CORE) traits.textContentType = self.textContentTypeForQuickboard; #else traits.textContentType = contentTypeFromFieldName(_focusedElementInformation.autofillFieldName); #endif auto privateTraits = (id )traits; if ([privateTraits respondsToSelector:@selector(setIsSingleLineDocument:)]) { switch (_focusedElementInformation.elementType) { case WebKit::InputType::ContentEditable: case WebKit::InputType::TextArea: privateTraits.isSingleLineDocument = NO; break; #if ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Color: #endif case WebKit::InputType::Date: case WebKit::InputType::DateTimeLocal: case WebKit::InputType::Drawing: case WebKit::InputType::Email: case WebKit::InputType::Month: case WebKit::InputType::Number: case WebKit::InputType::NumberPad: case WebKit::InputType::Password: case WebKit::InputType::Phone: case WebKit::InputType::Search: case WebKit::InputType::Select: case WebKit::InputType::Text: case WebKit::InputType::Time: case WebKit::InputType::URL: case WebKit::InputType::Week: privateTraits.isSingleLineDocument = YES; break; case WebKit::InputType::None: break; } } if ([privateTraits respondsToSelector:@selector(setShortcutConversionType:)]) privateTraits.shortcutConversionType = _focusedElementInformation.elementType == WebKit::InputType::Password ? UITextShortcutConversionTypeNo : UITextShortcutConversionTypeDefault; if ([traits isKindOfClass:UITextInputTraits.class]) [self _updateInteractionTintColor:(UITextInputTraits *)traits]; } - (UITextInteractionAssistant *)interactionAssistant { return _textInteractionAssistant.get(); } // NSRange support. Would like to deprecate to the extent possible, although some support // (i.e. selectionRange) has shipped as API. - (NSRange)selectionRange { return NSMakeRange(NSNotFound, 0); } - (CGRect)rectForNSRange:(NSRange)range { return CGRectZero; } - (NSRange)_markedTextNSRange { return NSMakeRange(NSNotFound, 0); } // DOM range support. - (DOMRange *)selectedDOMRange { return nil; } - (void)setSelectedDOMRange:(DOMRange *)range affinityDownstream:(BOOL)affinityDownstream { } // Modify text without starting a new undo grouping. - (void)replaceRangeWithTextWithoutClosingTyping:(UITextRange *)range replacementText:(NSString *)text { } // Caret rect support. Shouldn't be necessary, but firstRectForRange doesn't offer precisely // the same functionality. - (CGRect)rectContainingCaretSelection { return CGRectZero; } - (BOOL)_isTextInputContextFocused:(_WKTextInputContext *)context { ASSERT(context); // We ignore bounding rect changes as the bounding rect of the focused element is not kept up-to-date. return self._hasFocusedElement && context._textInputContext.isSameElement(_focusedElementInformation.elementContext); } - (void)_focusTextInputContext:(_WKTextInputContext *)context placeCaretAt:(CGPoint)point completionHandler:(void (^)(UIResponder *))completionHandler { ASSERT(context); // This function can be called more than once during a text interaction (e.g. ). if (![self becomeFirstResponder]) { completionHandler(nil); return; } if ([self _isTextInputContextFocused:context]) { completionHandler(_focusedElementInformation.isReadOnly ? nil : self); return; } _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; auto checkFocusedElement = [weakSelf = WeakObjCPtr { self }, context = adoptNS([context copy]), completionHandler = makeBlockPtr(completionHandler)] (bool success) { auto strongSelf = weakSelf.get(); if (!strongSelf) { completionHandler(nil); return; } bool isFocused = success && [strongSelf _isTextInputContextFocused:context.get()]; bool isEditable = success && !strongSelf->_focusedElementInformation.isReadOnly; strongSelf->_textInteractionDidChangeFocusedElement |= isFocused; strongSelf->_usingGestureForSelection = NO; completionHandler(isFocused && isEditable ? strongSelf.get() : nil); }; _page->focusTextInputContextAndPlaceCaret(context._textInputContext, WebCore::IntPoint { point }, WTFMove(checkFocusedElement)); } - (void)_requestTextInputContextsInRect:(CGRect)rect completionHandler:(void (^)(NSArray<_WKTextInputContext *> *))completionHandler { WebCore::FloatRect searchRect { rect }; #if ENABLE(EDITABLE_REGION) bool hitInteractionRect = self._hasFocusedElement && searchRect.inclusivelyIntersects(_focusedElementInformation.interactionRect); if (!self.webView._editable && !hitInteractionRect && !WebKit::mayContainEditableElementsInRect(self, rect)) { completionHandler(@[ ]); return; } #endif _page->textInputContextsInRect(searchRect, [weakSelf = WeakObjCPtr(self), completionHandler = makeBlockPtr(completionHandler)] (const Vector& contexts) { auto strongSelf = weakSelf.get(); if (!strongSelf || contexts.isEmpty()) { completionHandler(@[ ]); return; } completionHandler(createNSArray(contexts, [] (const auto& context) { return adoptNS([[_WKTextInputContext alloc] _initWithTextInputContext:context]); }).get()); }); } - (void)_willBeginTextInteractionInTextInputContext:(_WKTextInputContext *)context { ASSERT(context); _page->setCanShowPlaceholder(context._textInputContext, false); ++_activeTextInteractionCount; if (_activeTextInteractionCount > 1) return; _textInteractionDidChangeFocusedElement = NO; _page->setShouldRevealCurrentSelectionAfterInsertion(false); _autocorrectionContextNeedsUpdate = YES; _usingGestureForSelection = YES; } - (void)_didFinishTextInteractionInTextInputContext:(_WKTextInputContext *)context { ASSERT(context); _page->setCanShowPlaceholder(context._textInputContext, true); ASSERT(_activeTextInteractionCount > 0); --_activeTextInteractionCount; if (_activeTextInteractionCount) return; _usingGestureForSelection = NO; if (_textInteractionDidChangeFocusedElement) { // Mark to zoom to reveal the newly focused element on the next editor state update. // Then tell the web process to reveal the current selection, which will send us (the // UI process) an editor state update. _page->setWaitingForPostLayoutEditorStateUpdateAfterFocusingElement(true); _textInteractionDidChangeFocusedElement = NO; } _page->setShouldRevealCurrentSelectionAfterInsertion(true); } - (void)modifierFlagsDidChangeFrom:(UIKeyModifierFlags)oldFlags to:(UIKeyModifierFlags)newFlags { auto dispatchSyntheticFlagsChangedEvents = [&] (UIKeyModifierFlags flags, bool keyDown) { if (flags & UIKeyModifierShift) [self handleKeyWebEvent:adoptNS([[WKSyntheticFlagsChangedWebEvent alloc] initWithShiftState:keyDown]).get()]; if (flags & UIKeyModifierAlphaShift) [self handleKeyWebEvent:adoptNS([[WKSyntheticFlagsChangedWebEvent alloc] initWithCapsLockState:keyDown]).get()]; }; UIKeyModifierFlags removedFlags = oldFlags & ~newFlags; UIKeyModifierFlags addedFlags = newFlags & ~oldFlags; if (removedFlags) dispatchSyntheticFlagsChangedEvents(removedFlags, false); if (addedFlags) dispatchSyntheticFlagsChangedEvents(addedFlags, true); } - (BOOL)shouldSuppressUpdateCandidateView { return _candidateViewNeedsUpdate; } // Web events. - (BOOL)requiresKeyEvents { return YES; } - (void)_handleKeyUIEvent:(::UIEvent *)event { bool isHardwareKeyboardEvent = !!event._hidEvent; // We only want to handle key event from the hardware keyboard when we are // first responder and we are not interacting with editable content. if ([self isFirstResponder] && isHardwareKeyboardEvent && (_inputPeripheral || !_page->editorState().isContentEditable)) { if ([_inputPeripheral respondsToSelector:@selector(handleKeyEvent:)]) { if ([_inputPeripheral handleKeyEvent:event]) return; } if (!_seenHardwareKeyDownInNonEditableElement) { _seenHardwareKeyDownInNonEditableElement = YES; [self reloadInputViews]; } [super _handleKeyUIEvent:event]; return; } [super _handleKeyUIEvent:event]; } - (void)generateSyntheticEditingCommand:(WebKit::SyntheticEditingCommandType)command { _page->generateSyntheticEditingCommand(command); } - (void)handleKeyWebEvent:(::WebEvent *)theEvent { _page->handleKeyboardEvent(WebKit::NativeWebKeyboardEvent(theEvent, WebKit::NativeWebKeyboardEvent::HandledByInputMethod::No)); } - (void)handleKeyWebEvent:(::WebEvent *)theEvent withCompletionHandler:(void (^)(::WebEvent *theEvent, BOOL wasHandled))completionHandler { if (!isUIThread()) { RELEASE_LOG_FAULT(KeyHandling, "%s was invoked on a background thread.", __PRETTY_FUNCTION__); completionHandler(theEvent, NO); return; } [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::DeniedForGesture]; using HandledByInputMethod = WebKit::NativeWebKeyboardEvent::HandledByInputMethod; auto* keyboard = [UIKeyboardImpl sharedInstance]; if ((_page->editorState().isContentEditable || _treatAsContentEditableUntilNextEditorStateUpdate) && [keyboard handleKeyInputMethodCommandForCurrentEvent]) { completionHandler(theEvent, YES); _page->handleKeyboardEvent(WebKit::NativeWebKeyboardEvent(theEvent, HandledByInputMethod::Yes)); return; } if (_page->handleKeyboardEvent(WebKit::NativeWebKeyboardEvent(theEvent, HandledByInputMethod::No))) _keyWebEventHandler = makeBlockPtr(completionHandler); else completionHandler(theEvent, NO); } - (void)_didHandleKeyEvent:(::WebEvent *)event eventWasHandled:(BOOL)eventWasHandled { if ([event isKindOfClass:[WKSyntheticFlagsChangedWebEvent class]]) return; if (!(event.keyboardFlags & WebEventKeyboardInputModifierFlagsChanged)) [_keyboardScrollingAnimator handleKeyEvent:event]; if (auto handler = WTFMove(_keyWebEventHandler)) { handler(event, eventWasHandled); return; } } - (BOOL)_interpretKeyEvent:(::WebEvent *)event isCharEvent:(BOOL)isCharEvent { if (event.keyboardFlags & WebEventKeyboardInputModifierFlagsChanged) return NO; BOOL contentEditable = _page->editorState().isContentEditable; if (!contentEditable && event.isTabKey) return NO; if ([_keyboardScrollingAnimator beginWithEvent:event] || [_keyboardScrollingAnimator scrollTriggeringKeyIsPressed]) return YES; UIKeyboardImpl *keyboard = [UIKeyboardImpl sharedInstance]; if (!isCharEvent && [keyboard handleKeyTextCommandForCurrentEvent]) return YES; if (isCharEvent && [keyboard handleKeyAppCommandForCurrentEvent]) return YES; // Don't insert character for an unhandled Command-key key command. This matches iOS and Mac platform conventions. if (event.modifierFlags & WebEventFlagMaskCommandKey) return NO; NSString *characters = event.characters; if (!characters.length) return NO; switch ([characters characterAtIndex:0]) { case NSBackspaceCharacter: case NSDeleteCharacter: if (contentEditable) { [keyboard deleteFromInputWithFlags:event.keyboardFlags]; return YES; } break; case NSEnterCharacter: case NSCarriageReturnCharacter: if (contentEditable && isCharEvent) { // Map \r from HW keyboard to \n to match the behavior of the soft keyboard. [keyboard addInputString:@"\n" withFlags:0 withInputManagerHint:nil]; return YES; } break; default: if (contentEditable && isCharEvent) { [keyboard addInputString:event.characters withFlags:event.keyboardFlags withInputManagerHint:event.inputManagerHint]; return YES; } break; } return NO; } - (NSArray *)filePickerAcceptedTypeIdentifiers { if (!_fileUploadPanel) return @[]; return [_fileUploadPanel acceptedTypeIdentifiers]; } - (void)dismissFilePicker { [_fileUploadPanel dismiss]; } - (BOOL)isScrollableForKeyboardScrollViewAnimator:(WKKeyboardScrollViewAnimator *)animator { if (_page->editorState().isContentEditable) return NO; if (_focusedElementInformation.elementType == WebKit::InputType::Select) return NO; if (!self.webView.scrollView.scrollEnabled) return NO; return YES; } - (CGFloat)keyboardScrollViewAnimator:(WKKeyboardScrollViewAnimator *)animator distanceForIncrement:(WebCore::ScrollGranularity)increment inDirection:(WebCore::ScrollDirection)direction { BOOL directionIsHorizontal = direction == WebCore::ScrollDirection::ScrollLeft || direction == WebCore::ScrollDirection::ScrollRight; switch (increment) { case WebCore::ScrollGranularity::Document: { CGSize documentSize = [self convertRect:self.bounds toView:self.webView].size; return directionIsHorizontal ? documentSize.width : documentSize.height; } case WebCore::ScrollGranularity::Page: { CGSize pageSize = [self convertSize:CGSizeMake(0, WebCore::Scrollbar::pageStep(_page->unobscuredContentRect().height(), self.bounds.size.height)) toView:self.webView]; return directionIsHorizontal ? pageSize.width : pageSize.height; } case WebCore::ScrollGranularity::Line: return [self convertSize:CGSizeMake(0, WebCore::Scrollbar::pixelsPerLineStep()) toView:self.webView].height; case WebCore::ScrollGranularity::Pixel: return 0; } ASSERT_NOT_REACHED(); return 0; } - (void)keyboardScrollViewAnimatorWillScroll:(WKKeyboardScrollViewAnimator *)animator { [self willStartZoomOrScroll]; } - (void)keyboardScrollViewAnimatorDidFinishScrolling:(WKKeyboardScrollViewAnimator *)animator { [_webView _didFinishScrolling:self.webView.scrollView]; } - (void)executeEditCommandWithCallback:(NSString *)commandName { _autocorrectionContextNeedsUpdate = YES; // FIXME: Editing commands are not considered by WebKit as user initiated even if they are the result // of keydown or keyup. We need to query the keyboard to determine if this was called from the keyboard // or not to know whether to tell WebKit to treat this command as user initiated or not. [self beginSelectionChange]; RetainPtr view = self; _page->executeEditCommand(commandName, { }, [view] { [view endSelectionChange]; }); } - (void)_deleteByWord { [self executeEditCommandWithCallback:@"deleteWordBackward"]; } - (void)_deleteForwardByWord { [self executeEditCommandWithCallback:@"deleteWordForward"]; } - (void)_deleteToStartOfLine { [self executeEditCommandWithCallback:@"deleteToBeginningOfLine"]; } - (void)_deleteToEndOfLine { [self executeEditCommandWithCallback:@"deleteToEndOfLine"]; } - (void)_deleteForwardAndNotify:(BOOL)notify { [self executeEditCommandWithCallback:@"deleteForward"]; } - (void)_deleteToEndOfParagraph { [self executeEditCommandWithCallback:@"deleteToEndOfParagraph"]; } - (void)_transpose { [self executeEditCommandWithCallback:@"transpose"]; } - (UITextInputArrowKeyHistory *)_moveUp:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveUpAndModifySelection" : @"moveUp"]; return nil; } - (UITextInputArrowKeyHistory *)_moveDown:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveDownAndModifySelection" : @"moveDown"]; return nil; } - (UITextInputArrowKeyHistory *)_moveLeft:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending? @"moveLeftAndModifySelection" : @"moveLeft"]; return nil; } - (UITextInputArrowKeyHistory *)_moveRight:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveRightAndModifySelection" : @"moveRight"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToStartOfWord:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveWordBackwardAndModifySelection" : @"moveWordBackward"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToStartOfParagraph:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveBackwardAndModifySelection" : @"moveBackward"]; [self executeEditCommandWithCallback:extending ? @"moveToBeginningOfParagraphAndModifySelection" : @"moveToBeginningOfParagraph"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToStartOfLine:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveToBeginningOfLineAndModifySelection" : @"moveToBeginningOfLine"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToStartOfDocument:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveToBeginningOfDocumentAndModifySelection" : @"moveToBeginningOfDocument"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToEndOfWord:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveWordForwardAndModifySelection" : @"moveWordForward"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToEndOfParagraph:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveForwardAndModifySelection" : @"moveForward"]; [self executeEditCommandWithCallback:extending ? @"moveToEndOfParagraphAndModifySelection" : @"moveToEndOfParagraph"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToEndOfLine:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveToEndOfLineAndModifySelection" : @"moveToEndOfLine"]; return nil; } - (UITextInputArrowKeyHistory *)_moveToEndOfDocument:(BOOL)extending withHistory:(UITextInputArrowKeyHistory *)history { [self executeEditCommandWithCallback:extending ? @"moveToEndOfDocumentAndModifySelection" : @"moveToEndOfDocument"]; return nil; } // Sets a buffer to make room for autocorrection views - (void)setBottomBufferHeight:(CGFloat)bottomBuffer { } - (UIView *)automaticallySelectedOverlay { return [self unscaledView]; } - (UITextGranularity)selectionGranularity { return UITextGranularityCharacter; } // Should return an array of NSDictionary objects that key/value paries for the final text, correction identifier and // alternative selection counts using the keys defined at the top of this header. - (NSArray *)metadataDictionariesForDictationResults { return nil; } // The can all be (and have been) trivially implemented in terms of UITextInput. Deprecate and remove. - (void)moveBackward:(unsigned)count { } - (void)moveForward:(unsigned)count { } - (unichar)characterBeforeCaretSelection { return 0; } - (NSString *)wordContainingCaretSelection { return nil; } - (DOMRange *)wordRangeContainingCaretSelection { return nil; } - (void)setMarkedText:(NSString *)text { } - (BOOL)hasContent { auto& editorState = _page->editorState(); return !editorState.selectionIsNone && editorState.postLayoutData().hasContent; } - (void)selectAll { } - (UIColor *)textColorForCaretSelection { return [UIColor blackColor]; } - (UIFont *)fontForCaretSelection { UIFont *font = _autocorrectionData.font.get(); double zoomScale = self._contentZoomScale; return std::abs(zoomScale - 1) > FLT_EPSILON ? [font fontWithSize:font.pointSize * zoomScale] : font; } - (BOOL)hasSelection { return NO; } - (BOOL)isPosition:(UITextPosition *)position atBoundary:(UITextGranularity)granularity inDirection:(UITextDirection)direction { if (granularity == UITextGranularityParagraph) { if (direction == UITextStorageDirectionBackward && [position isEqual:self.selectedTextRange.start]) return _page->editorState().postLayoutData().selectionStartIsAtParagraphBoundary; if (direction == UITextStorageDirectionForward && [position isEqual:self.selectedTextRange.end]) return _page->editorState().postLayoutData().selectionEndIsAtParagraphBoundary; } return NO; } - (UITextPosition *)positionFromPosition:(UITextPosition *)position toBoundary:(UITextGranularity)granularity inDirection:(UITextDirection)direction { return nil; } - (BOOL)isPosition:(UITextPosition *)position withinTextUnit:(UITextGranularity)granularity inDirection:(UITextDirection)direction { return NO; } - (UITextRange *)rangeEnclosingPosition:(UITextPosition *)position withGranularity:(UITextGranularity)granularity inDirection:(UITextDirection)direction { return nil; } - (void)takeTraitsFrom:(UITextInputTraits *)traits { [[self textInputTraits] takeTraitsFrom:traits]; } - (void)_showKeyboard { [self setUpTextSelectionAssistant]; if (self.isFirstResponder && !_suppressSelectionAssistantReasons) [_textInteractionAssistant activateSelection]; #if !PLATFORM(WATCHOS) [self reloadInputViews]; #endif } - (void)_hideKeyboard { SetForScope isHidingKeyboardScope { _isHidingKeyboard, YES }; self.inputDelegate = nil; [self setUpTextSelectionAssistant]; [_textInteractionAssistant deactivateSelection]; [_formAccessoryView hideAutoFillButton]; // FIXME: Does it make sense to call -reloadInputViews on watchOS? [self reloadInputViews]; if (_formAccessoryView) [self _updateAccessory]; } #if ENABLE(IOS_FORM_CONTROL_REFRESH) - (BOOL)_formControlRefreshEnabled { if (!_page) return NO; return _page->preferences().iOSFormControlRefreshEnabled(); } #endif - (const WebKit::FocusedElementInformation&)focusedElementInformation { return _focusedElementInformation; } - (Vector&)focusedSelectElementOptions { return _focusedElementInformation.selectOptions; } // Note that selectability is also affected by the CSS property user-select. static bool mayContainSelectableText(WebKit::InputType type) { switch (type) { case WebKit::InputType::None: // The following types have custom UI and do not look or behave like a text field. #if ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Color: #endif case WebKit::InputType::Date: case WebKit::InputType::DateTimeLocal: case WebKit::InputType::Drawing: case WebKit::InputType::Month: case WebKit::InputType::Select: case WebKit::InputType::Time: return false; // The following types look and behave like a text field. case WebKit::InputType::ContentEditable: case WebKit::InputType::Email: case WebKit::InputType::Number: case WebKit::InputType::NumberPad: case WebKit::InputType::Password: case WebKit::InputType::Phone: case WebKit::InputType::Search: case WebKit::InputType::Text: case WebKit::InputType::TextArea: case WebKit::InputType::URL: case WebKit::InputType::Week: return true; } } - (BOOL)_shouldShowKeyboardForElement:(const WebKit::FocusedElementInformation&)information { if (information.inputMode == WebCore::InputMode::None) return NO; return [self _shouldShowKeyboardForElementIgnoringInputMode:information]; } - (BOOL)_shouldShowKeyboardForElementIgnoringInputMode:(const WebKit::FocusedElementInformation&)information { return mayContainSelectableText(information.elementType) || [self _elementTypeRequiresAccessoryView:information.elementType]; } static RetainPtr> createInputPeripheralWithView(WebKit::InputType type, WKContentView *view) { #if PLATFORM(WATCHOS) UNUSED_PARAM(type); UNUSED_PARAM(view); return nil; #else switch (type) { case WebKit::InputType::Select: return adoptNS([[WKFormSelectControl alloc] initWithView:view]); #if ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Color: return adoptNS([[WKFormColorControl alloc] initWithView:view]); #endif // ENABLE(INPUT_TYPE_COLOR) case WebKit::InputType::Date: case WebKit::InputType::DateTimeLocal: case WebKit::InputType::Month: case WebKit::InputType::Time: return adoptNS([[WKDateTimeInputControl alloc] initWithView:view]); default: return nil; } #endif } - (void)_elementDidFocus:(const WebKit::FocusedElementInformation&)information userIsInteracting:(BOOL)userIsInteracting blurPreviousNode:(BOOL)blurPreviousNode activityStateChanges:(OptionSet)activityStateChanges userObject:(NSObject *)userObject { SetForScope isChangingFocusForScope { _isChangingFocus, self._hasFocusedElement }; SetForScope isFocusingElementWithKeyboardForScope { _isFocusingElementWithKeyboard, [self _shouldShowKeyboardForElement:information] }; auto runModalJavaScriptDialogCallbackIfNeeded = makeScopeExit([&] { if (auto callback = std::exchange(_pendingRunModalJavaScriptDialogCallback, { })) callback(); }); auto inputViewUpdateDeferrer = std::exchange(_inputViewUpdateDeferrer, nullptr); _autocorrectionContextNeedsUpdate = YES; _didAccessoryTabInitiateFocus = _isChangingFocusUsingAccessoryTab; id <_WKInputDelegate> inputDelegate = [_webView _inputDelegate]; RetainPtr focusedElementInfo = adoptNS([[WKFocusedElementInfo alloc] initWithFocusedElementInformation:information isUserInitiated:userIsInteracting userObject:userObject]); _WKFocusStartsInputSessionPolicy startInputSessionPolicy = _WKFocusStartsInputSessionPolicyAuto; if ([inputDelegate respondsToSelector:@selector(_webView:focusShouldStartInputSession:)]) { if ([inputDelegate _webView:self.webView focusShouldStartInputSession:focusedElementInfo.get()]) startInputSessionPolicy = _WKFocusStartsInputSessionPolicyAllow; else startInputSessionPolicy = _WKFocusStartsInputSessionPolicyDisallow; } if ([inputDelegate respondsToSelector:@selector(_webView:decidePolicyForFocusedElement:)]) startInputSessionPolicy = [inputDelegate _webView:self.webView decidePolicyForFocusedElement:focusedElementInfo.get()]; BOOL shouldShowInputView = [&] { switch (startInputSessionPolicy) { case _WKFocusStartsInputSessionPolicyAuto: // The default behavior is to allow node assistance if the user is interacting. // We also allow node assistance if the keyboard already is showing, unless we're in extra zoom mode. if (userIsInteracting) return YES; if (self.isFirstResponder || _becomingFirstResponder) { // When the software keyboard is being used to enter an url, only the focus activity state is changing. // In this case, auto focus on the page being navigated to should be disabled, unless a hardware // keyboard is attached. if (activityStateChanges && activityStateChanges != WebCore::ActivityState::IsFocused) return YES; #if HAVE(PEPPER_UI_CORE) if (_isChangingFocus && ![_focusedFormControlView isHidden]) return YES; #else if (_isChangingFocus) return YES; if ([self _shouldShowKeyboardForElementIgnoringInputMode:information] && UIKeyboard.isInHardwareKeyboardMode) return YES; #endif } return NO; case _WKFocusStartsInputSessionPolicyAllow: return YES; case _WKFocusStartsInputSessionPolicyDisallow: return NO; default: ASSERT_NOT_REACHED(); return NO; } }(); // Do not present input peripherals if a validation message is being displayed. if (information.isFocusingWithValidationMessage && !_isFocusingElementWithKeyboard) shouldShowInputView = NO; if (blurPreviousNode) { // Defer view updates until the end of this function to avoid a noticeable flash when switching focus // between elements that require the keyboard. if (!inputViewUpdateDeferrer) inputViewUpdateDeferrer = makeUnique(self); [self _elementDidBlur]; } if (!shouldShowInputView || information.elementType == WebKit::InputType::None) { _page->setIsShowingInputViewForFocusedElement(false); return; } _page->setIsShowingInputViewForFocusedElement(true); // FIXME: We should remove this check when we manage to send ElementDidFocus from the WebProcess // only when it is truly time to show the keyboard. if (self._hasFocusedElement && _focusedElementInformation.elementContext.isSameElement(information.elementContext)) { if (_inputPeripheral) { if (!self.isFirstResponder) [self becomeFirstResponder]; [self accessoryOpen]; } return; } [_webView _resetFocusPreservationCount]; _focusRequiresStrongPasswordAssistance = NO; _additionalContextForStrongPasswordAssistance = nil; if ([inputDelegate respondsToSelector:@selector(_webView:focusRequiresStrongPasswordAssistance:)]) _focusRequiresStrongPasswordAssistance = [inputDelegate _webView:self.webView focusRequiresStrongPasswordAssistance:focusedElementInfo.get()]; if ([inputDelegate respondsToSelector:@selector(_webViewAdditionalContextForStrongPasswordAssistance:)]) _additionalContextForStrongPasswordAssistance = [inputDelegate _webViewAdditionalContextForStrongPasswordAssistance:self.webView]; else _additionalContextForStrongPasswordAssistance = @{ }; bool delegateImplementsWillStartInputSession = [inputDelegate respondsToSelector:@selector(_webView:willStartInputSession:)]; bool delegateImplementsDidStartInputSession = [inputDelegate respondsToSelector:@selector(_webView:didStartInputSession:)]; if (delegateImplementsWillStartInputSession || delegateImplementsDidStartInputSession) { [_formInputSession invalidate]; _formInputSession = adoptNS([[WKFormInputSession alloc] initWithContentView:self focusedElementInfo:focusedElementInfo.get() requiresStrongPasswordAssistance:_focusRequiresStrongPasswordAssistance]); } if (delegateImplementsWillStartInputSession) [inputDelegate _webView:self.webView willStartInputSession:_formInputSession.get()]; BOOL requiresKeyboard = mayContainSelectableText(information.elementType); BOOL editableChanged = [self setIsEditable:requiresKeyboard]; _focusedElementInformation = information; _traits = nil; if (![self isFirstResponder]) [self becomeFirstResponder]; if (!_suppressSelectionAssistantReasons && requiresKeyboard && activityStateChanges.contains(WebCore::ActivityState::IsFocused)) { _treatAsContentEditableUntilNextEditorStateUpdate = YES; [_textInteractionAssistant activateSelection]; _page->restoreSelectionInFocusedEditableElement(); _page->scheduleFullEditorStateUpdate(); } _inputPeripheral = createInputPeripheralWithView(_focusedElementInformation.elementType, self); _waitingForKeyboardToStartAnimatingInAfterElementFocus = requiresKeyboard; #if HAVE(PEPPER_UI_CORE) [self addFocusedFormControlOverlay]; if (!_isChangingFocus) [self presentViewControllerForCurrentFocusedElement]; #else [self reloadInputViews]; #endif if (requiresKeyboard) { [self _showKeyboard]; if (!self.window.keyWindow) [self.window makeKeyWindow]; } // The custom fixed position rect behavior is affected by -isFocusingElement, so if that changes we need to recompute rects. if (editableChanged) [_webView _scheduleVisibleContentRectUpdate]; // For elements that have selectable content (e.g. text field) we need to wait for the web process to send an up-to-date // selection rect before we can zoom and reveal the selection. Non-selectable elements (e.g.