| /* |
| * 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 "CompletionHandlerCallChecker.h" |
| #import "DocumentEditingContext.h" |
| #import "InputViewUpdateDeferrer.h" |
| #import "InsertTextOptions.h" |
| #import "Logging.h" |
| #import "NativeWebKeyboardEvent.h" |
| #import "NativeWebTouchEvent.h" |
| #import "RemoteLayerTreeDrawingAreaProxy.h" |
| #import "RemoteLayerTreeViews.h" |
| #import "RemoteScrollingCoordinatorProxy.h" |
| #import "SmartMagnificationController.h" |
| #import "TextChecker.h" |
| #import "TextInputSPI.h" |
| #import "TextRecognitionUpdateResult.h" |
| #import "TextRecognitionUtilities.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 "WKHoverPlatter.h" |
| #import "WKHoverPlatterParameters.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 "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 <CoreText/CTFont.h> |
| #import <CoreText/CTFontDescriptor.h> |
| #import <MobileCoreServices/UTCoreTypes.h> |
| #import <UniformTypeIdentifiers/UTCoreTypes.h> |
| #import <WebCore/AppHighlight.h> |
| #import <WebCore/ColorCocoa.h> |
| #import <WebCore/ColorSerialization.h> |
| #import <WebCore/CompositionHighlight.h> |
| #import <WebCore/DOMPasteAccess.h> |
| #import <WebCore/DataDetection.h> |
| #import <WebCore/FloatQuad.h> |
| #import <WebCore/FloatRect.h> |
| #import <WebCore/FontAttributeChanges.h> |
| #import <WebCore/InputMode.h> |
| #import <WebCore/KeyEventCodesIOS.h> |
| #import <WebCore/KeyboardScroll.h> |
| #import <WebCore/LocalizedStrings.h> |
| #import <WebCore/MIMETypeRegistry.h> |
| #import <WebCore/NotImplemented.h> |
| #import <WebCore/Pasteboard.h> |
| #import <WebCore/Path.h> |
| #import <WebCore/PathUtilities.h> |
| #import <WebCore/PromisedAttachmentInfo.h> |
| #import <WebCore/RuntimeApplicationChecks.h> |
| #import <WebCore/ScrollTypes.h> |
| #import <WebCore/Scrollbar.h> |
| #import <WebCore/ShareData.h> |
| #import <WebCore/TextAlternativeWithRange.h> |
| #import <WebCore/TextIndicator.h> |
| #import <WebCore/TextIndicatorWindow.h> |
| #import <WebCore/TextRecognitionResult.h> |
| #import <WebCore/TouchAction.h> |
| #import <WebCore/UTIUtilities.h> |
| #import <WebCore/VisibleSelection.h> |
| #import <WebCore/WebCoreCALayerExtras.h> |
| #import <WebCore/WebEvent.h> |
| #import <WebCore/WebTextIndicatorLayer.h> |
| #import <WebCore/WritingDirection.h> |
| #import <WebKit/WebSelectionRect.h> // FIXME: WebKit should not include WebKitLegacy headers! |
| #import <pal/spi/cg/CoreGraphicsSPI.h> |
| #import <pal/spi/cocoa/DataDetectorsCoreSPI.h> |
| #import <pal/spi/cocoa/LaunchServicesSPI.h> |
| #import <pal/spi/cocoa/NSAttributedStringSPI.h> |
| #import <pal/spi/cocoa/QuartzCoreSPI.h> |
| #import <pal/spi/ios/DataDetectorsUISPI.h> |
| #import <pal/spi/ios/GraphicsServicesSPI.h> |
| #import <pal/spi/ios/ManagedConfigurationSPI.h> |
| #import <wtf/BlockObjCExceptions.h> |
| #import <wtf/BlockPtr.h> |
| #import <wtf/Scope.h> |
| #import <wtf/SetForScope.h> |
| #import <wtf/WeakObjCPtr.h> |
| #import <wtf/cocoa/NSURLExtras.h> |
| #import <wtf/cocoa/RuntimeApplicationChecksCocoa.h> |
| #import <wtf/cocoa/TypeCastsCocoa.h> |
| #import <wtf/cocoa/VectorCocoa.h> |
| #import <wtf/text/TextStream.h> |
| |
| #if ENABLE(DRAG_SUPPORT) |
| #import <WebCore/DragData.h> |
| #import <WebCore/DragItem.h> |
| #import <WebCore/PlatformPasteboard.h> |
| #import <WebCore/WebItemProviderPasteboard.h> |
| #endif |
| |
| #if HAVE(LOOKUP_GESTURE_RECOGNIZER) |
| #import <UIKit/_UILookupGestureRecognizer.h> |
| #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 ENABLE(HOVER_GESTURE_RECOGNIZER) |
| #import <WebKitAdditions/WKHoverGestureRecognizer.h> |
| #endif |
| |
| #import <pal/cocoa/VisionKitCoreSoftLink.h> |
| #import <pal/ios/ManagedConfigurationSoftLink.h> |
| #import <pal/ios/QuickLookSoftLink.h> |
| |
| #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) <UIPointerInteractionDelegate_ForWebKitOnly> |
| @end |
| #endif |
| |
| #if HAVE(PENCILKIT_TEXT_INPUT) |
| @interface WKContentView (WKUIIndirectScribbleInteractionDelegate) <UIIndirectScribbleInteractionDelegate> |
| @end |
| #endif |
| |
| #if HAVE(PEPPER_UI_CORE) |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| @interface WKContentView (QuickboardControllerSupport) <PUICQuickboardControllerDelegate> |
| @end |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| |
| @interface WKContentView (WatchSupport) <WKFocusedFormControlViewDelegate, WKSelectMenuListViewControllerDelegate, WKTextInputListViewControllerDelegate> |
| @end |
| #endif // HAVE(PEPPER_UI_CORE) |
| |
| static void *WKContentViewKVOTransformContext = &WKContentViewKVOTransformContext; |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| |
| @interface WKContentView (ImageAnalysis) <WKImageAnalysisGestureRecognizerDelegate> |
| @end |
| |
| #if USE(QUICK_LOOK) |
| |
| @interface WKContentView (ImageAnalysisPreview) <QLPreviewControllerDelegate, QLPreviewControllerDataSource, QLPreviewItemDataProvider> |
| @end |
| |
| #endif // USE(QUICK_LOOK) |
| |
| #endif // ENABLE(IMAGE_ANALYSIS) |
| |
| #if USE(APPLE_INTERNAL_SDK) |
| #import <WebKitAdditions/WKContentViewInteractionAdditions.mm> |
| #else |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| static bool canAttemptTextRecognitionForNonImageElements(const WebKit::InteractionInformationAtPosition& information, const WebKit::WebPreferences&) |
| { |
| return false; |
| } |
| #endif // ENABLE(IMAGE_ANALYSIS) |
| |
| #endif |
| |
| 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<ImageAnalysisGestureDeferralToken> { |
| public: |
| static RefPtr<ImageAnalysisGestureDeferralToken> 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<WKContentView> 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<WKTextSelectionRect *>*_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<WKTextSelectionRect *> *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) CGRect rect; |
| @property (nonatomic) NSUInteger index; |
| |
| + (WKFoundTextRange *)foundTextRangeWithRect:(CGRect)rect index:(NSUInteger)index; |
| |
| @end |
| |
| @interface WKFoundTextPosition : UITextPosition |
| |
| @property (nonatomic) NSUInteger index; |
| |
| + (WKFoundTextPosition *)textPositionWithIndex:(NSUInteger)index; |
| |
| @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 <UITextInput> *)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 <NSSecureCoding> *)userObject; |
| @end |
| |
| @implementation UITextInteractionAssistant (WebKit) |
| |
| - (BOOL)_wk_hasFloatingCursor |
| { |
| return self.inGesture && !self.interactions.inGesture; |
| } |
| |
| @end |
| |
| @implementation WKFormInputSession { |
| WeakObjCPtr<WKContentView> _contentView; |
| RetainPtr<WKFocusedElementInfo> _focusedElementInfo; |
| RetainPtr<UIView> _customInputView; |
| RetainPtr<UIView> _customInputAccessoryView; |
| RetainPtr<NSArray<UITextSuggestion *>> _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 <NSSecureCoding> *)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<WKFormControl>)_customInputView.get() controlEndEditing]; |
| } |
| |
| - (NSArray<UITextSuggestion *> *)suggestions |
| { |
| return _suggestions.get(); |
| } |
| |
| - (void)setSuggestions:(NSArray<UITextSuggestion *> *)suggestions |
| { |
| if (suggestions == _suggestions || [suggestions isEqualToArray:_suggestions.get()]) |
| return; |
| |
| _suggestions = adoptNS([suggestions copy]); |
| [_contentView updateTextSuggestionsForInputDelegate]; |
| } |
| |
| - (BOOL)requiresStrongPasswordAssistance |
| { |
| return _requiresStrongPasswordAssistance; |
| } |
| |
| - (void)invalidate |
| { |
| id <UITextInputSuggestionDelegate> suggestionDelegate = (id <UITextInputSuggestionDelegate>)[_contentView inputDelegate]; |
| [suggestionDelegate setSuggestions:nil]; |
| _contentView = nil; |
| } |
| |
| - (void)reloadFocusedElementContextView |
| { |
| [_contentView reloadContextViewForPresentedListViewController]; |
| } |
| |
| @end |
| |
| @implementation WKFocusedElementInfo { |
| WKInputType _type; |
| RetainPtr<NSString> _value; |
| BOOL _isUserInitiated; |
| RetainPtr<NSObject <NSSecureCoding>> _userObject; |
| RetainPtr<NSString> _placeholder; |
| RetainPtr<NSString> _label; |
| } |
| |
| - (instancetype)initWithFocusedElementInformation:(const WebKit::FocusedElementInformation&)information isUserInitiated:(BOOL)isUserInitiated userObject:(NSObject <NSSecureCoding> *)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 <NSSecureCoding> *)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<NSMutableArray> _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 <UIDragSession> session) |
| { |
| return [session.localContext isKindOfClass:[WKDragSessionContext class]] ? (WKDragSessionContext *)session.localContext : nil; |
| } |
| |
| static WKDragSessionContext *ensureLocalDragSessionContext(id <UIDragSession> 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<WebCore::SelectionGeometry>&)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)_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 ENABLE(HOVER_GESTURE_RECOGNIZER) |
| [self setUpHoverGestureRecognizer]; |
| #endif |
| |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) || ENABLE(HOVER_GESTURE_RECOGNIZER) |
| _hoverPlatter = adoptNS([[WKHoverPlatter alloc] initWithView:self.rootContentView delegate:self]); |
| #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()]; |
| |
| _highlightLongPressGestureRecognizer = adoptNS([[WKHighlightLongPressGestureRecognizer alloc] initWithTarget:self action:@selector(_highlightLongPressRecognized:)]); |
| [_highlightLongPressGestureRecognizer setDelay:highlightDelay]; |
| [_highlightLongPressGestureRecognizer setDelegate:self]; |
| [self addGestureRecognizer:_highlightLongPressGestureRecognizer.get()]; |
| |
| [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]; |
| [center addObserver:self selector:@selector(_willHideMenu:) name:UIMenuControllerWillHideMenuNotification object:nil]; |
| [center addObserver:self selector:@selector(_didHideMenu:) name:UIMenuControllerDidHideMenuNotification object:nil]; |
| [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<WebKit::SmartMagnificationController>(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<WebKit::TextCheckingController>(*_page); |
| #endif |
| |
| _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; |
| |
| _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; |
| } |
| |
| _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()]; |
| #endif |
| |
| #if ENABLE(HOVER_GESTURE_RECOGNIZER) |
| [_hoverGestureRecognizer setDelegate:nil]; |
| [self removeGestureRecognizer:_hoverGestureRecognizer.get()]; |
| #endif |
| |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) || ENABLE(HOVER_GESTURE_RECOGNIZER) |
| [_hoverPlatter invalidate]; |
| _hoverPlatter = nil; |
| #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 |
| |
| #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()]; |
| #endif |
| #if ENABLE(HOVER_GESTURE_RECOGNIZER) |
| [self removeGestureRecognizer:_hoverGestureRecognizer.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()]; |
| #endif |
| #if ENABLE(HOVER_GESTURE_RECOGNIZER) |
| [self addGestureRecognizer:_hoverGestureRecognizer.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 <UITextInputDelegate>)inputDelegate |
| { |
| _inputDelegate = inputDelegate; |
| } |
| |
| - (id <UITextInputDelegate>)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<WebKit::InputViewUpdateDeferrer>(self); |
| |
| BOOL didBecomeFirstResponder; |
| { |
| SetForScope<BOOL> becomingFirstResponder { _becomingFirstResponder, YES }; |
| didBecomeFirstResponder = [super becomeFirstResponder]; |
| } |
| |
| if (didBecomeFirstResponder) { |
| _page->installActivityStateChangeCompletionHandler([weakSelf = WeakObjCPtr<WKContentView>(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<BOOL> 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<id>(_keyWebEventHandler.get()), weakSelf = WeakObjCPtr<WKContentView>(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<NSNumber *, UITouch *> *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<unsigned>)activeTouchIdentifierForGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer |
| { |
| NSMapTable<NSNumber *, UITouch *> *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 <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([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<WebKit::RemoteLayerTreeDrawingAreaProxy>(*_page->drawingArea()).lastCommittedLayerTreeTransactionID(); |
| |
| #if ENABLE(TOUCH_EVENTS) |
| _page->resetPotentialTapSecurityOrigin(); |
| #endif |
| |
| WebKit::InteractionInformationRequest positionInformationRequest { WebCore::IntPoint(_lastInteractionLocation) }; |
| [self doAfterPositionInformationUpdate:[assistant = WeakObjCPtr<WKActionSheetAssistant>(_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<WKDeferringGestureRecognizer *> *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 ENABLE(HOVER_GESTURE_RECOGNIZER) |
| if (gestureRecognizer == _hoverGestureRecognizer) |
| return NO; |
| #endif |
| |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) |
| if (gestureRecognizer != _mouseGestureRecognizer && [_mouseGestureRecognizer 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 |
| { |
| #if ENABLE(HOVER_GESTURE_RECOGNIZER) |
| if (gestureRecognizer == _hoverGestureRecognizer) |
| return NO; |
| #endif |
| 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<id<WKUIDelegatePrivate>>(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<NSNumber *, UITouch *> *)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<WebKit::GestureRecognizerConsistencyEnforcer>(self); |
| |
| return *_gestureRecognizerConsistencyEnforcer; |
| } |
| |
| - (NSArray<WKDeferringGestureRecognizer *> *)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<WKDeferringGestureRecognizer *> *)_touchStartDeferringGestures |
| { |
| WKDeferringGestureRecognizer *recognizers[3]; |
| NSUInteger count = 0; |
| auto add = [&] (const RetainPtr<WKDeferringGestureRecognizer>& recognizer) { |
| if (recognizer) |
| recognizers[count++] = recognizer.get(); |
| }; |
| add(_touchStartDeferringGestureRecognizerForImmediatelyResettableGestures); |
| add(_touchStartDeferringGestureRecognizerForDelayedResettableGestures); |
| add(_touchStartDeferringGestureRecognizerForSyntheticTapGestures); |
| return [NSArray arrayWithObjects:recognizers count:count]; |
| } |
| |
| - (NSArray<WKDeferringGestureRecognizer *> *)_touchEndDeferringGestures |
| { |
| WKDeferringGestureRecognizer *recognizers[3]; |
| NSUInteger count = 0; |
| auto add = [&] (const RetainPtr<WKDeferringGestureRecognizer>& 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<WebCore::FloatQuad>&)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 |
| { |
| [_textInteractionAssistant willStartScrollingOverflow]; |
| } |
| |
| - (void)_scrollingNodeScrollingDidEnd |
| { |
| // 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]; |
| } |
| |
| - (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 |
| { |
| // 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)_zoomToRevealFocusedElement |
| { |
| if (_suppressSelectionAssistantReasons || _activeTextInteractionCount) |
| 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]]; |
| } |
| |
| - (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 <https://webkit.org/b/192878> 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); |
| } |
| |
| - (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 ENABLE(HOVER_GESTURE_RECOGNIZER) |
| if ([gestureRecognizer isKindOfClass:[WKHoverGestureRecognizer class]] || [otherGestureRecognizer isKindOfClass:[WKHoverGestureRecognizer 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 |
| |
| 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 <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_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]); |
| 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<Messages::WebPageProxy::DidReceivePositionInformation>(_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<NSValue *> *)_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 |
| { |
| 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 |
| { |
| 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 |
| { |
| 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<WebCore::SelectionGeometry>&)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]; |
| |
| _lastInteractionLocation = gestureRecognizer.startPoint; |
| |
| switch ([gestureRecognizer state]) { |
| case UIGestureRecognizerStateBegan: |
| _longPressCanClick = YES; |
| cancelPotentialTapIfNecessary(self); |
| _page->tapHighlightAtPosition([gestureRecognizer startPoint], [self nextTapIdentifier]); |
| _isTapHighlightIDValid = YES; |
| break; |
| case UIGestureRecognizerStateEnded: |
| if (_longPressCanClick && _positionInformation.isElement) { |
| [self _attemptSyntheticClickAtLocation:gestureRecognizer.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.location), WebKit::webEventModifierFlags(gestureRecognizer.modifierFlags), _layerTreeTransactionIdAtLastInteractionStart); |
| } |
| |
| - (void)_twoFingerSingleTapGestureRecognized:(UITapGestureRecognizer *)gestureRecognizer |
| { |
| _isTapHighlightIDValid = YES; |
| _isExpectingFastSingleTapCommit = YES; |
| _page->handleTwoFingerTapAtPoint(WebCore::roundedIntPoint(gestureRecognizer.centroid), 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 |
| { |
| 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(gestureRecognizer.location, 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.location; |
| |
| [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]; |
| } |
| |
| - (void)_doubleTapRecognized:(UITapGestureRecognizer *)gestureRecognizer |
| { |
| RELEASE_LOG(ViewGestures, "Identified a double tap (%p, pageProxyID=%llu)", self, _page->identifier().toUInt64()); |
| |
| [self _resetIsDoubleTapPending]; |
| _lastInteractionLocation = gestureRecognizer.location; |
| |
| _smartMagnificationController->handleSmartMagnificationGesture(gestureRecognizer.location); |
| } |
| |
| - (void)_resetIsDoubleTapPending |
| { |
| _isDoubleTapPending = NO; |
| } |
| |
| - (void)_nonBlockingDoubleTapRecognized:(UITapGestureRecognizer *)gestureRecognizer |
| { |
| _lastInteractionLocation = gestureRecognizer.location; |
| _isDoubleTapPending = YES; |
| } |
| |
| - (void)_twoFingerDoubleTapRecognized:(UITapGestureRecognizer *)gestureRecognizer |
| { |
| [self _resetIsDoubleTapPending]; |
| _lastInteractionLocation = gestureRecognizer.location; |
| |
| _smartMagnificationController->handleResetMagnificationGesture(gestureRecognizer.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 false; |
| 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<WKContentView> 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<WKContentView>(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<NSTextAlternatives *> *)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 |
| { |
| _page->replaceSelectedText(text, word); |
| } |
| |
| - (void)selectWordBackward |
| { |
| _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<CTFontDescriptorRef> fontDescriptor = adoptCF(CTFontDescriptorCreateWithNameAndSize(CFSTR("Helvetica"), 10)); |
| if (symbolicTraits) |
| fontDescriptor = adoptCF(CTFontDescriptorCreateCopyWithSymbolicTraits(fontDescriptor.get(), symbolicTraits, symbolicTraits)); |
| |
| RetainPtr<CTFontRef> 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()) 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. |
| // <rdar://problem/15254406> |
| 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:)) { |
| 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; |
| } |
| 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; |
| |
| if ([sender isKindOfClass:UIMenuController.class] && editorState.selectionIsRange) |
| return NO; |
| } |
| #endif // ENABLE(IMAGE_ANALYSIS) |
| |
| #if HAVE(UIFINDINTERACTION) |
| if (action == @selector(find:) || action == @selector(findNext:) || action == @selector(findPrevious:)) |
| return self.webView._findInteractionEnabled; |
| #endif |
| |
| return [super canPerformAction:action withSender:sender]; |
| } |
| |
| - (id)targetForAction:(SEL)action withSender:(id)sender |
| { |
| #if ENABLE(APP_HIGHLIGHTS) |
| if (action == @selector(createHighlightForCurrentQuickNoteWithRange:)) |
| return self.shouldAllowAppHighlightCreation && _page->appHighlightsVisibility() ? self : nil; |
| if (action == @selector(createHighlightForNewQuickNoteWithRange:)) |
| return self.shouldAllowAppHighlightCreation && !_page->appHighlightsVisibility() ? self : nil; |
| #endif |
| 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 |
| { |
| if (sender == UIMenuController.sharedMenuController && [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::GrantedForGesture]) |
| return; |
| |
| _page->executeEditCommand("paste"_s); |
| } |
| |
| - (void)_pasteAsQuotationForWebView:(id)sender |
| { |
| _page->executeEditCommand("PasteAsQuotation"_s); |
| } |
| |
| - (void)selectForWebView:(id)sender |
| { |
| if (!_page->preferences().textInteractionEnabled()) |
| return; |
| |
| [_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; |
| |
| [_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 |
| { |
| #if !PLATFORM(MACCATALYST) |
| MCProfileConnection *connection = [PAL::getMCProfileConnectionClass() sharedConnection]; |
| if ([connection effectiveBoolValueForSetting:PAL::get_ManagedConfiguration_MCFeatureDefinitionLookupAllowed()] == MCRestrictedBoolExplicitNo) |
| return; |
| #endif |
| |
| RetainPtr<WKContentView> view = self; |
| _page->getSelectionOrContentsAsString([view](const String& string) { |
| if (!string) |
| return; |
| |
| [view _showDictionary:string]; |
| }); |
| } |
| |
| - (void)accessibilityRetrieveSpeakSelectionContent |
| { |
| RetainPtr<WKContentView> view = self; |
| RetainPtr<WKWebView> 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<WebCore::SelectionGeometry>& 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<WebCore::SelectionGeometry>& geometries))completionHandler |
| { |
| RetainPtr<WKContentView> view = self; |
| _page->requestRectsAtSelectionOffsetWithText(offset, text, [view, offset, capturedCompletionHandler = makeBlockPtr(completionHandler)](const Vector<WebCore::SelectionGeometry>& 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)) { |
| [UIMenuController.sharedMenuController hideMenuFromView:self]; |
| 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<WebKit::SelectionFlags> 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<UIWKSelectionFlags>(uiFlags); |
| } |
| |
| static inline OptionSet<WebKit::SelectionFlags> toSelectionFlags(UIWKSelectionFlags uiFlags) |
| { |
| OptionSet<WebKit::SelectionFlags> 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<WebKit::SelectionFlags> 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<WebKit::SelectionFlags> 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 |
| { |
| _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<WebKit::SelectionFlags> 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 |
| { |
| _usingGestureForSelection = YES; |
| _page->updateSelectionWithTouches(WebCore::IntPoint(point), toSelectionTouch(touch), baseIsStart, [self, strongSelf = retainPtr(self), flags](const WebCore::IntPoint& point, WebKit::SelectionTouch touch, OptionSet<WebKit::SelectionFlags> 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 |
| { |
| _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<WebKit::SelectionFlags> 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<WKContentView> 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); |
| }); |
| } |
| |
| - (void)requestRectsToEvadeForSelectionCommandsWithCompletionHandler:(void(^)(NSArray<NSValue *> *rects))completionHandler |
| { |
| if (!completionHandler) { |
| [NSException raise:NSInvalidArgumentException format:@"Expected a nonnull completion handler in %s.", __PRETTY_FUNCTION__]; |
| return; |
| } |
| |
| if ([self _shouldSuppressSelectionCommands] || self.webView._editable) { |
| completionHandler(@[ ]); |
| return; |
| } |
| |
| if (_focusedElementInformation.elementType != WebKit::InputType::ContentEditable && _focusedElementInformation.elementType != WebKit::InputType::TextArea) { |
| completionHandler(@[ ]); |
| return; |
| } |
| |
| // Give the page some time to present custom editing UI before attempting to detect and evade it. |
| auto delayBeforeShowingCalloutBar = 0.25_s; |
| WorkQueue::main().dispatchAfter(delayBeforeShowingCalloutBar, [completion = makeBlockPtr(completionHandler), weakSelf = WeakObjCPtr<WKContentView>(self)] () mutable { |
| if (!weakSelf) { |
| completion(@[ ]); |
| return; |
| } |
| |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf->_page) { |
| completion(@[ ]); |
| return; |
| } |
| |
| strongSelf->_page->requestEvasionRectsAboveSelection([completion = WTFMove(completion)] (auto& rects) { |
| completion(createNSArray(rects).get()); |
| }); |
| }); |
| } |
| |
| - (void)selectPositionAtPoint:(CGPoint)point completionHandler:(void (^)(void))completionHandler |
| { |
| [self _selectPositionAtPoint:point stayingWithinFocusedElement:self._hasFocusedElement completionHandler:completionHandler]; |
| } |
| |
| - (void)_selectPositionAtPoint:(CGPoint)point stayingWithinFocusedElement:(BOOL)stayingWithinFocusedElement completionHandler:(void (^)(void))completionHandler |
| { |
| _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 |
| { |
| _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 |
| { |
| _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 |
| { |
| _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 |
| { |
| _page->beginSelectionInDirection(toWKSelectionDirection(direction), [selectionHandler = makeBlockPtr(completionHandler)] (bool endIsMoving) { |
| selectionHandler(endIsMoving); |
| }); |
| } |
| |
| - (void)updateSelectionWithExtentPoint:(CGPoint)point completionHandler:(void (^)(BOOL endIsMoving))completionHandler |
| { |
| 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 |
| { |
| ++_suppressNonEditableSingleTapTextInteractionCount; |
| _page->updateSelectionWithExtentPointAndBoundary(WebCore::IntPoint(point), toWKTextGranularity(granularity), self._hasFocusedElement, [completionHandler = makeBlockPtr(completionHandler), protectedSelf = retainPtr(self)] (bool endIsMoving) { |
| completionHandler(endIsMoving); |
| --protectedSelf->_suppressNonEditableSingleTapTextInteractionCount; |
| }); |
| } |
| |
| - (UTF32Char)_characterBeforeCaretSelection |
| { |
| return _lastInsertedCharacterToOverrideCharacterBeforeSelection.value_or(_page->editorState().postLayoutData().characterBeforeSelection); |
| } |
| |
| - (UTF32Char)_characterInRelationToCaretSelection:(int)amount |
| { |
| switch (amount) { |
| case 0: |
| return _page->editorState().postLayoutData().characterAfterSelection; |
| case -1: |
| return self._characterBeforeCaretSelection; |
| case -2: |
| return _page->editorState().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)replaceDictatedText:(NSString*)oldText withText:(NSString *)newText |
| { |
| _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 <rdar://problem/16207002> 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 false; |
| })(); |
| |
| if (respondWithLastKnownAutocorrectionContext) { |
| completionHandler([WKAutocorrectionContext autocorrectionContextWithWebContext:_lastAutocorrectionContext]); |
| return; |
| } |
| |
| // FIXME: Remove the synchronous call when <rdar://problem/16207002> is fixed. |
| const bool useSyncRequest = true; |
| |
| if (useSyncRequest && _pendingAutocorrectionContextHandler) { |
| completionHandler(WKAutocorrectionContext.emptyAutocorrectionContext); |
| return; |
| } |
| |
| _pendingAutocorrectionContextHandler = completionHandler; |
| _page->requestAutocorrectionContext(); |
| |
| if (useSyncRequest) { |
| _page->process().connection()->waitForAndDispatchImmediately<Messages::WebPageProxy::HandleAutocorrectionContext>(_page->webPageID(), 1_s, IPC::WaitForOption::DispatchIncomingSyncMessagesWhileWaiting); |
| [self _cancelPendingAutocorrectionContextHandler]; |
| } |
| } |
| |
| - (void)_handleAutocorrectionContext:(const WebKit::WebAutocorrectionContext&)context |
| { |
| _lastAutocorrectionContext = context; |
| [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<BOOL> unsuppressSoftwareKeyboardScope { _isUnsuppressingSoftwareKeyboardUsingLastAutocorrectionContext, YES }; |
| self._suppressSoftwareKeyboard = NO; |
| } |
| |
| - (void)runModalJavaScriptDialog:(CompletionHandler<void()>&&)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 HAVE(UIKIT_WITH_MOUSE_SUPPORT) || ENABLE(HOVER_GESTURE_RECOGNIZER) |
| [_hoverPlatter dismissPlatterWithAnimation:NO]; |
| #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<uint8_t> { 0, 225, 0, 127 } : WebCore::SRGBA<uint8_t> { 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 <https://bugs.webkit.org/show_bug.cgi?id=134409>. |
| [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 |
| { |
| _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<WKContentView>(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 |
| { |
| // 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<WKTextSelectionRect *> *textSelectionRects(const Vector<WebCore::SelectionGeometry>& 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<WebCore::CompositionHighlight> compositionHighlights(NSAttributedString *string) |
| { |
| if (!string.length) |
| return { }; |
| |
| Vector<WebCore::CompositionHighlight> highlights; |
| [string enumerateAttributesInRange:NSMakeRange(0, string.length) options:0 usingBlock:[&highlights](NSDictionary<NSAttributedStringKey, id> *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<unsigned>(range.location), static_cast<unsigned>(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<WebCore::CompositionHighlight> 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<WebCore::CompositionHighlight> { } selectedRange:selectedRange]; |
| } |
| |
| - (void)_setMarkedText:(NSString *)markedText highlights:(const Vector<WebCore::CompositionHighlight>&)highlights selectedRange:(NSRange)selectedRange |
| { |
| _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 *)toPosition |
| { |
| #if HAVE(UIFINDINTERACTION) |
| if ([from isKindOfClass:[WKFoundTextPosition class]] && [toPosition isKindOfClass:[WKFoundTextPosition class]]) |
| return ((WKFoundTextPosition *)from).index - ((WKFoundTextPosition *)toPosition).index; |
| #endif |
| |
| return 0; |
| } |
| |
| - (id <UITextInputTokenizer>)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 |
| { |
| 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(); |
| } |
| } |
| |
| - (void)insertText:(NSString *)aStringValue alternatives:(NSArray<NSString *> *)alternatives style:(UITextAlternativeStyle)style |
| { |
| if (!alternatives.count) |
| [self insertText:aStringValue]; |
| else { |
| 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)); |
| } |
| } |
| |
| - (BOOL)hasText |
| { |
| auto& editorState = _page->editorState(); |
| return !editorState.isMissingPostLayoutData && editorState.postLayoutData().hasPlainText; |
| } |
| |
| // 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 <UITextInputTraits>)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"); |
| 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; |
| } |
| |
| 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 <UITextInputTraits_Private>)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<UITextInput> *))completionHandler |
| { |
| ASSERT(context); |
| // This function can be called more than once during a text interaction (e.g. <rdar://problem/59430806>). |
| if (![self becomeFirstResponder]) { |
| completionHandler(nil); |
| return; |
| } |
| if ([self _isTextInputContextFocused:context]) { |
| completionHandler(_focusedElementInformation.isReadOnly ? nil : self); |
| return; |
| } |
| _usingGestureForSelection = YES; |
| auto checkFocusedElement = [weakSelf = WeakObjCPtr<WKContentView> { 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<WKContentView>(self), completionHandler = makeBlockPtr(completionHandler)] (const Vector<WebCore::ElementContext>& 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); |
| _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<NSString *> *)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]; |
| } |
| |
| - (void)executeEditCommandWithCallback:(NSString *)commandName |
| { |
| // 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<WKContentView> 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 |
| { |
| 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<WebKit::OptionItem>&)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 false; |
| |
| if (mayContainSelectableText(information.elementType)) |
| return true; |
| |
| return [self _elementTypeRequiresAccessoryView:information.elementType]; |
| } |
| |
| static RetainPtr<NSObject <WKFormPeripheral>> 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<WebCore::ActivityState::Flag>)activityStateChanges userObject:(NSObject <NSSecureCoding> *)userObject |
| { |
| SetForScope<BOOL> isChangingFocusForScope { _isChangingFocus, self._hasFocusedElement }; |
| SetForScope<BOOL> isFocusingElementWithKeyboardForScope { _isFocusingElementWithKeyboard, [self _shouldShowKeyboardForElement:information] }; |
| |
| auto runModalJavaScriptDialogCallbackIfNeeded = makeScopeExit([&] { |
| if (auto callback = std::exchange(_pendingRunModalJavaScriptDialogCallback, { })) |
| callback(); |
| }); |
| |
| auto inputViewUpdateDeferrer = std::exchange(_inputViewUpdateDeferrer, nullptr); |
| |
| _didAccessoryTabInitiateFocus = _isChangingFocusUsingAccessoryTab; |
| |
| id <_WKInputDelegate> inputDelegate = [_webView _inputDelegate]; |
| RetainPtr<WKFocusedElementInfo> 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 (_isFocusingElementWithKeyboard && [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<WebKit::InputViewUpdateDeferrer>(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 isSelectable = mayContainSelectableText(information.elementType); |
| BOOL editableChanged = [self setIsEditable:isSelectable]; |
| _focusedElementInformation = information; |
| _traits = nil; |
| |
| if (![self isFirstResponder]) |
| [self becomeFirstResponder]; |
| |
| if (!_suppressSelectionAssistantReasons && isSelectable && activityStateChanges.contains(WebCore::ActivityState::IsFocused)) { |
| _treatAsContentEditableUntilNextEditorStateUpdate = YES; |
| [_textInteractionAssistant activateSelection]; |
| _page->restoreSelectionInFocusedEditableElement(); |
| _page->scheduleFullEditorStateUpdate(); |
| } |
| |
| _inputPeripheral = createInputPeripheralWithView(_focusedElementInformation.elementType, self); |
| |
| #if HAVE(PEPPER_UI_CORE) |
| [self addFocusedFormControlOverlay]; |
| if (!_isChangingFocus) |
| [self presentViewControllerForCurrentFocusedElement]; |
| #else |
| [self reloadInputViews]; |
| #endif |
| |
| if (isSelectable) { |
| [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. <select>) can be zoomed |
| // immediately because they have no selection to reveal. |
| BOOL needsEditorStateUpdate = mayContainSelectableText(_focusedElementInformation.elementType); |
| if (needsEditorStateUpdate) |
| _page->setWaitingForPostLayoutEditorStateUpdateAfterFocusingElement(true); |
| else |
| [self _zoomToRevealFocusedElement]; |
| |
| [self _updateAccessory]; |
| |
| #if HAVE(PEPPER_UI_CORE) |
| if (_isChangingFocus) |
| [_focusedFormControlView reloadData:YES]; |
| #endif |
| |
| // _inputPeripheral has been initialized in inputView called by reloadInputViews. |
| [_inputPeripheral beginEditing]; |
| |
| if (delegateImplementsDidStartInputSession) |
| [inputDelegate _webView:self.webView didStartInputSession:_formInputSession.get()]; |
| |
| [_webView didStartFormControlInteraction]; |
| } |
| |
| - (void)_elementDidBlur |
| { |
| SetForScope<BOOL> isBlurringFocusedElementForScope { _isBlurringFocusedElement, YES }; |
| |
| [self _endEditing]; |
| |
| [_formInputSession invalidate]; |
| _formInputSession = nil; |
| |
| #if ENABLE(DATALIST_ELEMENT) |
| _dataListTextSuggestionsInputView = nil; |
| _dataListTextSuggestions = nil; |
| #endif |
| |
| BOOL editableChanged = [self setIsEditable:NO]; |
| // FIXME: We should completely invalidate _focusedElementInformation here, instead of a subset of individual members. |
| _focusedElementInformation.elementType = WebKit::InputType::None; |
| _focusedElementInformation.shouldSynthesizeKeyEventsForEditing = false; |
| _focusedElementInformation.shouldAvoidResizingWhenInputViewBoundsChange = false; |
| _focusedElementInformation.shouldAvoidScrollingWhenFocusedContentIsVisible = false; |
| _focusedElementInformation.shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation = false; |
| _focusedElementInformation.isFocusingWithValidationMessage = false; |
| _inputPeripheral = nil; |
| _focusRequiresStrongPasswordAssistance = NO; |
| _additionalContextForStrongPasswordAssistance = nil; |
| |
| // When defocusing an editable element reset a seen keydown before calling -_hideKeyboard so that we |
| // re-evaluate whether we still need a keyboard when UIKit calls us back in -_requiresKeyboardWhenFirstResponder. |
| if (editableChanged) |
| _seenHardwareKeyDownInNonEditableElement = NO; |
| |
| [self _hideKeyboard]; |
| |
| #if HAVE(PEPPER_UI_CORE) |
| [self dismissAllInputViewControllers:YES]; |
| if (!_isChangingFocus) |
| [self removeFocusedFormControlOverlay]; |
| #endif |
| |
| if (editableChanged) { |
| // The custom fixed position rect behavior is affected by -isFocusingElement, so if that changes we need to recompute rects. |
| [_webView _scheduleVisibleContentRectUpdate]; |
| |
| [_webView didEndFormControlInteraction]; |
| _page->setIsShowingInputViewForFocusedElement(false); |
| } |
| |
| _page->setWaitingForPostLayoutEditorStateUpdateAfterFocusingElement(false); |
| |
| if (!_isChangingFocus) |
| _didAccessoryTabInitiateFocus = NO; |
| |
| _lastInsertedCharacterToOverrideCharacterBeforeSelection = std::nullopt; |
| } |
| |
| - (void)_updateInputContextAfterBlurringAndRefocusingElement |
| { |
| if (!self._hasFocusedElement || !_suppressSelectionAssistantReasons) |
| return; |
| |
| [UIKeyboardImpl.activeInstance updateForChangedSelection]; |
| } |
| |
| - (BOOL)shouldIgnoreKeyboardWillHideNotification |
| { |
| // Ignore keyboard will hide notifications sent during rotation. They're just there for |
| // backwards compatibility reasons and processing the will hide notification would |
| // temporarily screw up the unobscured view area. |
| if (UIPeripheralHost.sharedInstance.rotationState) |
| return YES; |
| |
| if (_isChangingFocus && _isFocusingElementWithKeyboard) |
| return YES; |
| |
| return NO; |
| } |
| |
| - (void)_hardwareKeyboardAvailabilityChanged |
| { |
| _seenHardwareKeyDownInNonEditableElement = NO; |
| [self reloadInputViews]; |
| } |
| |
| - (void)_didUpdateInputMode:(WebCore::InputMode)mode |
| { |
| if (!self.inputDelegate || !self._hasFocusedElement) |
| return; |
| |
| #if !PLATFORM(WATCHOS) |
| _focusedElementInformation.inputMode = mode; |
| [self reloadInputViews]; |
| #endif |
| } |
| |
| static BOOL allPasteboardItemOriginsMatchOrigin(UIPasteboard *pasteboard, const String& originIdentifier) |
| { |
| if (originIdentifier.isEmpty()) |
| return NO; |
| |
| auto *indices = [NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, [pasteboard numberOfItems])]; |
| auto *allCustomData = [pasteboard dataForPasteboardType:@(WebCore::PasteboardCustomData::cocoaType()) inItemSet:indices]; |
| if (!allCustomData.count) |
| return NO; |
| |
| BOOL foundAtLeastOneMatchingIdentifier = NO; |
| for (NSData *data in allCustomData) { |
| if (!data.length) |
| continue; |
| |
| auto buffer = WebCore::SharedBuffer::create(data); |
| if (WebCore::PasteboardCustomData::fromSharedBuffer(buffer.get()).origin() != originIdentifier) |
| return NO; |
| |
| foundAtLeastOneMatchingIdentifier = YES; |
| } |
| |
| return foundAtLeastOneMatchingIdentifier; |
| } |
| |
| - (void)_requestDOMPasteAccessForCategory:(WebCore::DOMPasteAccessCategory)pasteAccessCategory elementRect:(const WebCore::IntRect&)elementRect originIdentifier:(const String&)originIdentifier completionHandler:(CompletionHandler<void(WebCore::DOMPasteAccessResponse)>&&)completionHandler |
| { |
| if (auto existingCompletionHandler = std::exchange(_domPasteRequestHandler, WTFMove(completionHandler))) { |
| ASSERT_NOT_REACHED(); |
| existingCompletionHandler(WebCore::DOMPasteAccessResponse::DeniedForGesture); |
| } |
| |
| _domPasteRequestCategory = pasteAccessCategory; |
| |
| if (allPasteboardItemOriginsMatchOrigin(pasteboardForAccessCategory(pasteAccessCategory), originIdentifier)) { |
| [self _handleDOMPasteRequestWithResult:WebCore::DOMPasteAccessResponse::GrantedForCommand]; |
| return; |
| } |
| |
| WebCore::IntRect menuControllerRect = elementRect; |
| |
| const CGFloat maximumElementWidth = 300; |
| const CGFloat maximumElementHeight = 120; |
| if (elementRect.isEmpty() || elementRect.width() > maximumElementWidth || elementRect.height() > maximumElementHeight) { |
| const CGFloat interactionLocationMargin = 10; |
| menuControllerRect = { WebCore::IntPoint(_lastInteractionLocation), { } }; |
| menuControllerRect.inflate(interactionLocationMargin); |
| } |
| |
| [UIMenuController.sharedMenuController showMenuFromView:self rect:menuControllerRect]; |
| } |
| |
| - (void)doAfterEditorStateUpdateAfterFocusingElement:(dispatch_block_t)block |
| { |
| if (!_page->waitingForPostLayoutEditorStateUpdateAfterFocusingElement()) { |
| block(); |
| return; |
| } |
| |
| _actionsToPerformAfterEditorStateUpdate.append(makeBlockPtr(block)); |
| } |
| |
| - (void)_didUpdateEditorState |
| { |
| [self _updateInitialWritingDirectionIfNecessary]; |
| |
| // FIXME: If the initial writing direction just changed, we should wait until we get the next post-layout editor state |
| // before zooming to reveal the selection rect. |
| if (mayContainSelectableText(_focusedElementInformation.elementType)) |
| [self _zoomToRevealFocusedElement]; |
| |
| _treatAsContentEditableUntilNextEditorStateUpdate = NO; |
| |
| for (auto block : std::exchange(_actionsToPerformAfterEditorStateUpdate, { })) |
| block(); |
| } |
| |
| - (void)_updateInitialWritingDirectionIfNecessary |
| { |
| if (!_page->isEditable()) |
| return; |
| |
| auto& editorState = _page->editorState(); |
| if (editorState.selectionIsNone || editorState.selectionIsRange) |
| return; |
| |
| UIKeyboardImpl *keyboard = UIKeyboardImpl.activeInstance; |
| if (keyboard.delegate != self) |
| return; |
| |
| // Synchronize the keyboard's writing direction with the newly received EditorState. |
| [keyboard setInitialDirection]; |
| } |
| |
| - (void)updateCurrentFocusedElementInformation:(Function<void(bool didUpdate)>&&)callback |
| { |
| WeakObjCPtr<WKContentView> weakSelf { self }; |
| auto identifierBeforeUpdate = _focusedElementInformation.identifier; |
| _page->requestFocusedElementInformation([callback = WTFMove(callback), identifierBeforeUpdate, weakSelf] (auto& info) { |
| if (!weakSelf || !info || info->identifier != identifierBeforeUpdate) { |
| // If the focused element may have changed in the meantime, don't overwrite focused element information. |
| callback(false); |
| return; |
| } |
| |
| weakSelf.get()->_focusedElementInformation = info.value(); |
| callback(true); |
| }); |
| } |
| |
| - (void)reloadContextViewForPresentedListViewController |
| { |
| #if HAVE(PEPPER_UI_CORE) |
| if ([_presentedFullScreenInputViewController isKindOfClass:[WKTextInputListViewController class]]) |
| [(WKTextInputListViewController *)_presentedFullScreenInputViewController.get() reloadContextView]; |
| #endif |
| } |
| |
| #if HAVE(PEPPER_UI_CORE) |
| |
| - (void)addFocusedFormControlOverlay |
| { |
| if (_focusedFormControlView) |
| return; |
| |
| _activeFocusedStateRetainBlock = makeBlockPtr(self.webView._retainActiveFocusedState); |
| |
| _focusedFormControlView = adoptNS([[WKFocusedFormControlView alloc] initWithFrame:self.webView.bounds delegate:self]); |
| [_focusedFormControlView hide:NO]; |
| [_webView addSubview:_focusedFormControlView.get()]; |
| [self setInputDelegate:_focusedFormControlView.get()]; |
| } |
| |
| - (void)removeFocusedFormControlOverlay |
| { |
| if (!_focusedFormControlView) |
| return; |
| |
| if (auto releaseActiveFocusState = WTFMove(_activeFocusedStateRetainBlock)) |
| releaseActiveFocusState(); |
| |
| [_focusedFormControlView removeFromSuperview]; |
| _focusedFormControlView = nil; |
| [self setInputDelegate:nil]; |
| } |
| |
| - (RetainPtr<PUICTextInputContext>)createQuickboardTextInputContext |
| { |
| auto context = adoptNS([[PUICTextInputContext alloc] init]); |
| [self _updateTextInputTraits:context.get()]; |
| [context setInitialText:_focusedElementInformation.value]; |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| [context setAcceptsEmoji:YES]; |
| [context setShouldPresentModernTextInputUI:YES]; |
| [context setPlaceholder:self.inputLabelText]; |
| #endif |
| return context; |
| } |
| |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| |
| - (RetainPtr<PUICQuickboardController>)_createQuickboardController:(UIViewController *)presentingViewController |
| { |
| auto quickboardController = adoptNS([[PUICQuickboardController alloc] init]); |
| auto context = self.createQuickboardTextInputContext; |
| [quickboardController setQuickboardPresentingViewController:presentingViewController]; |
| [quickboardController setExcludedFromScreenCapture:[context isSecureTextEntry]]; |
| [quickboardController setTextInputContext:context.get()]; |
| [quickboardController setDelegate:self]; |
| |
| return quickboardController; |
| } |
| |
| static bool canUseQuickboardControllerFor(UITextContentType type) |
| { |
| return [type isEqualToString:UITextContentTypePassword]; |
| } |
| |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| |
| - (void)presentViewControllerForCurrentFocusedElement |
| { |
| [self dismissAllInputViewControllers:NO]; |
| |
| _shouldRestoreFirstResponderStatusAfterLosingFocus = self.isFirstResponder; |
| UIViewController *presentingViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:self]; |
| |
| ASSERT(!_presentedFullScreenInputViewController); |
| |
| BOOL prefersModalPresentation = NO; |
| |
| switch (_focusedElementInformation.elementType) { |
| case WebKit::InputType::Select: |
| _presentedFullScreenInputViewController = adoptNS([[WKSelectMenuListViewController alloc] initWithDelegate:self]); |
| break; |
| case WebKit::InputType::Time: |
| // Time inputs are special, in that the only UI affordances for dismissal are push buttons rather than status bar chevrons. |
| // As such, modal presentation and dismissal is preferred even if a navigation stack exists. |
| prefersModalPresentation = YES; |
| _presentedFullScreenInputViewController = adoptNS([[WKTimePickerViewController alloc] initWithDelegate:self]); |
| break; |
| case WebKit::InputType::Date: |
| _presentedFullScreenInputViewController = adoptNS([[WKDatePickerViewController alloc] initWithDelegate:self]); |
| break; |
| case WebKit::InputType::None: |
| break; |
| default: { |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| if (canUseQuickboardControllerFor(self.textContentTypeForQuickboard)) { |
| _presentedQuickboardController = [self _createQuickboardController:presentingViewController]; |
| break; |
| } |
| #endif |
| _presentedFullScreenInputViewController = adoptNS([[WKTextInputListViewController alloc] initWithDelegate:self]); |
| break; |
| } |
| } |
| |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| ASSERT_IMPLIES(_presentedQuickboardController, !_presentedFullScreenInputViewController); |
| ASSERT_IMPLIES(_presentedFullScreenInputViewController, !_presentedQuickboardController); |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| ASSERT(self._isPresentingFullScreenInputView); |
| ASSERT(presentingViewController); |
| |
| if (!prefersModalPresentation && [presentingViewController isKindOfClass:[UINavigationController class]]) |
| _inputNavigationViewControllerForFullScreenInputs = (UINavigationController *)presentingViewController; |
| else |
| _inputNavigationViewControllerForFullScreenInputs = nil; |
| |
| RetainPtr<UIViewController> controller; |
| if (_presentedFullScreenInputViewController) { |
| // Present the input view controller on an existing navigation stack, if possible. If there is no navigation stack we can use, fall back to presenting modally. |
| // This is because the HI specification (for certain scenarios) calls for navigation-style view controller presentation, but WKWebView can't make any guarantees |
| // about clients' view controller hierarchies, so we can only try our best to avoid presenting modally. Clients can implicitly opt in to specced behavior by using |
| // UINavigationController to present the web view. |
| if (_inputNavigationViewControllerForFullScreenInputs) |
| [_inputNavigationViewControllerForFullScreenInputs pushViewController:_presentedFullScreenInputViewController.get() animated:YES]; |
| else |
| [presentingViewController presentViewController:_presentedFullScreenInputViewController.get() animated:YES completion:nil]; |
| controller = _presentedFullScreenInputViewController.get(); |
| } |
| |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| if (_presentedQuickboardController) { |
| [_presentedQuickboardController present]; |
| controller = [presentingViewController presentedViewController]; |
| } |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| |
| // Presenting a fullscreen input view controller fully obscures the web view. Without taking this token, the web content process will get backgrounded. |
| _page->process().startBackgroundActivityForFullscreenInput(); |
| |
| // FIXME: PUICQuickboardController does not present its view controller immediately, since it asynchronously |
| // establishes a connection to QuickboardViewService before presenting the remote view controller. |
| // Fixing this requires a version of `-[PUICQuickboardController present]` that takes a completion handler. |
| [presentingViewController.transitionCoordinator animateAlongsideTransition:nil completion:[weakWebView = WeakObjCPtr<WKWebView>(_webView), controller] (id <UIViewControllerTransitionCoordinatorContext>) { |
| auto strongWebView = weakWebView.get(); |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([strongWebView UIDelegate]); |
| if ([uiDelegate respondsToSelector:@selector(_webView:didPresentFocusedElementViewController:)]) |
| [uiDelegate _webView:strongWebView.get() didPresentFocusedElementViewController:controller.get()]; |
| }]; |
| } |
| |
| - (BOOL)_isPresentingFullScreenInputView |
| { |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| if (_presentedQuickboardController) |
| return YES; |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| return _presentedFullScreenInputViewController; |
| } |
| |
| - (void)dismissAllInputViewControllers:(BOOL)animated |
| { |
| auto navigationController = std::exchange(_inputNavigationViewControllerForFullScreenInputs, nil); |
| |
| if (!self._isPresentingFullScreenInputView) { |
| ASSERT(!navigationController); |
| return; |
| } |
| |
| if (auto controller = std::exchange(_presentedFullScreenInputViewController, nil)) { |
| auto dispatchDidDismiss = makeBlockPtr([weakWebView = WeakObjCPtr<WKWebView>(_webView), controller] { |
| auto strongWebView = weakWebView.get(); |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([strongWebView UIDelegate]); |
| if ([uiDelegate respondsToSelector:@selector(_webView:didDismissFocusedElementViewController:)]) |
| [uiDelegate _webView:strongWebView.get() didDismissFocusedElementViewController:controller.get()]; |
| }); |
| |
| if ([navigationController viewControllers].lastObject == controller.get()) { |
| [navigationController popViewControllerAnimated:animated]; |
| [[controller transitionCoordinator] animateAlongsideTransition:nil completion:[dispatchDidDismiss = WTFMove(dispatchDidDismiss)] (id <UIViewControllerTransitionCoordinatorContext>) { |
| dispatchDidDismiss(); |
| }]; |
| } else if (auto presentedViewController = retainPtr([controller presentedViewController])) { |
| [presentedViewController dismissViewControllerAnimated:animated completion:[controller, animated, dispatchDidDismiss = WTFMove(dispatchDidDismiss)] { |
| [controller dismissViewControllerAnimated:animated completion:dispatchDidDismiss.get()]; |
| }]; |
| } else |
| [controller dismissViewControllerAnimated:animated completion:dispatchDidDismiss.get()]; |
| } |
| |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| if (auto controller = std::exchange(_presentedQuickboardController, nil)) { |
| auto presentedViewController = retainPtr([controller quickboardPresentingViewController].presentedViewController); |
| [controller dismissWithCompletion:[weakWebView = WeakObjCPtr<WKWebView>(_webView), presentedViewController] { |
| auto strongWebView = weakWebView.get(); |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([strongWebView UIDelegate]); |
| if ([uiDelegate respondsToSelector:@selector(_webView:didDismissFocusedElementViewController:)]) |
| [uiDelegate _webView:strongWebView.get() didDismissFocusedElementViewController:presentedViewController.get()]; |
| }]; |
| } |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| |
| if (_shouldRestoreFirstResponderStatusAfterLosingFocus) { |
| _shouldRestoreFirstResponderStatusAfterLosingFocus = NO; |
| if (!self.isFirstResponder) |
| [self becomeFirstResponder]; |
| } |
| |
| _page->process().endBackgroundActivityForFullscreenInput(); |
| } |
| |
| - (void)focusedFormControlViewDidSubmit:(WKFocusedFormControlView *)view |
| { |
| [self insertText:@"\n"]; |
| _page->blurFocusedElement(); |
| } |
| |
| - (void)focusedFormControlViewDidCancel:(WKFocusedFormControlView *)view |
| { |
| _page->blurFocusedElement(); |
| } |
| |
| - (void)focusedFormControlViewDidBeginEditing:(WKFocusedFormControlView *)view |
| { |
| [self updateCurrentFocusedElementInformation:[weakSelf = WeakObjCPtr<WKContentView>(self)] (bool didUpdate) { |
| if (!didUpdate) |
| return; |
| |
| auto strongSelf = weakSelf.get(); |
| [strongSelf presentViewControllerForCurrentFocusedElement]; |
| [strongSelf->_focusedFormControlView hide:YES]; |
| }]; |
| } |
| |
| - (CGRect)rectForFocusedFormControlView:(WKFocusedFormControlView *)view |
| { |
| return [self convertRect:_focusedElementInformation.interactionRect toView:view]; |
| } |
| |
| - (CGRect)nextRectForFocusedFormControlView:(WKFocusedFormControlView *)view |
| { |
| if (!_focusedElementInformation.hasNextNode) |
| return CGRectNull; |
| |
| return [self convertRect:_focusedElementInformation.nextNodeRect toView:view]; |
| } |
| |
| - (CGRect)previousRectForFocusedFormControlView:(WKFocusedFormControlView *)view |
| { |
| if (!_focusedElementInformation.hasPreviousNode) |
| return CGRectNull; |
| |
| return [self convertRect:_focusedElementInformation.previousNodeRect toView:view]; |
| } |
| |
| - (UIScrollView *)scrollViewForFocusedFormControlView:(WKFocusedFormControlView *)view |
| { |
| return self._scroller; |
| } |
| |
| - (NSString *)actionNameForFocusedFormControlView:(WKFocusedFormControlView *)view |
| { |
| if (_focusedElementInformation.formAction.isEmpty()) |
| return nil; |
| |
| switch (_focusedElementInformation.elementType) { |
| case WebKit::InputType::Select: |
| case WebKit::InputType::Time: |
| case WebKit::InputType::Date: |
| #if ENABLE(INPUT_TYPE_COLOR) |
| case WebKit::InputType::Color: |
| #endif |
| return nil; |
| case WebKit::InputType::Search: |
| return WebCore::formControlSearchButtonTitle(); |
| default: |
| return WebCore::formControlGoButtonTitle(); |
| } |
| } |
| |
| - (void)focusedFormControlViewDidRequestNextNode:(WKFocusedFormControlView *)view |
| { |
| if (_focusedElementInformation.hasNextNode) |
| _page->focusNextFocusedElement(true); |
| } |
| |
| - (void)focusedFormControlViewDidRequestPreviousNode:(WKFocusedFormControlView *)view |
| { |
| if (_focusedElementInformation.hasPreviousNode) |
| _page->focusNextFocusedElement(false); |
| } |
| |
| - (BOOL)hasNextNodeForFocusedFormControlView:(WKFocusedFormControlView *)view |
| { |
| return _focusedElementInformation.hasNextNode; |
| } |
| |
| - (BOOL)hasPreviousNodeForFocusedFormControlView:(WKFocusedFormControlView *)view |
| { |
| return _focusedElementInformation.hasPreviousNode; |
| } |
| |
| - (void)focusedFormControllerDidUpdateSuggestions:(WKFocusedFormControlView *)view |
| { |
| if (_isBlurringFocusedElement || ![_presentedFullScreenInputViewController isKindOfClass:[WKTextInputListViewController class]]) |
| return; |
| |
| [(WKTextInputListViewController *)_presentedFullScreenInputViewController updateTextSuggestions:[_focusedFormControlView suggestions]]; |
| } |
| |
| #pragma mark - WKSelectMenuListViewControllerDelegate |
| |
| - (void)selectMenu:(WKSelectMenuListViewController *)selectMenu didSelectItemAtIndex:(NSUInteger)index |
| { |
| ASSERT(!_focusedElementInformation.isMultiSelect); |
| [self updateFocusedElementSelectedIndex:index allowsMultipleSelection:false]; |
| } |
| |
| - (NSUInteger)numberOfItemsInSelectMenu:(WKSelectMenuListViewController *)selectMenu |
| { |
| return self.focusedSelectElementOptions.size(); |
| } |
| |
| - (NSString *)selectMenu:(WKSelectMenuListViewController *)selectMenu displayTextForItemAtIndex:(NSUInteger)index |
| { |
| auto& options = self.focusedSelectElementOptions; |
| if (index >= options.size()) { |
| ASSERT_NOT_REACHED(); |
| return @""; |
| } |
| |
| return options[index].text; |
| } |
| |
| - (void)selectMenu:(WKSelectMenuListViewController *)selectMenu didCheckItemAtIndex:(NSUInteger)index checked:(BOOL)checked |
| { |
| ASSERT(_focusedElementInformation.isMultiSelect); |
| if (index >= self.focusedSelectElementOptions.size()) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| auto& option = self.focusedSelectElementOptions[index]; |
| if (option.isSelected == checked) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| [self updateFocusedElementSelectedIndex:index allowsMultipleSelection:true]; |
| option.isSelected = checked; |
| } |
| |
| - (BOOL)selectMenuUsesMultipleSelection:(WKSelectMenuListViewController *)selectMenu |
| { |
| return _focusedElementInformation.isMultiSelect; |
| } |
| |
| - (BOOL)selectMenu:(WKSelectMenuListViewController *)selectMenu hasSelectedOptionAtIndex:(NSUInteger)index |
| { |
| if (index >= self.focusedSelectElementOptions.size()) { |
| ASSERT_NOT_REACHED(); |
| return NO; |
| } |
| |
| return self.focusedSelectElementOptions[index].isSelected; |
| } |
| |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| |
| #pragma mark - PUICQuickboardControllerDelegate |
| |
| - (void)quickboardController:(PUICQuickboardController *)controller textInputValueDidChange:(NSAttributedString *)attributedText |
| { |
| _page->setTextAsync(attributedText.string); |
| [self dismissQuickboardViewControllerAndRevealFocusedFormOverlayIfNecessary:controller]; |
| } |
| |
| - (void)quickboardControllerTextInputValueCancelled:(PUICQuickboardController *)controller |
| { |
| [self dismissQuickboardViewControllerAndRevealFocusedFormOverlayIfNecessary:controller]; |
| } |
| |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| |
| #endif // HAVE(PEPPER_UI_CORE) |
| |
| - (void)_wheelChangedWithEvent:(UIEvent *)event |
| { |
| #if HAVE(PEPPER_UI_CORE) |
| if ([_focusedFormControlView handleWheelEvent:event]) |
| return; |
| #endif |
| [super _wheelChangedWithEvent:event]; |
| } |
| |
| - (void)_updateSelectionAssistantSuppressionState |
| { |
| static const double minimumFocusedElementAreaForSuppressingSelectionAssistant = 4; |
| |
| auto& editorState = _page->editorState(); |
| if (editorState.isMissingPostLayoutData) |
| return; |
| |
| BOOL editableRootIsTransparentOrFullyClipped = NO; |
| BOOL focusedElementIsTooSmall = NO; |
| if (!editorState.selectionIsNone) { |
| auto& postLayoutData = editorState.postLayoutData(); |
| if (postLayoutData.editableRootIsTransparentOrFullyClipped) |
| editableRootIsTransparentOrFullyClipped = YES; |
| |
| if (self._hasFocusedElement) { |
| auto elementArea = postLayoutData.selectionClipRect.area<RecordOverflow>(); |
| if (!elementArea.hasOverflowed() && elementArea < minimumFocusedElementAreaForSuppressingSelectionAssistant) |
| focusedElementIsTooSmall = YES; |
| } |
| } |
| |
| if (editableRootIsTransparentOrFullyClipped) |
| [self _startSuppressingSelectionAssistantForReason:WebKit::EditableRootIsTransparentOrFullyClipped]; |
| else |
| [self _stopSuppressingSelectionAssistantForReason:WebKit::EditableRootIsTransparentOrFullyClipped]; |
| |
| if (focusedElementIsTooSmall) |
| [self _startSuppressingSelectionAssistantForReason:WebKit::FocusedElementIsTooSmall]; |
| else |
| [self _stopSuppressingSelectionAssistantForReason:WebKit::FocusedElementIsTooSmall]; |
| } |
| |
| - (void)_selectionChanged |
| { |
| #if ENABLE(APP_HIGHLIGHTS) |
| [self setUpAppHighlightMenusIfNeeded]; |
| #endif |
| |
| [self _updateSelectionAssistantSuppressionState]; |
| |
| _cachedSelectedTextRange = nil; |
| _selectionNeedsUpdate = YES; |
| // If we are changing the selection with a gesture there is no need |
| // to wait to paint the selection. |
| if (_usingGestureForSelection) |
| [self _updateChangedSelection]; |
| |
| if (_candidateViewNeedsUpdate) { |
| _candidateViewNeedsUpdate = NO; |
| if ([self.inputDelegate respondsToSelector:@selector(layoutHasChanged)]) |
| [(id <UITextInputDelegatePrivate>)self.inputDelegate layoutHasChanged]; |
| } |
| |
| [_webView _didChangeEditorState]; |
| |
| if (!_page->editorState().isMissingPostLayoutData) { |
| _lastInsertedCharacterToOverrideCharacterBeforeSelection = std::nullopt; |
| if (!_usingGestureForSelection && _focusedElementInformation.autocapitalizeType == WebCore::AutocapitalizeType::Words) |
| [UIKeyboardImpl.sharedInstance clearShiftState]; |
| |
| if (!_usingGestureForSelection && !_selectionChangeNestingLevel && _page->editorState().triggeredByAccessibilitySelectionChange) { |
| // Force UIKit to reload all EditorState-based UI; in particular, this includes text candidates. |
| [self beginSelectionChange]; |
| [self endSelectionChange]; |
| } |
| } |
| } |
| |
| - (void)selectWordForReplacement |
| { |
| [self beginSelectionChange]; |
| _page->extendSelection(WebCore::TextGranularity::WordGranularity, [weakSelf = WeakObjCPtr<WKContentView>(self)] { |
| if (auto strongSelf = weakSelf.get()) |
| [strongSelf endSelectionChange]; |
| }); |
| } |
| |
| #if ENABLE(PLATFORM_DRIVEN_TEXT_CHECKING) |
| - (void)replaceSelectionOffset:(NSInteger)selectionOffset length:(NSUInteger)length withAnnotatedString:(NSAttributedString *)annotatedString relativeReplacementRange:(NSRange)relativeReplacementRange |
| { |
| _textCheckingController->replaceRelativeToSelection(annotatedString, selectionOffset, length, relativeReplacementRange.location, relativeReplacementRange.length); |
| } |
| |
| - (void)removeAnnotation:(NSAttributedStringKey)annotationName forSelectionOffset:(NSInteger)selectionOffset length:(NSUInteger)length |
| { |
| _textCheckingController->removeAnnotationRelativeToSelection(annotationName, selectionOffset, length); |
| } |
| #endif |
| |
| - (void)_updateChangedSelection |
| { |
| [self _updateChangedSelection:NO]; |
| } |
| |
| - (void)_updateChangedSelection:(BOOL)force |
| { |
| auto& editorState = _page->editorState(); |
| if (editorState.isMissingPostLayoutData) |
| return; |
| |
| auto& postLayoutData = editorState.postLayoutData(); |
| WebKit::WKSelectionDrawingInfo selectionDrawingInfo { editorState }; |
| if (force || selectionDrawingInfo != _lastSelectionDrawingInfo) { |
| LOG_WITH_STREAM(Selection, stream << "_updateChangedSelection " << selectionDrawingInfo); |
| |
| if (_lastSelectionDrawingInfo.caretColor != selectionDrawingInfo.caretColor) { |
| // Force UIKit to update the background color of the selection caret to reflect the new -insertionPointColor. |
| [[_textInteractionAssistant selectionView] tintColorDidChange]; |
| } |
| |
| _cachedSelectedTextRange = nil; |
| _lastSelectionDrawingInfo = selectionDrawingInfo; |
| |
| // FIXME: We need to figure out what to do if the selection is changed by Javascript. |
| if (_textInteractionAssistant) { |
| _markedText = editorState.hasComposition ? postLayoutData.markedText : String { }; |
| [_textInteractionAssistant selectionChanged]; |
| } |
| |
| _selectionNeedsUpdate = NO; |
| if (_shouldRestoreSelection) { |
| [_textInteractionAssistant didEndScrollingOverflow]; |
| _shouldRestoreSelection = NO; |
| } |
| } |
| |
| if (postLayoutData.isStableStateUpdate && _needsDeferredEndScrollingSelectionUpdate && _page->inStableState()) { |
| auto firstResponder = self.firstResponder; |
| if ((!firstResponder || self == firstResponder) && !_suppressSelectionAssistantReasons) |
| [_textInteractionAssistant activateSelection]; |
| |
| [_textInteractionAssistant didEndScrollingOverflow]; |
| |
| _needsDeferredEndScrollingSelectionUpdate = NO; |
| } |
| } |
| |
| - (BOOL)shouldAllowHidingSelectionCommands |
| { |
| ASSERT(_ignoreSelectionCommandFadeCount >= 0); |
| return !_ignoreSelectionCommandFadeCount; |
| } |
| |
| - (BOOL)supportsTextSelectionWithCharacterGranularity |
| { |
| return YES; |
| } |
| |
| - (BOOL)hasHiddenContentEditable |
| { |
| return _suppressSelectionAssistantReasons.containsAny({ WebKit::EditableRootIsTransparentOrFullyClipped, WebKit::FocusedElementIsTooSmall }); |
| } |
| |
| - (BOOL)_shouldSuppressSelectionCommands |
| { |
| return !!_suppressSelectionAssistantReasons; |
| } |
| |
| - (void)_startSuppressingSelectionAssistantForReason:(WebKit::SuppressSelectionAssistantReason)reason |
| { |
| bool wasSuppressingSelectionAssistant = !!_suppressSelectionAssistantReasons; |
| _suppressSelectionAssistantReasons.add(reason); |
| |
| if (!wasSuppressingSelectionAssistant) |
| [_textInteractionAssistant deactivateSelection]; |
| } |
| |
| - (void)_stopSuppressingSelectionAssistantForReason:(WebKit::SuppressSelectionAssistantReason)reason |
| { |
| bool wasSuppressingSelectionAssistant = !!_suppressSelectionAssistantReasons; |
| _suppressSelectionAssistantReasons.remove(reason); |
| |
| if (wasSuppressingSelectionAssistant && !_suppressSelectionAssistantReasons) |
| [_textInteractionAssistant activateSelection]; |
| } |
| |
| #if ENABLE(DATALIST_ELEMENT) |
| |
| - (UIView <WKFormControl> *)dataListTextSuggestionsInputView |
| { |
| return _dataListTextSuggestionsInputView.get(); |
| } |
| |
| - (NSArray<UITextSuggestion *> *)dataListTextSuggestions |
| { |
| return _dataListTextSuggestions.get(); |
| } |
| |
| - (void)setDataListTextSuggestionsInputView:(UIView <WKFormControl> *)suggestionsInputView |
| { |
| if (_dataListTextSuggestionsInputView == suggestionsInputView) |
| return; |
| |
| _dataListTextSuggestionsInputView = suggestionsInputView; |
| |
| if (![_formInputSession customInputView]) |
| [self reloadInputViews]; |
| } |
| |
| - (void)setDataListTextSuggestions:(NSArray<UITextSuggestion *> *)textSuggestions |
| { |
| if (textSuggestions == _dataListTextSuggestions || [textSuggestions isEqualToArray:_dataListTextSuggestions.get()]) |
| return; |
| |
| _dataListTextSuggestions = textSuggestions; |
| |
| if (![_formInputSession suggestions].count) |
| [self updateTextSuggestionsForInputDelegate]; |
| } |
| |
| #endif |
| |
| - (void)updateTextSuggestionsForInputDelegate |
| { |
| // Text suggestions vended from clients take precedence over text suggestions from a focused form control with a datalist. |
| id <UITextInputSuggestionDelegate> inputDelegate = (id <UITextInputSuggestionDelegate>)self.inputDelegate; |
| NSArray<UITextSuggestion *> *formInputSessionSuggestions = [_formInputSession suggestions]; |
| if (formInputSessionSuggestions.count) { |
| [inputDelegate setSuggestions:formInputSessionSuggestions]; |
| return; |
| } |
| |
| #if ENABLE(DATALIST_ELEMENT) |
| if ([_dataListTextSuggestions count]) { |
| [inputDelegate setSuggestions:_dataListTextSuggestions.get()]; |
| return; |
| } |
| #endif |
| |
| [inputDelegate setSuggestions:nil]; |
| } |
| |
| - (void)_showPlaybackTargetPicker:(BOOL)hasVideo fromRect:(const WebCore::IntRect&)elementRect routeSharingPolicy:(WebCore::RouteSharingPolicy)routeSharingPolicy routingContextUID:(NSString *)routingContextUID |
| { |
| #if ENABLE(AIRPLAY_PICKER) |
| // FIXME: Likely we can remove this special case for watchOS and tvOS. |
| #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) |
| if (!_airPlayRoutePicker) |
| _airPlayRoutePicker = adoptNS([[WKAirPlayRoutePicker alloc] init]); |
| [_airPlayRoutePicker showFromView:self routeSharingPolicy:routeSharingPolicy routingContextUID:routingContextUID hasVideo:hasVideo]; |
| #else |
| if (!_airPlayRoutePicker) |
| _airPlayRoutePicker = adoptNS([[WKAirPlayRoutePicker alloc] initWithView:self]); |
| [_airPlayRoutePicker show:hasVideo fromRect:elementRect]; |
| #endif |
| #endif |
| } |
| |
| - (void)_showRunOpenPanel:(API::OpenPanelParameters*)parameters frameInfo:(const WebKit::FrameInfoData&)frameInfo resultListener:(WebKit::WebOpenPanelResultListenerProxy*)listener |
| { |
| ASSERT(!_fileUploadPanel); |
| if (_fileUploadPanel) |
| return; |
| |
| _frameInfoForFileUploadPanel = frameInfo; |
| _fileUploadPanel = adoptNS([[WKFileUploadPanel alloc] initWithView:self]); |
| [_fileUploadPanel setDelegate:self]; |
| [_fileUploadPanel presentWithParameters:parameters resultListener:listener]; |
| } |
| |
| - (void)fileUploadPanelDidDismiss:(WKFileUploadPanel *)fileUploadPanel |
| { |
| ASSERT(_fileUploadPanel.get() == fileUploadPanel); |
| |
| [_fileUploadPanel setDelegate:nil]; |
| _fileUploadPanel = nil; |
| } |
| |
| - (BOOL)fileUploadPanelDestinationIsManaged:(WKFileUploadPanel *)fileUploadPanel |
| { |
| ASSERT(_fileUploadPanel.get() == fileUploadPanel); |
| |
| auto webView = _webView.get(); |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([webView UIDelegate]); |
| return [uiDelegate respondsToSelector:@selector(_webView:fileUploadPanelContentIsManagedWithInitiatingFrame:)] |
| && [uiDelegate _webView:webView.get() fileUploadPanelContentIsManagedWithInitiatingFrame:wrapper(API::FrameInfo::create(WTFMove(_frameInfoForFileUploadPanel), _page.get()))]; |
| } |
| |
| - (void)_showShareSheet:(const WebCore::ShareDataWithParsedURL&)data inRect:(std::optional<WebCore::FloatRect>)rect completionHandler:(CompletionHandler<void(bool)>&&)completionHandler |
| { |
| #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) |
| if (_shareSheet) |
| [_shareSheet dismiss]; |
| |
| _shareSheet = adoptNS([[WKShareSheet alloc] initWithView:self.webView]); |
| [_shareSheet setDelegate:self]; |
| |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) |
| if (!rect) { |
| if (auto lastMouseLocation = [_mouseGestureRecognizer lastMouseLocation]) { |
| auto hoverLocationInWebView = [self convertPoint:*lastMouseLocation toView:self.webView]; |
| rect = WebCore::FloatRect(hoverLocationInWebView.x, hoverLocationInWebView.y, 1, 1); |
| } |
| } |
| #endif |
| |
| [_shareSheet presentWithParameters:data inRect:rect completionHandler:WTFMove(completionHandler)]; |
| #endif |
| } |
| |
| #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) |
| - (void)shareSheetDidDismiss:(WKShareSheet *)shareSheet |
| { |
| ASSERT(_shareSheet == shareSheet); |
| |
| [_shareSheet setDelegate:nil]; |
| _shareSheet = nil; |
| } |
| |
| - (void)shareSheet:(WKShareSheet *)shareSheet willShowActivityItems:(NSArray *)activityItems |
| { |
| ASSERT(_shareSheet == shareSheet); |
| |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| if ([uiDelegate respondsToSelector:@selector(_webView:willShareActivityItems:)]) |
| [uiDelegate _webView:self.webView willShareActivityItems:activityItems]; |
| } |
| |
| #endif |
| |
| - (void)_showContactPicker:(const WebCore::ContactsRequestData&)requestData completionHandler:(WTF::CompletionHandler<void(std::optional<Vector<WebCore::ContactInfo>>&&)>&&)completionHandler |
| { |
| #if HAVE(CONTACTSUI) |
| _contactPicker = adoptNS([[WKContactPicker alloc] initWithView:self.webView]); |
| [_contactPicker setDelegate:self]; |
| [_contactPicker presentWithRequestData:requestData completionHandler:WTFMove(completionHandler)]; |
| #else |
| completionHandler(std::nullopt); |
| #endif |
| } |
| |
| #if HAVE(CONTACTSUI) |
| - (void)contactPickerDidPresent:(WKContactPicker *)contactPicker |
| { |
| ASSERT(_contactPicker == contactPicker); |
| |
| [_webView _didPresentContactPicker]; |
| } |
| |
| - (void)contactPickerDidDismiss:(WKContactPicker *)contactPicker |
| { |
| ASSERT(_contactPicker == contactPicker); |
| |
| [_contactPicker setDelegate:nil]; |
| _contactPicker = nil; |
| |
| [_webView _didDismissContactPicker]; |
| } |
| #endif |
| |
| - (NSString *)inputLabelText |
| { |
| if (!_focusedElementInformation.label.isEmpty()) |
| return _focusedElementInformation.label; |
| |
| if (!_focusedElementInformation.ariaLabel.isEmpty()) |
| return _focusedElementInformation.ariaLabel; |
| |
| if (!_focusedElementInformation.title.isEmpty()) |
| return _focusedElementInformation.title; |
| |
| return _focusedElementInformation.placeholder; |
| } |
| |
| #pragma mark - UITextInputMultiDocument |
| |
| - (BOOL)_restoreFocusWithToken:(id <NSCopying, NSSecureCoding>)token |
| { |
| if (_focusStateStack.isEmpty()) { |
| ASSERT_NOT_REACHED(); |
| return NO; |
| } |
| |
| if (_focusStateStack.takeLast()) |
| [_webView _decrementFocusPreservationCount]; |
| |
| // FIXME: Our current behavior in -_restoreFocusWithToken: does not force the web view to become first responder |
| // by refocusing the currently focused element. As such, we return NO here so that UIKit will tell WKContentView |
| // to become first responder in the future. |
| return NO; |
| } |
| |
| - (void)startRelinquishingFirstResponderToFocusedElement |
| { |
| if (_isRelinquishingFirstResponderToFocusedElement) |
| return; |
| |
| _isRelinquishingFirstResponderToFocusedElement = YES; |
| [_webView _incrementFocusPreservationCount]; |
| } |
| |
| - (void)stopRelinquishingFirstResponderToFocusedElement |
| { |
| if (!_isRelinquishingFirstResponderToFocusedElement) |
| return; |
| |
| _isRelinquishingFirstResponderToFocusedElement = NO; |
| [_webView _decrementFocusPreservationCount]; |
| } |
| |
| - (void)_preserveFocusWithToken:(id <NSCopying, NSSecureCoding>)token destructively:(BOOL)destructively |
| { |
| if (!_inputPeripheral) { |
| [_webView _incrementFocusPreservationCount]; |
| _focusStateStack.append(true); |
| } else |
| _focusStateStack.append(false); |
| } |
| |
| #pragma mark - Implementation of UIWebTouchEventsGestureRecognizerDelegate. |
| |
| - (BOOL)gestureRecognizer:(UIWebTouchEventsGestureRecognizer *)gestureRecognizer shouldIgnoreWebTouchWithEvent:(UIEvent *)event |
| { |
| _touchEventsCanPreventNativeGestures = YES; |
| |
| return [self gestureRecognizer:gestureRecognizer isInterruptingMomentumScrollingWithEvent:event]; |
| } |
| |
| - (BOOL)gestureRecognizer:(UIGestureRecognizer *)gestureRecognizer isInterruptingMomentumScrollingWithEvent:(UIEvent *)event |
| { |
| NSSet<UITouch *> *touches = [event touchesForGestureRecognizer:gestureRecognizer]; |
| for (UITouch *touch in touches) { |
| if ([touch.view isKindOfClass:[UIScrollView class]] && [(UIScrollView *)touch.view _isInterruptingDeceleration]) |
| return YES; |
| } |
| return self._scroller._isInterruptingDeceleration; |
| } |
| |
| - (BOOL)isAnyTouchOverActiveArea:(NSSet *)touches |
| { |
| return YES; |
| } |
| |
| #pragma mark - Implementation of WKActionSheetAssistantDelegate. |
| |
| - (std::optional<WebKit::InteractionInformationAtPosition>)positionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant |
| { |
| WebKit::InteractionInformationRequest request(_positionInformation.request.point); |
| request.includeSnapshot = true; |
| request.includeLinkIndicator = assistant.needsLinkIndicator; |
| request.linkIndicatorShouldHaveLegacyMargins = !self._shouldUseContextMenus; |
| if (![self ensurePositionInformationIsUpToDate:request]) |
| return std::nullopt; |
| |
| return _positionInformation; |
| } |
| |
| - (void)updatePositionInformationForActionSheetAssistant:(WKActionSheetAssistant *)assistant |
| { |
| _hasValidPositionInformation = NO; |
| WebKit::InteractionInformationRequest request(_positionInformation.request.point); |
| request.includeSnapshot = true; |
| request.includeLinkIndicator = assistant.needsLinkIndicator; |
| request.linkIndicatorShouldHaveLegacyMargins = !self._shouldUseContextMenus; |
| |
| [self requestAsynchronousPositionInformationUpdate:request]; |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant performAction:(WebKit::SheetAction)action |
| { |
| _page->performActionOnElement((uint32_t)action); |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant openElementAtLocation:(CGPoint)location |
| { |
| [self _attemptSyntheticClickAtLocation:location modifierFlags:0]; |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant shareElementWithURL:(NSURL *)url rect:(CGRect)boundingRect |
| { |
| WebCore::ShareDataWithParsedURL shareData; |
| shareData.url = { url }; |
| [self _showShareSheet:shareData inRect: { [self convertRect:boundingRect toView:self.webView] } completionHandler:nil]; |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant shareElementWithImage:(UIImage *)image rect:(CGRect)boundingRect |
| { |
| WebCore::ShareDataWithParsedURL shareData; |
| NSString* fileName = [NSString stringWithFormat:@"%@.png", (NSString*)WEB_UI_STRING("Shared Image", "Default name for the file created for a shared image with no explicit name.")]; |
| shareData.files = { { fileName, WebCore::SharedBuffer::create(UIImagePNGRepresentation(image)) } }; |
| [self _showShareSheet:shareData inRect: { [self convertRect:boundingRect toView:self.webView] } completionHandler:nil]; |
| } |
| |
| #if HAVE(APP_LINKS) |
| - (BOOL)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeAppLinkActionsForElement:(_WKActivatedElementInfo *)element |
| { |
| return _page->uiClient().shouldIncludeAppLinkActionsForElement(element); |
| } |
| #endif |
| |
| - (BOOL)actionSheetAssistant:(WKActionSheetAssistant *)assistant showCustomSheetForElement:(_WKActivatedElementInfo *)element |
| { |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| |
| if ([uiDelegate respondsToSelector:@selector(_webView:showCustomSheetForElement:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate _webView:self.webView showCustomSheetForElement:element]) { |
| #if ENABLE(DRAG_SUPPORT) |
| BOOL shouldCancelAllTouches = !_dragDropInteractionState.dragSession(); |
| #else |
| BOOL shouldCancelAllTouches = YES; |
| #endif |
| |
| // Prevent tap-and-hold and panning. |
| if (shouldCancelAllTouches) |
| [UIApp _cancelAllTouches]; |
| |
| return YES; |
| } |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| |
| return NO; |
| } |
| |
| // FIXME: Likely we can remove this special case for watchOS and tvOS. |
| #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) |
| - (CGRect)unoccludedWindowBoundsForActionSheetAssistant:(WKActionSheetAssistant *)assistant |
| { |
| UIEdgeInsets contentInset = [[_webView scrollView] adjustedContentInset]; |
| CGRect rect = UIEdgeInsetsInsetRect([_webView bounds], contentInset); |
| return [_webView convertRect:rect toView:[self window]]; |
| } |
| #endif |
| |
| - (RetainPtr<NSArray>)actionSheetAssistant:(WKActionSheetAssistant *)assistant decideActionsForElement:(_WKActivatedElementInfo *)element defaultActions:(RetainPtr<NSArray>)defaultActions |
| { |
| return _page->uiClient().actionsForElement(element, WTFMove(defaultActions)); |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant willStartInteractionWithElement:(_WKActivatedElementInfo *)element |
| { |
| _page->startInteractionWithPositionInformation(_positionInformation); |
| } |
| |
| - (void)actionSheetAssistantDidStopInteraction:(WKActionSheetAssistant *)assistant |
| { |
| _page->stopInteraction(); |
| } |
| |
| - (NSDictionary *)dataDetectionContextForPositionInformation:(const WebKit::InteractionInformationAtPosition&)positionInformation |
| { |
| RetainPtr<NSMutableDictionary> context; |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| if ([uiDelegate respondsToSelector:@selector(_dataDetectionContextForWebView:)]) |
| context = adoptNS([[uiDelegate _dataDetectionContextForWebView:self.webView] mutableCopy]); |
| |
| if (!context) |
| context = adoptNS([[NSMutableDictionary alloc] init]); |
| |
| #if ENABLE(DATA_DETECTION) |
| if (!positionInformation.textBefore.isEmpty()) |
| context.get()[getkDataDetectorsLeadingText()] = positionInformation.textBefore; |
| if (!positionInformation.textAfter.isEmpty()) |
| context.get()[getkDataDetectorsTrailingText()] = positionInformation.textAfter; |
| |
| CGRect sourceRect; |
| if (positionInformation.isLink) |
| sourceRect = positionInformation.linkIndicator.textBoundingRectInRootViewCoordinates; |
| else if (!positionInformation.dataDetectorBounds.isEmpty()) |
| sourceRect = positionInformation.dataDetectorBounds; |
| else |
| sourceRect = positionInformation.bounds; |
| |
| CGRect frameInContainerViewCoordinates = [self convertRect:sourceRect toView:self.containerForContextMenuHintPreviews]; |
| return [getDDContextMenuActionClass() updateContext:context.get() withSourceRect:frameInContainerViewCoordinates]; |
| #else |
| return context.autorelease(); |
| #endif |
| } |
| |
| - (NSDictionary *)dataDetectionContextForActionSheetAssistant:(WKActionSheetAssistant *)assistant positionInformation:(const WebKit::InteractionInformationAtPosition&)positionInformation |
| { |
| return [self dataDetectionContextForPositionInformation:positionInformation]; |
| } |
| |
| - (NSString *)selectedTextForActionSheetAssistant:(WKActionSheetAssistant *)assistant |
| { |
| return [self selectedText]; |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant getAlternateURLForImage:(UIImage *)image completion:(void (^)(NSURL *alternateURL, NSDictionary *userInfo))completion |
| { |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| if ([uiDelegate respondsToSelector:@selector(_webView:getAlternateURLFromImage:completionHandler:)]) { |
| [uiDelegate _webView:self.webView getAlternateURLFromImage:image completionHandler:^(NSURL *alternateURL, NSDictionary *userInfo) { |
| completion(alternateURL, userInfo); |
| }]; |
| } else |
| completion(nil, nil); |
| } |
| |
| #if USE(UICONTEXTMENU) |
| |
| - (UITargetedPreview *)createTargetedContextMenuHintForActionSheetAssistant:(WKActionSheetAssistant *)assistant |
| { |
| return [self _createTargetedContextMenuHintPreviewIfPossible]; |
| } |
| |
| - (void)removeContextMenuViewIfPossibleForActionSheetAssistant:(WKActionSheetAssistant *)assistant |
| { |
| [self _removeContextMenuHintContainerIfPossible]; |
| } |
| |
| - (void)actionSheetAssistantDidShowContextMenu:(WKActionSheetAssistant *)assistant |
| { |
| [_webView _didShowContextMenu]; |
| } |
| |
| - (void)actionSheetAssistantDidDismissContextMenu:(WKActionSheetAssistant *)assistant |
| { |
| [_webView _didDismissContextMenu]; |
| } |
| |
| - (void)_targetedPreviewContainerDidRemoveLastSubview:(WKTargetedPreviewContainer *)containerView |
| { |
| if (_contextMenuHintContainerView == containerView) |
| [self _removeContextMenuHintContainerIfPossible]; |
| } |
| |
| #endif // USE(UICONTEXTMENU) |
| |
| - (BOOL)_shouldUseContextMenus |
| { |
| #if HAVE(LINK_PREVIEW) && USE(UICONTEXTMENU) |
| return linkedOnOrAfter(SDKVersion::FirstThatHasUIContextMenuInteraction); |
| #endif |
| return NO; |
| } |
| |
| - (BOOL)_shouldUseContextMenusForFormControls |
| { |
| #if ENABLE(IOS_FORM_CONTROL_REFRESH) |
| return self._formControlRefreshEnabled && self._shouldUseContextMenus; |
| #endif |
| return NO; |
| } |
| |
| - (BOOL)_shouldAvoidResizingWhenInputViewBoundsChange |
| { |
| return _focusedElementInformation.shouldAvoidResizingWhenInputViewBoundsChange; |
| } |
| |
| - (BOOL)_shouldAvoidScrollingWhenFocusedContentIsVisible |
| { |
| return _focusedElementInformation.shouldAvoidScrollingWhenFocusedContentIsVisible; |
| } |
| |
| - (BOOL)_shouldUseLegacySelectPopoverDismissalBehavior |
| { |
| if (WebKit::currentUserInterfaceIdiomIsSmallScreen()) |
| return NO; |
| |
| if (_focusedElementInformation.elementType != WebKit::InputType::Select) |
| return NO; |
| |
| if (!_focusedElementInformation.shouldUseLegacySelectPopoverDismissalBehaviorInDataActivation) |
| return NO; |
| |
| return WebCore::IOSApplication::isDataActivation(); |
| } |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| |
| - (BOOL)shouldDeferGestureDueToImageAnalysis:(UIGestureRecognizer *)gesture |
| { |
| return gesture == [_textInteractionAssistant loupeGesture] || gesture._wk_isTapAndAHalf || gesture == [_textInteractionAssistant forcePressGesture]; |
| } |
| |
| #endif // ENABLE(IMAGE_ANALYSIS) |
| |
| #if HAVE(PASTEBOARD_DATA_OWNER) |
| |
| static WebCore::DataOwnerType coreDataOwnerType(_UIDataOwner platformType) |
| { |
| switch (platformType) { |
| case _UIDataOwnerUser: |
| return WebCore::DataOwnerType::User; |
| case _UIDataOwnerEnterprise: |
| return WebCore::DataOwnerType::Enterprise; |
| case _UIDataOwnerShared: |
| return WebCore::DataOwnerType::Shared; |
| case _UIDataOwnerUndefined: |
| return WebCore::DataOwnerType::Undefined; |
| } |
| ASSERT_NOT_REACHED(); |
| return WebCore::DataOwnerType::Undefined; |
| } |
| |
| - (WebCore::DataOwnerType)_dataOwnerForPasteboard:(WebKit::PasteboardAccessIntent)intent |
| { |
| if (![self respondsToSelector:@selector(_dataOwnerForPaste)]) { |
| // FIXME: Remove this once the relevant bots have fix for <rdar://problem/73852335>. |
| return WebCore::DataOwnerType::Undefined; |
| } |
| |
| if (intent == WebKit::PasteboardAccessIntent::Read) |
| return coreDataOwnerType(self._dataOwnerForPaste); |
| |
| if (intent == WebKit::PasteboardAccessIntent::Write) |
| return coreDataOwnerType(self._dataOwnerForCopy); |
| |
| ASSERT_NOT_REACHED(); |
| return WebCore::DataOwnerType::Undefined; |
| } |
| |
| #endif // HAVE(PASTEBOARD_DATA_OWNER) |
| |
| - (RetainPtr<WKTargetedPreviewContainer>)_createPreviewContainerWithLayerName:(NSString *)layerName |
| { |
| auto container = adoptNS([[WKTargetedPreviewContainer alloc] initWithContentView:self]); |
| [container layer].anchorPoint = CGPointZero; |
| [container layer].name = layerName; |
| return container; |
| } |
| |
| - (UIView *)containerForDropPreviews |
| { |
| if (!_dropPreviewContainerView) { |
| _dropPreviewContainerView = [self _createPreviewContainerWithLayerName:@"Drop Preview Container"]; |
| [_interactionViewsContainerView addSubview:_dropPreviewContainerView.get()]; |
| } |
| |
| ASSERT([_dropPreviewContainerView superview]); |
| return _dropPreviewContainerView.get(); |
| } |
| |
| - (void)_removeContainerForDropPreviews |
| { |
| if (!_dropPreviewContainerView) |
| return; |
| |
| [std::exchange(_dropPreviewContainerView, nil) removeFromSuperview]; |
| } |
| |
| - (UIView *)containerForDragPreviews |
| { |
| if (!_dragPreviewContainerView) { |
| _dragPreviewContainerView = [self _createPreviewContainerWithLayerName:@"Drag Preview Container"]; |
| [_interactionViewsContainerView addSubview:_dragPreviewContainerView.get()]; |
| } |
| |
| ASSERT([_dragPreviewContainerView superview]); |
| return _dragPreviewContainerView.get(); |
| } |
| |
| - (void)_removeContainerForDragPreviews |
| { |
| if (!_dragPreviewContainerView) |
| return; |
| |
| [std::exchange(_dragPreviewContainerView, nil) removeFromSuperview]; |
| } |
| |
| - (UIView *)containerForContextMenuHintPreviews |
| { |
| if (!_contextMenuHintContainerView) { |
| _contextMenuHintContainerView = [self _createPreviewContainerWithLayerName:@"Context Menu Hint Preview Container"]; |
| |
| RetainPtr<UIView> containerView; |
| |
| if (auto uiDelegate = static_cast<id<WKUIDelegatePrivate>>(self.webView.UIDelegate)) { |
| if ([uiDelegate respondsToSelector:@selector(_contextMenuHintPreviewContainerViewForWebView:)]) |
| containerView = [uiDelegate _contextMenuHintPreviewContainerViewForWebView:self.webView]; |
| } |
| |
| if (!containerView) |
| containerView = _interactionViewsContainerView; |
| |
| [containerView addSubview:_contextMenuHintContainerView.get()]; |
| } |
| |
| ASSERT([_contextMenuHintContainerView superview]); |
| return _contextMenuHintContainerView.get(); |
| } |
| |
| - (void)_removeContainerForContextMenuHintPreviews |
| { |
| if (!_contextMenuHintContainerView) |
| return; |
| |
| [std::exchange(_contextMenuHintContainerView, nil) removeFromSuperview]; |
| |
| _scrollViewForTargetedPreview = nil; |
| _scrollViewForTargetedPreviewInitialOffset = CGPointZero; |
| } |
| |
| - (void)_updateFrameOfContainerForContextMenuHintPreviewsIfNeeded |
| { |
| if (!_contextMenuHintContainerView) |
| return; |
| |
| CGPoint newOffset = [_scrollViewForTargetedPreview convertPoint:CGPointZero toView:[_contextMenuHintContainerView superview]]; |
| |
| CGRect frame = [_contextMenuHintContainerView frame]; |
| frame.origin.x = newOffset.x - _scrollViewForTargetedPreviewInitialOffset.x; |
| frame.origin.y = newOffset.y - _scrollViewForTargetedPreviewInitialOffset.y; |
| [_contextMenuHintContainerView setFrame:frame]; |
| } |
| |
| - (void)_updateTargetedPreviewScrollViewUsingContainerScrollingNodeID:(WebCore::ScrollingNodeID)scrollingNodeID |
| { |
| if (scrollingNodeID) { |
| if (auto* scrollingCoordinator = _page->scrollingCoordinatorProxy()) { |
| if (UIScrollView *scrollViewForScrollingNode = scrollingCoordinator->scrollViewForScrollingNodeID(scrollingNodeID)) |
| _scrollViewForTargetedPreview = scrollViewForScrollingNode; |
| } |
| } |
| |
| if (!_scrollViewForTargetedPreview) |
| _scrollViewForTargetedPreview = self.webView.scrollView; |
| |
| _scrollViewForTargetedPreviewInitialOffset = [_scrollViewForTargetedPreview convertPoint:CGPointZero toView:[_contextMenuHintContainerView superview]]; |
| } |
| |
| #pragma mark - WKDeferringGestureRecognizerDelegate |
| |
| - (WebKit::ShouldDeferGestures)deferringGestureRecognizer:(WKDeferringGestureRecognizer *)deferringGestureRecognizer willBeginTouchesWithEvent:(UIEvent *)event |
| { |
| self.gestureRecognizerConsistencyEnforcer.beginTracking(deferringGestureRecognizer); |
| |
| return [self gestureRecognizer:deferringGestureRecognizer isInterruptingMomentumScrollingWithEvent:event] ? WebKit::ShouldDeferGestures::No : WebKit::ShouldDeferGestures::Yes; |
| } |
| |
| - (void)deferringGestureRecognizer:(WKDeferringGestureRecognizer *)deferringGestureRecognizer didTransitionToState:(UIGestureRecognizerState)state |
| { |
| if (state == UIGestureRecognizerStateEnded || state == UIGestureRecognizerStateFailed || state == UIGestureRecognizerStateCancelled) |
| self.gestureRecognizerConsistencyEnforcer.endTracking(deferringGestureRecognizer); |
| } |
| |
| - (void)deferringGestureRecognizer:(WKDeferringGestureRecognizer *)deferringGestureRecognizer didEndTouchesWithEvent:(UIEvent *)event |
| { |
| self.gestureRecognizerConsistencyEnforcer.endTracking(deferringGestureRecognizer); |
| |
| if (deferringGestureRecognizer.state != UIGestureRecognizerStatePossible) |
| return; |
| |
| if (_page->isHandlingPreventableTouchStart() && [self _isTouchStartDeferringGesture:deferringGestureRecognizer]) |
| return; |
| |
| if (_page->isHandlingPreventableTouchEnd() && [self _isTouchEndDeferringGesture:deferringGestureRecognizer]) |
| return; |
| |
| if ([_touchEventGestureRecognizer state] == UIGestureRecognizerStatePossible) |
| return; |
| |
| // In the case where the touch event gesture recognizer has failed or ended already and we are not in the middle of handling |
| // an asynchronous (but preventable) touch event, this is our last chance to lift the gesture "gate" by failing the deferring |
| // gesture recognizer. |
| deferringGestureRecognizer.state = UIGestureRecognizerStateFailed; |
| } |
| |
| - (BOOL)deferringGestureRecognizer:(WKDeferringGestureRecognizer *)deferringGestureRecognizer shouldDeferOtherGestureRecognizer:(UIGestureRecognizer *)gestureRecognizer |
| { |
| #if ENABLE(IOS_TOUCH_EVENTS) |
| if ([self _touchEventsMustRequireGestureRecognizerToFail:gestureRecognizer]) |
| return NO; |
| |
| if (_failedTouchStartDeferringGestures && _failedTouchStartDeferringGestures->contains(deferringGestureRecognizer) |
| && deferringGestureRecognizer.state == UIGestureRecognizerStatePossible) { |
| // This deferring gesture no longer has an oppportunity to defer native gestures (either because the touch region did not have any |
| // active touch event listeners, or because any active touch event listeners on the page have already executed, and did not prevent |
| // default). UIKit may have already reset the gesture to Possible state underneath us, in which case we still need to treat it as |
| // if it has already failed; otherwise, we will incorrectly defer other gestures in the web view, such as scroll view pinching. |
| return NO; |
| } |
| |
| auto webView = _webView.getAutoreleased(); |
| auto view = gestureRecognizer.view; |
| BOOL gestureIsInstalledOnOrUnderWebView = NO; |
| while (view) { |
| if (view == webView) { |
| gestureIsInstalledOnOrUnderWebView = YES; |
| break; |
| } |
| view = view.superview; |
| } |
| |
| if (!gestureIsInstalledOnOrUnderWebView) |
| return NO; |
| |
| if ([gestureRecognizer isKindOfClass:WKDeferringGestureRecognizer.class]) |
| return NO; |
| |
| if (gestureRecognizer == _touchEventGestureRecognizer) |
| return NO; |
| |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) |
| if (gestureRecognizer == _mouseGestureRecognizer) |
| return NO; |
| #endif |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| if (deferringGestureRecognizer == _imageAnalysisDeferringGestureRecognizer) |
| return [self shouldDeferGestureDueToImageAnalysis:gestureRecognizer]; |
| #endif |
| |
| auto mayDelayResetOfContainingSubgraph = [&](UIGestureRecognizer *gesture) -> BOOL { |
| #if USE(UICONTEXTMENU) && HAVE(LINK_PREVIEW) |
| if (gesture == [_contextMenuInteraction gestureRecognizerForFailureRelationships]) |
| return YES; |
| #endif |
| |
| #if ENABLE(DRAG_SUPPORT) |
| if (gesture.delegate == [_dragInteraction _initiationDriver]) |
| return YES; |
| #endif |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| if (gestureRecognizer == _imageAnalysisGestureRecognizer || gestureRecognizer == _imageAnalysisTimeoutGestureRecognizer) |
| return YES; |
| #endif |
| |
| if (gestureRecognizer._wk_isTapAndAHalf) |
| return YES; |
| |
| if (gestureRecognizer == [_textInteractionAssistant loupeGesture]) |
| return YES; |
| |
| if ([gesture isKindOfClass:UITapGestureRecognizer.class]) { |
| UITapGestureRecognizer *tapGesture = (UITapGestureRecognizer *)gesture; |
| return tapGesture.numberOfTapsRequired > 1 && tapGesture.numberOfTouchesRequired < 2; |
| } |
| |
| return NO; |
| }; |
| |
| BOOL isSyntheticTap = [gestureRecognizer isKindOfClass:WKSyntheticTapGestureRecognizer.class]; |
| BOOL mayDelayReset = mayDelayResetOfContainingSubgraph(gestureRecognizer); |
| if ([gestureRecognizer isKindOfClass:UITapGestureRecognizer.class]) { |
| if (deferringGestureRecognizer == _touchEndDeferringGestureRecognizerForSyntheticTapGestures) |
| return isSyntheticTap; |
| |
| if (deferringGestureRecognizer == _touchEndDeferringGestureRecognizerForDelayedResettableGestures) |
| return !isSyntheticTap && mayDelayReset; |
| |
| if (deferringGestureRecognizer == _touchEndDeferringGestureRecognizerForImmediatelyResettableGestures) |
| return !isSyntheticTap && !mayDelayReset; |
| } |
| |
| if (isSyntheticTap) |
| return deferringGestureRecognizer == _touchStartDeferringGestureRecognizerForSyntheticTapGestures; |
| |
| if (mayDelayReset) |
| return deferringGestureRecognizer == _touchStartDeferringGestureRecognizerForDelayedResettableGestures; |
| |
| return deferringGestureRecognizer == _touchStartDeferringGestureRecognizerForImmediatelyResettableGestures; |
| #else |
| UNUSED_PARAM(deferringGestureRecognizer); |
| UNUSED_PARAM(gestureRecognizer); |
| return NO; |
| #endif |
| } |
| |
| #if ENABLE(DRAG_SUPPORT) |
| |
| static BOOL shouldEnableDragInteractionForPolicy(_WKDragInteractionPolicy policy) |
| { |
| switch (policy) { |
| case _WKDragInteractionPolicyAlwaysEnable: |
| return YES; |
| case _WKDragInteractionPolicyAlwaysDisable: |
| return NO; |
| default: |
| return [UIDragInteraction isEnabledByDefault]; |
| } |
| } |
| |
| - (void)_didChangeDragInteractionPolicy |
| { |
| [_dragInteraction setEnabled:shouldEnableDragInteractionForPolicy(self.webView._dragInteractionPolicy)]; |
| } |
| |
| - (NSTimeInterval)dragLiftDelay |
| { |
| static const NSTimeInterval mediumDragLiftDelay = 0.5; |
| static const NSTimeInterval longDragLiftDelay = 0.65; |
| auto dragLiftDelay = self.webView.configuration._dragLiftDelay; |
| if (dragLiftDelay == _WKDragLiftDelayMedium) |
| return mediumDragLiftDelay; |
| if (dragLiftDelay == _WKDragLiftDelayLong) |
| return longDragLiftDelay; |
| return _UIDragInteractionDefaultLiftDelay(); |
| } |
| |
| - (id <WKUIDelegatePrivate>)webViewUIDelegate |
| { |
| return (id <WKUIDelegatePrivate>)[_webView UIDelegate]; |
| } |
| |
| - (void)setUpDragAndDropInteractions |
| { |
| _dragInteraction = adoptNS([[UIDragInteraction alloc] initWithDelegate:self]); |
| _dropInteraction = adoptNS([[UIDropInteraction alloc] initWithDelegate:self]); |
| [_dragInteraction _setLiftDelay:self.dragLiftDelay]; |
| [_dragInteraction setEnabled:shouldEnableDragInteractionForPolicy(self.webView._dragInteractionPolicy)]; |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) |
| [_dragInteraction _setAllowsPointerDragBeforeLiftDelay:NO]; |
| #endif |
| |
| [self addInteraction:_dragInteraction.get()]; |
| [self addInteraction:_dropInteraction.get()]; |
| } |
| |
| - (void)teardownDragAndDropInteractions |
| { |
| if (_dragInteraction) |
| [self removeInteraction:_dragInteraction.get()]; |
| |
| if (_dropInteraction) |
| [self removeInteraction:_dropInteraction.get()]; |
| |
| _dragInteraction = nil; |
| _dropInteraction = nil; |
| |
| [self cleanUpDragSourceSessionState]; |
| } |
| |
| - (void)_startDrag:(RetainPtr<CGImageRef>)image item:(const WebCore::DragItem&)item |
| { |
| ASSERT(item.sourceAction); |
| |
| if (item.promisedAttachmentInfo) |
| [self _prepareToDragPromisedAttachment:item.promisedAttachmentInfo]; |
| |
| auto dragImage = adoptNS([[UIImage alloc] initWithCGImage:image.get() scale:_page->deviceScaleFactor() orientation:UIImageOrientationUp]); |
| _dragDropInteractionState.stageDragItem(item, dragImage.get()); |
| } |
| |
| - (void)_didHandleAdditionalDragItemsRequest:(BOOL)added |
| { |
| auto completion = _dragDropInteractionState.takeAddDragItemCompletionBlock(); |
| if (!completion) |
| return; |
| |
| auto *registrationLists = [[WebItemProviderPasteboard sharedInstance] takeRegistrationLists]; |
| if (!added || ![registrationLists count] || !_dragDropInteractionState.hasStagedDragSource()) { |
| _dragDropInteractionState.clearStagedDragSource(); |
| completion(@[ ]); |
| return; |
| } |
| |
| auto stagedDragSource = _dragDropInteractionState.stagedDragSource(); |
| NSArray *dragItemsToAdd = [self _itemsForBeginningOrAddingToSessionWithRegistrationLists:registrationLists stagedDragSource:stagedDragSource]; |
| |
| RELEASE_LOG(DragAndDrop, "Drag session: %p adding %tu items", _dragDropInteractionState.dragSession(), dragItemsToAdd.count); |
| _dragDropInteractionState.clearStagedDragSource(dragItemsToAdd.count ? WebKit::DragDropInteractionState::DidBecomeActive::Yes : WebKit::DragDropInteractionState::DidBecomeActive::No); |
| |
| completion(dragItemsToAdd); |
| |
| if (dragItemsToAdd.count) |
| _page->didStartDrag(); |
| } |
| |
| - (void)_didHandleDragStartRequest:(BOOL)started |
| { |
| BlockPtr<void()> savedCompletionBlock = _dragDropInteractionState.takeDragStartCompletionBlock(); |
| ASSERT(savedCompletionBlock); |
| |
| RELEASE_LOG(DragAndDrop, "Handling drag start request (started: %d, completion block: %p)", started, savedCompletionBlock.get()); |
| if (savedCompletionBlock) |
| savedCompletionBlock(); |
| |
| if (!_dragDropInteractionState.dragSession().items.count) { |
| auto positionForDragEnd = WebCore::roundedIntPoint(_dragDropInteractionState.adjustedPositionForDragEnd()); |
| [self cleanUpDragSourceSessionState]; |
| if (started) { |
| // A client of the Objective C SPI or UIKit might have prevented the drag from beginning entirely in the UI process, in which case |
| // we need to balance the `dragstart` event with a `dragend`. |
| _page->dragEnded(positionForDragEnd, positionForDragEnd, { }); |
| } |
| } |
| } |
| |
| - (void)computeClientAndGlobalPointsForDropSession:(id <UIDropSession>)session outClientPoint:(CGPoint *)outClientPoint outGlobalPoint:(CGPoint *)outGlobalPoint |
| { |
| // FIXME: This makes the behavior of drag events on iOS consistent with other synthetic mouse events on iOS (see WebPage::completeSyntheticClick). |
| // However, we should experiment with making the client position relative to the window and the global position in document coordinates. See |
| // https://bugs.webkit.org/show_bug.cgi?id=173855 for more details. |
| auto locationInContentView = [session locationInView:self]; |
| if (outClientPoint) |
| *outClientPoint = locationInContentView; |
| |
| if (outGlobalPoint) |
| *outGlobalPoint = locationInContentView; |
| } |
| |
| static UIDropOperation dropOperationForWebCoreDragOperation(std::optional<WebCore::DragOperation> operation) |
| { |
| if (operation) { |
| if (*operation == WebCore::DragOperation::Move) |
| return UIDropOperationMove; |
| if (*operation == WebCore::DragOperation::Copy) |
| return UIDropOperationCopy; |
| } |
| return UIDropOperationCancel; |
| } |
| |
| static std::optional<WebCore::DragOperation> coreDragOperationForUIDropOperation(UIDropOperation dropOperation) |
| { |
| switch (dropOperation) { |
| case UIDropOperationCancel: |
| return std::nullopt; |
| case UIDropOperationForbidden: |
| return WebCore::DragOperation::Private; |
| case UIDropOperationCopy: |
| return WebCore::DragOperation::Copy; |
| case UIDropOperationMove: |
| return WebCore::DragOperation::Move; |
| } |
| ASSERT_NOT_REACHED(); |
| return std::nullopt; |
| } |
| |
| - (WebCore::DragData)dragDataForDropSession:(id <UIDropSession>)session dragDestinationAction:(WKDragDestinationAction)dragDestinationAction |
| { |
| CGPoint global; |
| CGPoint client; |
| [self computeClientAndGlobalPointsForDropSession:session outClientPoint:&client outGlobalPoint:&global]; |
| |
| auto dragOperationMask = WebCore::anyDragOperation(); |
| if (!session.allowsMoveOperation) |
| dragOperationMask.remove(WebCore::DragOperation::Move); |
| |
| return { |
| session, |
| WebCore::roundedIntPoint(client), |
| WebCore::roundedIntPoint(global), |
| dragOperationMask, |
| { }, |
| WebKit::coreDragDestinationActionMask(dragDestinationAction), |
| _page->webPageID() |
| }; |
| } |
| |
| - (void)cleanUpDragSourceSessionState |
| { |
| if (_waitingForEditDragSnapshot) |
| return; |
| |
| if (_dragDropInteractionState.dragSession() || _dragDropInteractionState.isPerformingDrop()) |
| RELEASE_LOG(DragAndDrop, "Cleaning up dragging state (has pending operation: %d)", [[WebItemProviderPasteboard sharedInstance] hasPendingOperation]); |
| |
| if (![[WebItemProviderPasteboard sharedInstance] hasPendingOperation]) { |
| // If we're performing a drag operation, don't clear out the pasteboard yet, since another web view may still require access to it. |
| // The pasteboard will be cleared after the last client is finished performing a drag operation using the item providers. |
| [[WebItemProviderPasteboard sharedInstance] setItemProviders:nil]; |
| } |
| |
| [[WebItemProviderPasteboard sharedInstance] clearRegistrationLists]; |
| [self _restoreCalloutBarIfNeeded]; |
| |
| [self _removeContainerForDragPreviews]; |
| [std::exchange(_visibleContentViewSnapshot, nil) removeFromSuperview]; |
| [_editDropCaretView remove]; |
| _editDropCaretView = nil; |
| _shouldRestoreCalloutBarAfterDrop = NO; |
| |
| _dragDropInteractionState.dragAndDropSessionsDidEnd(); |
| _dragDropInteractionState = { }; |
| } |
| |
| static NSArray<NSItemProvider *> *extractItemProvidersFromDragItems(NSArray<UIDragItem *> *dragItems) |
| { |
| NSMutableArray<NSItemProvider *> *providers = [NSMutableArray array]; |
| for (UIDragItem *item in dragItems) { |
| if (NSItemProvider *provider = item.itemProvider) |
| [providers addObject:provider]; |
| } |
| return providers; |
| } |
| |
| static NSArray<NSItemProvider *> *extractItemProvidersFromDropSession(id <UIDropSession> session) |
| { |
| return extractItemProvidersFromDragItems(session.items); |
| } |
| |
| - (void)_willReceiveEditDragSnapshot |
| { |
| _waitingForEditDragSnapshot = YES; |
| } |
| |
| - (void)_didReceiveEditDragSnapshot:(std::optional<WebCore::TextIndicatorData>)data |
| { |
| _waitingForEditDragSnapshot = NO; |
| |
| [self _deliverDelayedDropPreviewIfPossible:data]; |
| [self cleanUpDragSourceSessionState]; |
| |
| if (auto action = WTFMove(_actionToPerformAfterReceivingEditDragSnapshot)) |
| action(); |
| } |
| |
| - (void)_deliverDelayedDropPreviewIfPossible:(std::optional<WebCore::TextIndicatorData>)data |
| { |
| if (!_visibleContentViewSnapshot) |
| return; |
| |
| if (!data) |
| return; |
| |
| if (!data->contentImage) |
| return; |
| |
| auto snapshotWithoutSelection = data->contentImageWithoutSelection; |
| if (!snapshotWithoutSelection) |
| return; |
| |
| auto unselectedSnapshotImage = snapshotWithoutSelection->nativeImage(); |
| if (!unselectedSnapshotImage) |
| return; |
| |
| if (!_dropAnimationCount) |
| return; |
| |
| auto unselectedContentImageForEditDrag = adoptNS([[UIImage alloc] initWithCGImage:unselectedSnapshotImage->platformImage().get() scale:_page->deviceScaleFactor() orientation:UIImageOrientationUp]); |
| _unselectedContentSnapshot = adoptNS([[UIImageView alloc] initWithImage:unselectedContentImageForEditDrag.get()]); |
| [_unselectedContentSnapshot setFrame:data->contentImageWithoutSelectionRectInRootViewCoordinates]; |
| |
| [self insertSubview:_unselectedContentSnapshot.get() belowSubview:_visibleContentViewSnapshot.get()]; |
| _dragDropInteractionState.deliverDelayedDropPreview(self, self.containerForDropPreviews, data.value()); |
| } |
| |
| - (void)_didPerformDragOperation:(BOOL)handled |
| { |
| RELEASE_LOG(DragAndDrop, "Finished performing drag controller operation (handled: %d)", handled); |
| [[WebItemProviderPasteboard sharedInstance] decrementPendingOperationCount]; |
| id <UIDropSession> dropSession = _dragDropInteractionState.dropSession(); |
| if ([self.webViewUIDelegate respondsToSelector:@selector(_webView:dataInteractionOperationWasHandled:forSession:itemProviders:)]) |
| [self.webViewUIDelegate _webView:self.webView dataInteractionOperationWasHandled:handled forSession:dropSession itemProviders:[WebItemProviderPasteboard sharedInstance].itemProviders]; |
| |
| CGPoint global; |
| CGPoint client; |
| [self computeClientAndGlobalPointsForDropSession:dropSession outClientPoint:&client outGlobalPoint:&global]; |
| [self cleanUpDragSourceSessionState]; |
| auto currentDragOperation = _page->currentDragOperation(); |
| _page->dragEnded(WebCore::roundedIntPoint(client), WebCore::roundedIntPoint(global), currentDragOperation ? *currentDragOperation : OptionSet<WebCore::DragOperation>({ })); |
| } |
| |
| - (void)_didChangeDragCaretRect:(CGRect)previousRect currentRect:(CGRect)rect |
| { |
| BOOL previousRectIsEmpty = CGRectIsEmpty(previousRect); |
| BOOL currentRectIsEmpty = CGRectIsEmpty(rect); |
| if (previousRectIsEmpty && currentRectIsEmpty) |
| return; |
| |
| if (previousRectIsEmpty) { |
| _editDropCaretView = adoptNS([[_UITextDragCaretView alloc] initWithTextInputView:self]); |
| [_editDropCaretView insertAtPosition:[WKTextPosition textPositionWithRect:rect]]; |
| return; |
| } |
| |
| if (currentRectIsEmpty) { |
| [_editDropCaretView remove]; |
| _editDropCaretView = nil; |
| return; |
| } |
| |
| [_editDropCaretView updateToPosition:[WKTextPosition textPositionWithRect:rect]]; |
| } |
| |
| - (void)_prepareToDragPromisedAttachment:(const WebCore::PromisedAttachmentInfo&)info |
| { |
| auto session = retainPtr(_dragDropInteractionState.dragSession()); |
| if (!session) { |
| ASSERT_NOT_REACHED(); |
| return; |
| } |
| |
| auto numberOfAdditionalTypes = info.additionalTypes.size(); |
| ASSERT(numberOfAdditionalTypes == info.additionalData.size()); |
| |
| RELEASE_LOG(DragAndDrop, "Drag session: %p preparing to drag with attachment identifier: %s", session.get(), info.attachmentIdentifier.utf8().data()); |
| |
| NSString *utiType = nil; |
| NSString *fileName = nil; |
| if (auto attachment = _page->attachmentForIdentifier(info.attachmentIdentifier)) { |
| utiType = attachment->utiType(); |
| fileName = attachment->fileName(); |
| } |
| |
| auto registrationList = adoptNS([[WebItemProviderRegistrationInfoList alloc] init]); |
| [registrationList setPreferredPresentationStyle:WebPreferredPresentationStyleAttachment]; |
| if ([fileName length]) |
| [registrationList setSuggestedName:fileName]; |
| if (numberOfAdditionalTypes == info.additionalData.size() && numberOfAdditionalTypes) { |
| for (size_t index = 0; index < numberOfAdditionalTypes; ++index) { |
| auto nsData = info.additionalData[index]->createNSData(); |
| [registrationList addData:nsData.get() forType:info.additionalTypes[index]]; |
| } |
| } |
| |
| [registrationList addPromisedType:utiType fileCallback:[session = WTFMove(session), weakSelf = WeakObjCPtr<WKContentView>(self), info] (WebItemProviderFileCallback callback) { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) { |
| callback(nil, [NSError errorWithDomain:WKErrorDomain code:WKErrorWebViewInvalidated userInfo:nil]); |
| return; |
| } |
| |
| NSString *temporaryBlobDirectory = FileSystem::createTemporaryDirectory(@"blobs"); |
| NSURL *destinationURL = [NSURL fileURLWithPath:[temporaryBlobDirectory stringByAppendingPathComponent:[NSUUID UUID].UUIDString] isDirectory:NO]; |
| |
| auto attachment = strongSelf->_page->attachmentForIdentifier(info.attachmentIdentifier); |
| if (attachment && attachment->fileWrapper()) { |
| RELEASE_LOG(DragAndDrop, "Drag session: %p delivering promised attachment: %s at path: %@", session.get(), info.attachmentIdentifier.utf8().data(), destinationURL.path); |
| NSError *fileWrapperError = nil; |
| if ([attachment->fileWrapper() writeToURL:destinationURL options:0 originalContentsURL:nil error:&fileWrapperError]) |
| callback(destinationURL, nil); |
| else |
| callback(nil, fileWrapperError); |
| } else |
| callback(nil, [NSError errorWithDomain:WKErrorDomain code:WKErrorWebViewInvalidated userInfo:nil]); |
| |
| [ensureLocalDragSessionContext(session.get()) addTemporaryDirectory:temporaryBlobDirectory]; |
| }]; |
| |
| WebItemProviderPasteboard *pasteboard = [WebItemProviderPasteboard sharedInstance]; |
| pasteboard.itemProviders = @[ [registrationList itemProvider] ]; |
| [pasteboard stageRegistrationLists:@[ registrationList.get() ]]; |
| } |
| |
| - (WKDragDestinationAction)_dragDestinationActionForDropSession:(id <UIDropSession>)session |
| { |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| if ([uiDelegate respondsToSelector:@selector(_webView:dragDestinationActionMaskForDraggingInfo:)]) |
| return [uiDelegate _webView:self.webView dragDestinationActionMaskForDraggingInfo:session]; |
| |
| return WKDragDestinationActionAny & ~WKDragDestinationActionLoad; |
| } |
| |
| - (OptionSet<WebCore::DragSourceAction>)_allowedDragSourceActions |
| { |
| auto allowedActions = WebCore::anyDragSourceAction(); |
| if (!self.isFirstResponder || !_suppressSelectionAssistantReasons.isEmpty()) { |
| // Don't allow starting a drag on a selection when selection views are not visible. |
| allowedActions.remove(WebCore::DragSourceAction::Selection); |
| } |
| return allowedActions; |
| } |
| |
| - (id <UIDragDropSession>)currentDragOrDropSession |
| { |
| if (_dragDropInteractionState.dropSession()) |
| return _dragDropInteractionState.dropSession(); |
| return _dragDropInteractionState.dragSession(); |
| } |
| |
| - (void)_restoreCalloutBarIfNeeded |
| { |
| if (!_shouldRestoreCalloutBarAfterDrop) |
| return; |
| |
| // FIXME: This SPI should be renamed in UIKit to reflect a more general purpose of revealing hidden interaction assistant controls. |
| [_textInteractionAssistant didEndScrollingOverflow]; |
| _shouldRestoreCalloutBarAfterDrop = NO; |
| } |
| |
| - (NSArray<UIDragItem *> *)_itemsForBeginningOrAddingToSessionWithRegistrationLists:(NSArray<WebItemProviderRegistrationInfoList *> *)registrationLists stagedDragSource:(const WebKit::DragSourceState&)stagedDragSource |
| { |
| if (!registrationLists.count) |
| return @[ ]; |
| |
| NSMutableArray *adjustedItemProviders = [NSMutableArray array]; |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| if ([uiDelegate respondsToSelector:@selector(_webView:adjustedDataInteractionItemProvidersForItemProvider:representingObjects:additionalData:)]) { |
| // FIXME: We should consider a new UI delegate hook that accepts a list of item providers, so we don't need to invoke this delegate method repeatedly for multiple items. |
| for (WebItemProviderRegistrationInfoList *list in registrationLists) { |
| NSItemProvider *defaultItemProvider = list.itemProvider; |
| if (!defaultItemProvider) |
| continue; |
| |
| auto representingObjects = adoptNS([[NSMutableArray alloc] init]); |
| auto additionalData = adoptNS([[NSMutableDictionary alloc] init]); |
| [list enumerateItems:[representingObjects, additionalData] (id <WebItemProviderRegistrar> item, NSUInteger) { |
| if ([item respondsToSelector:@selector(representingObjectForClient)]) |
| [representingObjects addObject:item.representingObjectForClient]; |
| if ([item respondsToSelector:@selector(typeIdentifierForClient)] && [item respondsToSelector:@selector(dataForClient)]) |
| [additionalData setObject:item.dataForClient forKey:item.typeIdentifierForClient]; |
| }]; |
| NSArray *adjustedItems = [uiDelegate _webView:self.webView adjustedDataInteractionItemProvidersForItemProvider:defaultItemProvider representingObjects:representingObjects.get() additionalData:additionalData.get()]; |
| if (adjustedItems.count) |
| [adjustedItemProviders addObjectsFromArray:adjustedItems]; |
| } |
| } else { |
| for (WebItemProviderRegistrationInfoList *list in registrationLists) { |
| if (auto *defaultItemProvider = list.itemProvider) |
| [adjustedItemProviders addObject:defaultItemProvider]; |
| } |
| } |
| |
| NSMutableArray *dragItems = [NSMutableArray arrayWithCapacity:adjustedItemProviders.count]; |
| for (NSItemProvider *itemProvider in adjustedItemProviders) { |
| auto item = adoptNS([[UIDragItem alloc] initWithItemProvider:itemProvider]); |
| [item _setPrivateLocalContext:@(stagedDragSource.itemIdentifier)]; |
| [dragItems addObject:item.get()]; |
| } |
| |
| return dragItems; |
| } |
| |
| - (void)cancelActiveTextInteractionGestures |
| { |
| [[_textInteractionAssistant loupeGesture] _wk_cancel]; |
| [[_textInteractionAssistant forcePressGesture] _wk_cancel]; |
| } |
| |
| - (UIView *)textEffectsWindow |
| { |
| return [UITextEffectsWindow sharedTextEffectsWindowForWindowScene:self.window.windowScene]; |
| } |
| |
| - (NSDictionary *)_autofillContext |
| { |
| if (!self._hasFocusedElement) |
| return nil; |
| |
| auto context = adoptNS([[NSMutableDictionary alloc] init]); |
| context.get()[@"_WKAutofillContextVersion"] = @(2); |
| |
| if (_focusRequiresStrongPasswordAssistance && _focusedElementInformation.elementType == WebKit::InputType::Password) { |
| context.get()[@"_automaticPasswordKeyboard"] = @YES; |
| context.get()[@"strongPasswordAdditionalContext"] = _additionalContextForStrongPasswordAssistance.get(); |
| } else if (_focusedElementInformation.acceptsAutofilledLoginCredentials) |
| context.get()[@"_acceptsLoginCredentials"] = @YES; |
| |
| NSURL *platformURL = _focusedElementInformation.representingPageURL; |
| if (platformURL) |
| context.get()[@"_WebViewURL"] = platformURL; |
| |
| return context.autorelease(); |
| } |
| |
| - (BOOL)supportsImagePaste |
| { |
| return mayContainSelectableText(_focusedElementInformation.elementType); |
| } |
| |
| #if USE(UICONTEXTMENU) |
| static RetainPtr<UIImage> uiImageForImage(WebCore::Image* image) |
| { |
| if (!image) |
| return nil; |
| |
| auto nativeImage = image->nativeImage(); |
| if (!nativeImage) |
| return nil; |
| |
| return adoptNS([[UIImage alloc] initWithCGImage:nativeImage->platformImage().get()]); |
| } |
| |
| // FIXME: This should be merged with createTargetedDragPreview in DragDropInteractionState. |
| static RetainPtr<UITargetedPreview> createTargetedPreview(UIImage *image, UIView *rootView, UIView *previewContainer, const WebCore::FloatRect& frameInRootViewCoordinates, const Vector<WebCore::FloatRect>& clippingRectsInFrameCoordinates, UIColor *backgroundColor) |
| { |
| if (frameInRootViewCoordinates.isEmpty() || !image || !previewContainer.window) |
| return nil; |
| |
| WebCore::FloatRect frameInContainerCoordinates = [rootView convertRect:frameInRootViewCoordinates toView:previewContainer]; |
| if (frameInContainerCoordinates.isEmpty()) |
| return nil; |
| |
| auto scalingRatio = frameInContainerCoordinates.size() / frameInRootViewCoordinates.size(); |
| auto clippingRectValuesInFrameCoordinates = createNSArray(clippingRectsInFrameCoordinates, [&] (WebCore::FloatRect rect) { |
| rect.scale(scalingRatio); |
| return [NSValue valueWithCGRect:rect]; |
| }); |
| |
| RetainPtr<UIPreviewParameters> parameters; |
| if ([clippingRectValuesInFrameCoordinates count]) |
| parameters = adoptNS([[UIPreviewParameters alloc] initWithTextLineRects:clippingRectValuesInFrameCoordinates.get()]); |
| else |
| parameters = adoptNS([[UIPreviewParameters alloc] init]); |
| |
| [parameters setBackgroundColor:(backgroundColor ?: [UIColor clearColor])]; |
| |
| CGPoint centerInContainerCoordinates = { CGRectGetMidX(frameInContainerCoordinates), CGRectGetMidY(frameInContainerCoordinates) }; |
| auto target = adoptNS([[UIPreviewTarget alloc] initWithContainer:previewContainer center:centerInContainerCoordinates]); |
| |
| auto imageView = adoptNS([[UIImageView alloc] initWithImage:image]); |
| [imageView setFrame:frameInContainerCoordinates]; |
| return adoptNS([[UITargetedPreview alloc] initWithView:imageView.get() parameters:parameters.get() target:target.get()]); |
| } |
| |
| static RetainPtr<UITargetedPreview> createFallbackTargetedPreview(UIView *rootView, UIView *containerView, const WebCore::FloatRect& frameInRootViewCoordinates, UIColor *backgroundColor) |
| { |
| if (!containerView.window) |
| return nil; |
| |
| if (frameInRootViewCoordinates.isEmpty()) |
| return nil; |
| |
| auto parameters = adoptNS([[UIPreviewParameters alloc] init]); |
| if (backgroundColor) |
| [parameters setBackgroundColor:backgroundColor]; |
| |
| RetainPtr snapshotView = [rootView resizableSnapshotViewFromRect:frameInRootViewCoordinates afterScreenUpdates:NO withCapInsets:UIEdgeInsetsZero]; |
| if (!snapshotView) |
| snapshotView = adoptNS([UIView new]); |
| |
| CGRect frameInContainerViewCoordinates = [rootView convertRect:frameInRootViewCoordinates toView:containerView]; |
| |
| if (CGRectIsEmpty(frameInContainerViewCoordinates)) |
| return nil; |
| |
| [snapshotView setFrame:frameInContainerViewCoordinates]; |
| |
| CGPoint centerInContainerViewCoordinates = CGPointMake(CGRectGetMidX(frameInContainerViewCoordinates), CGRectGetMidY(frameInContainerViewCoordinates)); |
| auto target = adoptNS([[UIPreviewTarget alloc] initWithContainer:containerView center:centerInContainerViewCoordinates]); |
| |
| return adoptNS([[UITargetedPreview alloc] initWithView:snapshotView.get() parameters:parameters.get() target:target.get()]); |
| } |
| |
| - (UITargetedPreview *)_createTargetedContextMenuHintPreviewForFocusedElement |
| { |
| auto backgroundColor = [&]() -> UIColor * { |
| switch (_focusedElementInformation.elementType) { |
| case WebKit::InputType::Date: |
| case WebKit::InputType::Month: |
| case WebKit::InputType::DateTimeLocal: |
| case WebKit::InputType::Time: |
| return UIColor.clearColor; |
| default: |
| return nil; |
| } |
| }(); |
| |
| auto targetedPreview = createFallbackTargetedPreview(self, self.containerForContextMenuHintPreviews, _focusedElementInformation.interactionRect, backgroundColor); |
| |
| [self _updateTargetedPreviewScrollViewUsingContainerScrollingNodeID:_focusedElementInformation.containerScrollingNodeID]; |
| |
| _contextMenuInteractionTargetedPreview = WTFMove(targetedPreview); |
| return _contextMenuInteractionTargetedPreview.get(); |
| } |
| |
| - (BOOL)positionInformationHasImageOverlayDataDetector |
| { |
| return _positionInformation.isImageOverlayText && [_positionInformation.dataDetectorResults count]; |
| } |
| |
| - (UITargetedPreview *)_createTargetedContextMenuHintPreviewIfPossible |
| { |
| RetainPtr<UITargetedPreview> targetedPreview; |
| |
| if (_positionInformation.isLink && _positionInformation.linkIndicator.contentImage) { |
| auto indicator = _positionInformation.linkIndicator; |
| auto textIndicatorImage = uiImageForImage(indicator.contentImage.get()); |
| targetedPreview = createTargetedPreview(textIndicatorImage.get(), self, self.containerForContextMenuHintPreviews, indicator.textBoundingRectInRootViewCoordinates, indicator.textRectsInBoundingRectCoordinates, cocoaColor(indicator.estimatedBackgroundColor).get()); |
| } else if ((_positionInformation.isAttachment || _positionInformation.isImage) && _positionInformation.image) { |
| auto cgImage = _positionInformation.image->makeCGImageCopy(); |
| auto image = adoptNS([[UIImage alloc] initWithCGImage:cgImage.get()]); |
| targetedPreview = createTargetedPreview(image.get(), self, self.containerForContextMenuHintPreviews, _positionInformation.bounds, { }, nil); |
| } |
| |
| if (!targetedPreview) { |
| auto boundsForFallbackPreview = self.positionInformationHasImageOverlayDataDetector ? _positionInformation.dataDetectorBounds : _positionInformation.bounds; |
| targetedPreview = createFallbackTargetedPreview(self, self.containerForContextMenuHintPreviews, boundsForFallbackPreview, nil); |
| } |
| |
| [self _updateTargetedPreviewScrollViewUsingContainerScrollingNodeID:_positionInformation.containerScrollingNodeID]; |
| |
| _contextMenuInteractionTargetedPreview = WTFMove(targetedPreview); |
| return _contextMenuInteractionTargetedPreview.get(); |
| } |
| |
| - (void)_removeContextMenuHintContainerIfPossible |
| { |
| #if HAVE(LINK_PREVIEW) |
| // If a new _contextMenuElementInfo is installed, we've started another interaction, |
| // and removing the hint container view will cause the animation to break. |
| if (_contextMenuElementInfo) |
| return; |
| #endif |
| if (_isDisplayingContextMenuWithAnimation) |
| return; |
| #if ENABLE(DATA_DETECTION) |
| // We are also using this container for the action sheet assistant... |
| if ([_actionSheetAssistant hasContextMenuInteraction]) |
| return; |
| #endif |
| // and for the file upload panel... |
| if (_fileUploadPanel) |
| return; |
| |
| // and for the date/time picker. |
| if ([self dateTimeInputControl]) |
| return; |
| |
| if ([self selectControl]) |
| return; |
| |
| if ([_contextMenuHintContainerView subviews].count) |
| return; |
| |
| [self _removeContainerForContextMenuHintPreviews]; |
| } |
| |
| - (void)presentContextMenu:(UIContextMenuInteraction *)contextMenuInteraction atLocation:(CGPoint) location |
| { |
| if (!self.window) |
| return; |
| |
| [contextMenuInteraction _presentMenuAtLocation:location]; |
| } |
| |
| #endif // USE(UICONTEXTMENU) |
| |
| #if HAVE(UI_WK_DOCUMENT_CONTEXT) |
| |
| static inline OptionSet<WebKit::DocumentEditingContextRequest::Options> toWebDocumentRequestOptions(UIWKDocumentRequestFlags flags) |
| { |
| OptionSet<WebKit::DocumentEditingContextRequest::Options> options; |
| |
| if (flags & UIWKDocumentRequestText) |
| options.add(WebKit::DocumentEditingContextRequest::Options::Text); |
| if (flags & UIWKDocumentRequestAttributed) |
| options.add(WebKit::DocumentEditingContextRequest::Options::AttributedText); |
| if (flags & UIWKDocumentRequestRects) |
| options.add(WebKit::DocumentEditingContextRequest::Options::Rects); |
| if (flags & UIWKDocumentRequestSpatial) |
| options.add(WebKit::DocumentEditingContextRequest::Options::Spatial); |
| if (flags & UIWKDocumentRequestAnnotation) |
| options.add(WebKit::DocumentEditingContextRequest::Options::Annotation); |
| if (flags & UIWKDocumentRequestMarkedTextRects) |
| options.add(WebKit::DocumentEditingContextRequest::Options::MarkedTextRects); |
| if (flags & UIWKDocumentRequestSpatialAndCurrentSelection) |
| options.add(WebKit::DocumentEditingContextRequest::Options::SpatialAndCurrentSelection); |
| |
| return options; |
| } |
| |
| static WebKit::DocumentEditingContextRequest toWebRequest(UIWKDocumentRequest *request) |
| { |
| WebKit::DocumentEditingContextRequest webRequest = { |
| .options = toWebDocumentRequestOptions(request.flags), |
| .surroundingGranularity = toWKTextGranularity(request.surroundingGranularity), |
| .granularityCount = request.granularityCount, |
| .rect = request.documentRect |
| }; |
| |
| if (auto textInputContext = dynamic_objc_cast<_WKTextInputContext>(request.inputElementIdentifier)) |
| webRequest.textInputContext = [textInputContext _textInputContext]; |
| |
| return webRequest; |
| } |
| |
| - (void)adjustSelectionWithDelta:(NSRange)deltaRange completionHandler:(void (^)(void))completionHandler |
| { |
| // UIKit is putting casted signed integers into NSRange. Cast them back to reveal any negative values. |
| _page->updateSelectionWithDelta(static_cast<int64_t>(deltaRange.location), static_cast<int64_t>(deltaRange.length), [capturedCompletionHandler = makeBlockPtr(completionHandler)] { |
| capturedCompletionHandler(); |
| }); |
| } |
| |
| - (void)requestDocumentContext:(UIWKDocumentRequest *)request completionHandler:(void (^)(UIWKDocumentContext *))completionHandler |
| { |
| auto webRequest = toWebRequest(request); |
| OptionSet<WebKit::DocumentEditingContextRequest::Options> options = webRequest.options; |
| _page->requestDocumentEditingContext(webRequest, [capturedCompletionHandler = makeBlockPtr(completionHandler), options] (WebKit::DocumentEditingContext editingContext) { |
| capturedCompletionHandler(editingContext.toPlatformContext(options)); |
| }); |
| } |
| |
| - (void)selectPositionAtPoint:(CGPoint)point withContextRequest:(UIWKDocumentRequest *)request completionHandler:(void (^)(UIWKDocumentContext *))completionHandler |
| { |
| // FIXME: Reduce to 1 message. |
| [self selectPositionAtPoint:point completionHandler:^{ |
| [self requestDocumentContext:request completionHandler:^(UIWKDocumentContext *context) { |
| completionHandler(context); |
| }]; |
| }]; |
| } |
| |
| #endif |
| |
| - (void)insertTextPlaceholderWithSize:(CGSize)size completionHandler:(void (^)(UITextPlaceholder *))completionHandler |
| { |
| _page->insertTextPlaceholder(WebCore::IntSize { size }, [weakSelf = WeakObjCPtr<WKContentView>(self), completionHandler = makeBlockPtr(completionHandler)](const std::optional<WebCore::ElementContext>& placeholder) { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf || ![strongSelf webView] || !placeholder) { |
| completionHandler(nil); |
| return; |
| } |
| WebCore::ElementContext placeholderToUse { *placeholder }; |
| placeholderToUse.boundingRect = [strongSelf convertRect:placeholderToUse.boundingRect fromView:[strongSelf webView]]; |
| completionHandler(adoptNS([[WKTextPlaceholder alloc] initWithElementContext:placeholderToUse]).get()); |
| }); |
| } |
| |
| - (void)removeTextPlaceholder:(UITextPlaceholder *)placeholder willInsertText:(BOOL)willInsertText completionHandler:(void (^)(void))completionHandler |
| { |
| // FIXME: Implement support for willInsertText. See <https://bugs.webkit.org/show_bug.cgi?id=208747>. |
| if (auto* wkTextPlaceholder = dynamic_objc_cast<WKTextPlaceholder>(placeholder)) |
| _page->removeTextPlaceholder(wkTextPlaceholder.elementContext, makeBlockPtr(completionHandler)); |
| else |
| completionHandler(); |
| } |
| |
| static Vector<WebCore::IntSize> sizesOfPlaceholderElementsToInsertWhenDroppingItems(NSArray<NSItemProvider *> *itemProviders) |
| { |
| Vector<WebCore::IntSize> sizes; |
| for (NSItemProvider *item in itemProviders) { |
| if (!WebCore::MIMETypeRegistry::isSupportedImageMIMEType(WebCore::MIMETypeFromUTI(item.web_fileUploadContentTypes.firstObject))) |
| return { }; |
| |
| WebCore::IntSize presentationSize(item.preferredPresentationSize); |
| if (presentationSize.isEmpty()) |
| return { }; |
| |
| sizes.append(WTFMove(presentationSize)); |
| } |
| return sizes; |
| } |
| |
| - (BOOL)_handleDropByInsertingImagePlaceholders:(NSArray<NSItemProvider *> *)itemProviders session:(id <UIDropSession>)session |
| { |
| if (!self.webView._editable) |
| return NO; |
| |
| if (_dragDropInteractionState.dragSession()) |
| return NO; |
| |
| if (session.items.count != itemProviders.count) |
| return NO; |
| |
| auto imagePlaceholderSizes = sizesOfPlaceholderElementsToInsertWhenDroppingItems(itemProviders); |
| if (imagePlaceholderSizes.isEmpty()) |
| return NO; |
| |
| RELEASE_LOG(DragAndDrop, "Inserting dropped image placeholders for session: %p", session); |
| |
| _page->insertDroppedImagePlaceholders(imagePlaceholderSizes, [protectedSelf = retainPtr(self), dragItems = retainPtr(session.items)] (auto& placeholderRects, auto data) { |
| auto& state = protectedSelf->_dragDropInteractionState; |
| if (!data || !protectedSelf->_dropAnimationCount) { |
| RELEASE_LOG(DragAndDrop, "Failed to animate image placeholders: missing text indicator data."); |
| state.clearAllDelayedItemPreviewProviders(); |
| return; |
| } |
| |
| auto snapshotWithoutSelection = data->contentImageWithoutSelection; |
| if (!snapshotWithoutSelection) { |
| RELEASE_LOG(DragAndDrop, "Failed to animate image placeholders: missing unselected content image."); |
| state.clearAllDelayedItemPreviewProviders(); |
| return; |
| } |
| |
| auto unselectedSnapshotImage = snapshotWithoutSelection->nativeImage(); |
| if (!unselectedSnapshotImage) { |
| RELEASE_LOG(DragAndDrop, "Failed to animate image placeholders: could not decode unselected content image."); |
| state.clearAllDelayedItemPreviewProviders(); |
| return; |
| } |
| |
| auto unselectedContentImageForEditDrag = adoptNS([[UIImage alloc] initWithCGImage:unselectedSnapshotImage->platformImage().get() scale:protectedSelf->_page->deviceScaleFactor() orientation:UIImageOrientationUp]); |
| auto snapshotView = adoptNS([[UIImageView alloc] initWithImage:unselectedContentImageForEditDrag.get()]); |
| [snapshotView setFrame:data->contentImageWithoutSelectionRectInRootViewCoordinates]; |
| [protectedSelf addSubview:snapshotView.get()]; |
| protectedSelf->_unselectedContentSnapshot = WTFMove(snapshotView); |
| state.deliverDelayedDropPreview(protectedSelf.get(), [protectedSelf unobscuredContentRect], dragItems.get(), placeholderRects); |
| }); |
| |
| return YES; |
| } |
| |
| #pragma mark - UIDragInteractionDelegate |
| |
| - (BOOL)_dragInteraction:(UIDragInteraction *)interaction shouldDelayCompetingGestureRecognizer:(UIGestureRecognizer *)competingGestureRecognizer |
| { |
| if (_highlightLongPressGestureRecognizer == competingGestureRecognizer) { |
| // Since 3D touch still recognizes alongside the drag lift, and also requires the highlight long press |
| // gesture to be active to support cancelling when `touchstart` is prevented, we should also allow the |
| // highlight long press to recognize simultaneously, and manually cancel it when the drag lift is |
| // recognized (see _dragInteraction:prepareForSession:completion:). |
| return NO; |
| } |
| return [competingGestureRecognizer isKindOfClass:[UILongPressGestureRecognizer class]]; |
| } |
| |
| - (NSInteger)_dragInteraction:(UIDragInteraction *)interaction dataOwnerForSession:(id <UIDragSession>)session |
| { |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| NSInteger dataOwner = 0; |
| if ([uiDelegate respondsToSelector:@selector(_webView:dataOwnerForDragSession:)]) |
| dataOwner = [uiDelegate _webView:self.webView dataOwnerForDragSession:session]; |
| return dataOwner; |
| } |
| |
| - (void)_dragInteraction:(UIDragInteraction *)interaction itemsForAddingToSession:(id <UIDragSession>)session withTouchAtPoint:(CGPoint)point completion:(void(^)(NSArray<UIDragItem *> *))completion |
| { |
| if (!_dragDropInteractionState.shouldRequestAdditionalItemForDragSession(session)) { |
| completion(@[ ]); |
| return; |
| } |
| |
| _dragDropInteractionState.dragSessionWillRequestAdditionalItem(completion); |
| _page->requestAdditionalItemsForDragSession(WebCore::roundedIntPoint(point), WebCore::roundedIntPoint(point), self._allowedDragSourceActions); |
| } |
| |
| - (void)_dragInteraction:(UIDragInteraction *)interaction prepareForSession:(id <UIDragSession>)session completion:(dispatch_block_t)completion |
| { |
| RELEASE_LOG(DragAndDrop, "Preparing for drag session: %p", session); |
| if (self.currentDragOrDropSession) { |
| // FIXME: Support multiple simultaneous drag sessions in the future. |
| RELEASE_LOG(DragAndDrop, "Drag session failed: %p (a current drag session already exists)", session); |
| completion(); |
| return; |
| } |
| |
| [self cleanUpDragSourceSessionState]; |
| |
| auto prepareForSession = [weakSelf = WeakObjCPtr<WKContentView>(self), session = retainPtr(session), completion = makeBlockPtr(completion)] (WebKit::ProceedWithTextSelectionInImage proceedWithTextSelectionInImage) { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf || proceedWithTextSelectionInImage == WebKit::ProceedWithTextSelectionInImage::Yes) |
| return; |
| |
| auto dragOrigin = [session locationInView:strongSelf.get()]; |
| strongSelf->_dragDropInteractionState.prepareForDragSession(session.get(), completion.get()); |
| strongSelf->_page->requestDragStart(WebCore::roundedIntPoint(dragOrigin), WebCore::roundedIntPoint([strongSelf convertPoint:dragOrigin toView:[strongSelf window]]), [strongSelf _allowedDragSourceActions]); |
| RELEASE_LOG(DragAndDrop, "Drag session requested: %p at origin: {%.0f, %.0f}", session.get(), dragOrigin.x, dragOrigin.y); |
| }; |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| [self _doAfterPendingImageAnalysis:prepareForSession]; |
| #else |
| prepareForSession(WebKit::ProceedWithTextSelectionInImage::No); |
| #endif |
| } |
| |
| - (NSArray<UIDragItem *> *)dragInteraction:(UIDragInteraction *)interaction itemsForBeginningSession:(id <UIDragSession>)session |
| { |
| ASSERT(interaction == _dragInteraction); |
| RELEASE_LOG(DragAndDrop, "Drag items requested for session: %p", session); |
| if (_dragDropInteractionState.dragSession() != session) { |
| RELEASE_LOG(DragAndDrop, "Drag session failed: %p (delegate session does not match %p)", session, _dragDropInteractionState.dragSession()); |
| return @[ ]; |
| } |
| |
| if (!_dragDropInteractionState.hasStagedDragSource()) { |
| RELEASE_LOG(DragAndDrop, "Drag session failed: %p (missing staged drag source)", session); |
| return @[ ]; |
| } |
| |
| auto stagedDragSource = _dragDropInteractionState.stagedDragSource(); |
| auto *registrationLists = [[WebItemProviderPasteboard sharedInstance] takeRegistrationLists]; |
| NSArray *dragItems = [self _itemsForBeginningOrAddingToSessionWithRegistrationLists:registrationLists stagedDragSource:stagedDragSource]; |
| if (![dragItems count]) |
| _page->dragCancelled(); |
| else |
| [self _cancelLongPressGestureRecognizer]; |
| |
| RELEASE_LOG(DragAndDrop, "Drag session: %p starting with %tu items", session, [dragItems count]); |
| _dragDropInteractionState.clearStagedDragSource([dragItems count] ? WebKit::DragDropInteractionState::DidBecomeActive::Yes : WebKit::DragDropInteractionState::DidBecomeActive::No); |
| |
| return dragItems; |
| } |
| |
| - (UITargetedDragPreview *)dragInteraction:(UIDragInteraction *)interaction previewForLiftingItem:(UIDragItem *)item session:(id <UIDragSession>)session |
| { |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| if ([uiDelegate respondsToSelector:@selector(_webView:previewForLiftingItem:session:)]) { |
| UITargetedDragPreview *overriddenPreview = [uiDelegate _webView:self.webView previewForLiftingItem:item session:session]; |
| if (overriddenPreview) |
| return overriddenPreview; |
| } |
| return _dragDropInteractionState.previewForDragItem(item, self, self.containerForDragPreviews); |
| } |
| |
| - (void)dragInteraction:(UIDragInteraction *)interaction willAnimateLiftWithAnimator:(id <UIDragAnimating>)animator session:(id <UIDragSession>)session |
| { |
| RELEASE_LOG(DragAndDrop, "Drag session willAnimateLiftWithAnimator: %p", session); |
| if (_dragDropInteractionState.anyActiveDragSourceIs(WebCore::DragSourceAction::Selection)) { |
| [self cancelActiveTextInteractionGestures]; |
| if (!_shouldRestoreCalloutBarAfterDrop) { |
| // FIXME: This SPI should be renamed in UIKit to reflect a more general purpose of hiding interaction assistant controls. |
| [_textInteractionAssistant willStartScrollingOverflow]; |
| _shouldRestoreCalloutBarAfterDrop = YES; |
| } |
| } |
| |
| auto positionForDragEnd = WebCore::roundedIntPoint(_dragDropInteractionState.adjustedPositionForDragEnd()); |
| RetainPtr<WKContentView> protectedSelf(self); |
| [animator addCompletion:[session, positionForDragEnd, protectedSelf, page = _page] (UIViewAnimatingPosition finalPosition) { |
| #if RELEASE_LOG_DISABLED |
| UNUSED_PARAM(session); |
| #endif |
| if (finalPosition == UIViewAnimatingPositionStart) { |
| RELEASE_LOG(DragAndDrop, "Drag session ended at start: %p", session); |
| // The lift was canceled, so -dropInteraction:sessionDidEnd: will never be invoked. This is the last chance to clean up. |
| [protectedSelf cleanUpDragSourceSessionState]; |
| page->dragEnded(positionForDragEnd, positionForDragEnd, { }); |
| } |
| #if !RELEASE_LOG_DISABLED |
| else |
| RELEASE_LOG(DragAndDrop, "Drag session did not end at start: %p", session); |
| #endif |
| }]; |
| } |
| |
| - (void)dragInteraction:(UIDragInteraction *)interaction sessionWillBegin:(id <UIDragSession>)session |
| { |
| RELEASE_LOG(DragAndDrop, "Drag session beginning: %p", session); |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| if ([uiDelegate respondsToSelector:@selector(_webView:dataInteraction:sessionWillBegin:)]) |
| [uiDelegate _webView:self.webView dataInteraction:interaction sessionWillBegin:session]; |
| |
| [_actionSheetAssistant cleanupSheet]; |
| _dragDropInteractionState.dragSessionWillBegin(); |
| _page->didStartDrag(); |
| } |
| |
| - (void)dragInteraction:(UIDragInteraction *)interaction session:(id <UIDragSession>)session didEndWithOperation:(UIDropOperation)operation |
| { |
| RELEASE_LOG(DragAndDrop, "Drag session ended: %p (with operation: %tu, performing operation: %d, began dragging: %d)", session, operation, _dragDropInteractionState.isPerformingDrop(), _dragDropInteractionState.didBeginDragging()); |
| |
| [self _restoreCalloutBarIfNeeded]; |
| |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| if ([uiDelegate respondsToSelector:@selector(_webView:dataInteraction:session:didEndWithOperation:)]) |
| [uiDelegate _webView:self.webView dataInteraction:interaction session:session didEndWithOperation:operation]; |
| |
| if (_dragDropInteractionState.isPerformingDrop()) |
| return; |
| |
| [self cleanUpDragSourceSessionState]; |
| _page->dragEnded(WebCore::roundedIntPoint(_dragDropInteractionState.adjustedPositionForDragEnd()), WebCore::roundedIntPoint(_dragDropInteractionState.adjustedPositionForDragEnd()), coreDragOperationForUIDropOperation(operation)); |
| } |
| |
| - (UITargetedDragPreview *)dragInteraction:(UIDragInteraction *)interaction previewForCancellingItem:(UIDragItem *)item withDefault:(UITargetedDragPreview *)defaultPreview |
| { |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| if ([uiDelegate respondsToSelector:@selector(_webView:previewForCancellingItem:withDefault:)]) { |
| UITargetedDragPreview *overriddenPreview = [uiDelegate _webView:self.webView previewForCancellingItem:item withDefault:defaultPreview]; |
| if (overriddenPreview) |
| return overriddenPreview; |
| } |
| return _dragDropInteractionState.previewForDragItem(item, self, self.unscaledView); |
| } |
| |
| - (BOOL)_dragInteraction:(UIDragInteraction *)interaction item:(UIDragItem *)item shouldDelaySetDownAnimationWithCompletion:(void(^)(void))completion |
| { |
| _dragDropInteractionState.dragSessionWillDelaySetDownAnimation(completion); |
| return YES; |
| } |
| |
| - (void)dragInteraction:(UIDragInteraction *)interaction item:(UIDragItem *)item willAnimateCancelWithAnimator:(id <UIDragAnimating>)animator |
| { |
| _isAnimatingDragCancel = YES; |
| RELEASE_LOG(DragAndDrop, "Drag interaction willAnimateCancelWithAnimator"); |
| [animator addCompletion:[protectedSelf = retainPtr(self), page = _page] (UIViewAnimatingPosition finalPosition) { |
| RELEASE_LOG(DragAndDrop, "Drag interaction willAnimateCancelWithAnimator (animation completion block fired)"); |
| page->dragCancelled(); |
| if (auto completion = protectedSelf->_dragDropInteractionState.takeDragCancelSetDownBlock()) { |
| page->callAfterNextPresentationUpdate([completion, protectedSelf] (WebKit::CallbackBase::Error) { |
| completion(); |
| protectedSelf->_isAnimatingDragCancel = NO; |
| }); |
| } |
| }]; |
| } |
| |
| - (void)dragInteraction:(UIDragInteraction *)interaction sessionDidTransferItems:(id <UIDragSession>)session |
| { |
| [existingLocalDragSessionContext(session) cleanUpTemporaryDirectories]; |
| } |
| |
| #pragma mark - UIDropInteractionDelegate |
| |
| - (NSInteger)_dropInteraction:(UIDropInteraction *)interaction dataOwnerForSession:(id <UIDropSession>)session |
| { |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| NSInteger dataOwner = 0; |
| if ([uiDelegate respondsToSelector:@selector(_webView:dataOwnerForDropSession:)]) |
| dataOwner = [uiDelegate _webView:self.webView dataOwnerForDropSession:session]; |
| return dataOwner; |
| } |
| |
| - (BOOL)dropInteraction:(UIDropInteraction *)interaction canHandleSession:(id<UIDropSession>)session |
| { |
| // FIXME: Support multiple simultaneous drop sessions in the future. |
| id <UIDragDropSession> dragOrDropSession = self.currentDragOrDropSession; |
| RELEASE_LOG(DragAndDrop, "Can handle drag session: %p with local session: %p existing session: %p?", session, session.localDragSession, dragOrDropSession); |
| |
| return !dragOrDropSession || session.localDragSession == dragOrDropSession; |
| } |
| |
| - (void)dropInteraction:(UIDropInteraction *)interaction sessionDidEnter:(id <UIDropSession>)session |
| { |
| RELEASE_LOG(DragAndDrop, "Drop session entered: %p with %tu items", session, session.items.count); |
| auto dragData = [self dragDataForDropSession:session dragDestinationAction:[self _dragDestinationActionForDropSession:session]]; |
| |
| _dragDropInteractionState.dropSessionDidEnterOrUpdate(session, dragData); |
| |
| [[WebItemProviderPasteboard sharedInstance] setItemProviders:extractItemProvidersFromDropSession(session)]; |
| _page->dragEntered(dragData, WebCore::Pasteboard::nameOfDragPasteboard()); |
| } |
| |
| - (UIDropProposal *)dropInteraction:(UIDropInteraction *)interaction sessionDidUpdate:(id <UIDropSession>)session |
| { |
| [[WebItemProviderPasteboard sharedInstance] setItemProviders:extractItemProvidersFromDropSession(session)]; |
| |
| auto dragData = [self dragDataForDropSession:session dragDestinationAction:[self _dragDestinationActionForDropSession:session]]; |
| _page->dragUpdated(dragData, WebCore::Pasteboard::nameOfDragPasteboard()); |
| _dragDropInteractionState.dropSessionDidEnterOrUpdate(session, dragData); |
| |
| auto delegate = self.webViewUIDelegate; |
| auto operation = dropOperationForWebCoreDragOperation(_page->currentDragOperation()); |
| if ([delegate respondsToSelector:@selector(_webView:willUpdateDataInteractionOperationToOperation:forSession:)]) |
| operation = static_cast<UIDropOperation>([delegate _webView:self.webView willUpdateDataInteractionOperationToOperation:operation forSession:session]); |
| |
| auto proposal = adoptNS([[UIDropProposal alloc] initWithDropOperation:static_cast<UIDropOperation>(operation)]); |
| auto dragHandlingMethod = _page->currentDragHandlingMethod(); |
| if (dragHandlingMethod == WebCore::DragHandlingMethod::EditPlainText || dragHandlingMethod == WebCore::DragHandlingMethod::EditRichText) { |
| // When dragging near the top or bottom edges of an editable element, enabling precision drop mode may result in the drag session hit-testing outside of the editable |
| // element, causing the drag to no longer be accepted. This in turn disables precision drop mode, which causes the drag session to hit-test inside of the editable |
| // element again, which enables precision mode, thus continuing the cycle. To avoid precision mode thrashing, we forbid precision mode when dragging near the top or |
| // bottom of the editable element. |
| auto minimumDistanceFromVerticalEdgeForPreciseDrop = 25 / self.webView.scrollView.zoomScale; |
| [proposal setPrecise:CGRectContainsPoint(CGRectInset(_page->currentDragCaretEditableElementRect(), 0, minimumDistanceFromVerticalEdgeForPreciseDrop), [session locationInView:self])]; |
| } else |
| [proposal setPrecise:NO]; |
| |
| if ([delegate respondsToSelector:@selector(_webView:willUpdateDropProposalToProposal:forSession:)]) |
| proposal = [delegate _webView:self.webView willUpdateDropProposalToProposal:proposal.get() forSession:session]; |
| |
| return proposal.autorelease(); |
| } |
| |
| - (void)dropInteraction:(UIDropInteraction *)interaction sessionDidExit:(id <UIDropSession>)session |
| { |
| RELEASE_LOG(DragAndDrop, "Drop session exited: %p with %tu items", session, session.items.count); |
| [[WebItemProviderPasteboard sharedInstance] setItemProviders:extractItemProvidersFromDropSession(session)]; |
| |
| auto dragData = [self dragDataForDropSession:session dragDestinationAction:WKDragDestinationActionAny]; |
| _page->dragExited(dragData, WebCore::Pasteboard::nameOfDragPasteboard()); |
| _page->resetCurrentDragInformation(); |
| |
| _dragDropInteractionState.dropSessionDidExit(); |
| } |
| |
| - (void)dropInteraction:(UIDropInteraction *)interaction performDrop:(id <UIDropSession>)session |
| { |
| NSArray <NSItemProvider *> *itemProviders = extractItemProvidersFromDropSession(session); |
| id <WKUIDelegatePrivate> uiDelegate = self.webViewUIDelegate; |
| if ([uiDelegate respondsToSelector:@selector(_webView:performDataInteractionOperationWithItemProviders:)]) { |
| if ([uiDelegate _webView:self.webView performDataInteractionOperationWithItemProviders:itemProviders]) |
| return; |
| } |
| |
| if ([uiDelegate respondsToSelector:@selector(_webView:willPerformDropWithSession:)]) { |
| itemProviders = extractItemProvidersFromDragItems([uiDelegate _webView:self.webView willPerformDropWithSession:session]); |
| if (!itemProviders.count) |
| return; |
| } |
| |
| _dragDropInteractionState.dropSessionWillPerformDrop(); |
| |
| [[WebItemProviderPasteboard sharedInstance] setItemProviders:itemProviders]; |
| [[WebItemProviderPasteboard sharedInstance] incrementPendingOperationCount]; |
| auto dragData = [self dragDataForDropSession:session dragDestinationAction:WKDragDestinationActionAny]; |
| BOOL shouldSnapshotView = ![self _handleDropByInsertingImagePlaceholders:itemProviders session:session]; |
| |
| RELEASE_LOG(DragAndDrop, "Loading data from %tu item providers for session: %p", itemProviders.count, session); |
| // Always loading content from the item provider ensures that the web process will be allowed to call back in to the UI |
| // process to access pasteboard contents at a later time. Ideally, we only need to do this work if we're over a file input |
| // or the page prevented default on `dragover`, but without this, dropping into a normal editable areas will fail due to |
| // item providers not loading any data. |
| RetainPtr<WKContentView> retainedSelf(self); |
| [[WebItemProviderPasteboard sharedInstance] doAfterLoadingProvidedContentIntoFileURLs:[retainedSelf, capturedDragData = WTFMove(dragData), shouldSnapshotView] (NSArray *fileURLs) mutable { |
| RELEASE_LOG(DragAndDrop, "Loaded data into %tu files", fileURLs.count); |
| Vector<String> filenames; |
| for (NSURL *fileURL in fileURLs) |
| filenames.append([fileURL path]); |
| capturedDragData.setFileNames(filenames); |
| |
| WebKit::SandboxExtension::Handle sandboxExtensionHandle; |
| Vector<WebKit::SandboxExtension::Handle> sandboxExtensionForUpload; |
| auto dragPasteboardName = WebCore::Pasteboard::nameOfDragPasteboard(); |
| retainedSelf->_page->grantAccessToCurrentPasteboardData(dragPasteboardName); |
| retainedSelf->_page->createSandboxExtensionsIfNeeded(filenames, sandboxExtensionHandle, sandboxExtensionForUpload); |
| retainedSelf->_page->performDragOperation(capturedDragData, dragPasteboardName, WTFMove(sandboxExtensionHandle), WTFMove(sandboxExtensionForUpload)); |
| if (shouldSnapshotView) { |
| retainedSelf->_visibleContentViewSnapshot = [retainedSelf snapshotViewAfterScreenUpdates:NO]; |
| [retainedSelf->_visibleContentViewSnapshot setFrame:[retainedSelf bounds]]; |
| [retainedSelf addSubview:retainedSelf->_visibleContentViewSnapshot.get()]; |
| } |
| }]; |
| } |
| |
| - (void)dropInteraction:(UIDropInteraction *)interaction item:(UIDragItem *)item willAnimateDropWithAnimator:(id <UIDragAnimating>)animator |
| { |
| _dropAnimationCount++; |
| [animator addCompletion:[strongSelf = retainPtr(self)] (UIViewAnimatingPosition) { |
| if (!--strongSelf->_dropAnimationCount) |
| [std::exchange(strongSelf->_unselectedContentSnapshot, nil) removeFromSuperview]; |
| }]; |
| } |
| |
| - (void)dropInteraction:(UIDropInteraction *)interaction concludeDrop:(id <UIDropSession>)session |
| { |
| [self _removeContainerForDropPreviews]; |
| [std::exchange(_visibleContentViewSnapshot, nil) removeFromSuperview]; |
| [std::exchange(_unselectedContentSnapshot, nil) removeFromSuperview]; |
| _dragDropInteractionState.clearAllDelayedItemPreviewProviders(); |
| _page->didConcludeDrop(); |
| } |
| |
| - (UITargetedDragPreview *)dropInteraction:(UIDropInteraction *)interaction previewForDroppingItem:(UIDragItem *)item withDefault:(UITargetedDragPreview *)defaultPreview |
| { |
| _dragDropInteractionState.setDefaultDropPreview(item, defaultPreview); |
| |
| CGRect caretRect = _page->currentDragCaretRect(); |
| if (CGRectIsEmpty(caretRect)) |
| return nil; |
| |
| UIView *textEffectsWindow = self.textEffectsWindow; |
| auto caretRectInWindowCoordinates = [self convertRect:caretRect toView:textEffectsWindow]; |
| auto caretCenterInWindowCoordinates = CGPointMake(CGRectGetMidX(caretRectInWindowCoordinates), CGRectGetMidY(caretRectInWindowCoordinates)); |
| auto targetPreviewCenterInWindowCoordinates = CGPointMake(caretCenterInWindowCoordinates.x + defaultPreview.size.width / 2, caretCenterInWindowCoordinates.y + defaultPreview.size.height / 2); |
| auto target = adoptNS([[UIDragPreviewTarget alloc] initWithContainer:textEffectsWindow center:targetPreviewCenterInWindowCoordinates transform:CGAffineTransformIdentity]); |
| return [defaultPreview retargetedPreviewWithTarget:target.get()]; |
| } |
| |
| - (void)_dropInteraction:(UIDropInteraction *)interaction delayedPreviewProviderForDroppingItem:(UIDragItem *)item previewProvider:(void(^)(UITargetedDragPreview *preview))previewProvider |
| { |
| // FIXME: This doesn't currently handle multiple items in a drop session. |
| _dragDropInteractionState.prepareForDelayedDropPreview(item, previewProvider); |
| } |
| |
| - (void)dropInteraction:(UIDropInteraction *)interaction sessionDidEnd:(id <UIDropSession>)session |
| { |
| RELEASE_LOG(DragAndDrop, "Drop session ended: %p (performing operation: %d, began dragging: %d)", session, _dragDropInteractionState.isPerformingDrop(), _dragDropInteractionState.didBeginDragging()); |
| if (_dragDropInteractionState.isPerformingDrop()) { |
| // In the case where we are performing a drop, wait until after the drop is handled in the web process to reset drag and drop interaction state. |
| return; |
| } |
| |
| if (_dragDropInteractionState.didBeginDragging()) { |
| // In the case where the content view is a source of drag items, wait until -dragInteraction:session:didEndWithOperation: to reset drag and drop interaction state. |
| return; |
| } |
| |
| CGPoint global; |
| CGPoint client; |
| [self computeClientAndGlobalPointsForDropSession:session outClientPoint:&client outGlobalPoint:&global]; |
| [self cleanUpDragSourceSessionState]; |
| _page->dragEnded(WebCore::roundedIntPoint(client), WebCore::roundedIntPoint(global), { }); |
| } |
| |
| #endif |
| |
| #if HAVE(PEPPER_UI_CORE) |
| |
| - (void)dismissQuickboardViewControllerAndRevealFocusedFormOverlayIfNecessary:(id)controller |
| { |
| BOOL shouldRevealFocusOverlay = NO; |
| // In the case where there's nothing the user could potentially do besides dismiss the overlay, we can just automatically without asking the delegate. |
| if ([self.webView._inputDelegate respondsToSelector:@selector(_webView:shouldRevealFocusOverlayForInputSession:)] |
| && ([self actionNameForFocusedFormControlView:_focusedFormControlView.get()] || _focusedElementInformation.hasNextNode || _focusedElementInformation.hasPreviousNode)) |
| shouldRevealFocusOverlay = [self.webView._inputDelegate _webView:self.webView shouldRevealFocusOverlayForInputSession:_formInputSession.get()]; |
| |
| if (shouldRevealFocusOverlay) { |
| [_focusedFormControlView show:NO]; |
| [self updateCurrentFocusedElementInformation:[weakSelf = WeakObjCPtr<WKContentView>(self)] (bool didUpdate) { |
| if (!didUpdate) |
| return; |
| |
| auto focusedFormController = weakSelf.get()->_focusedFormControlView; |
| [focusedFormController reloadData:YES]; |
| [focusedFormController engageFocusedFormControlNavigation]; |
| }]; |
| } else |
| _page->blurFocusedElement(); |
| |
| bool shouldDismissViewController = [controller isKindOfClass:UIViewController.class] |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| && !_presentedQuickboardController |
| #endif |
| && controller != _presentedFullScreenInputViewController; |
| // The Quickboard view controller passed into this delegate method is not necessarily the view controller we originally presented; |
| // this happens in the case when the user chooses an input method (e.g. scribble) and a new Quickboard view controller is presented. |
| if (shouldDismissViewController) |
| [(UIViewController *)controller dismissViewControllerAnimated:YES completion:nil]; |
| |
| [self dismissAllInputViewControllers:controller == _presentedFullScreenInputViewController]; |
| } |
| |
| - (UITextContentType)textContentTypeForQuickboard |
| { |
| switch (_focusedElementInformation.elementType) { |
| case WebKit::InputType::Password: |
| return UITextContentTypePassword; |
| case WebKit::InputType::URL: |
| return UITextContentTypeURL; |
| case WebKit::InputType::Email: |
| return UITextContentTypeEmailAddress; |
| case WebKit::InputType::Phone: |
| return UITextContentTypeTelephoneNumber; |
| default: |
| // The element type alone is insufficient to infer content type; fall back to autofill data. |
| if (auto contentType = contentTypeFromFieldName(_focusedElementInformation.autofillFieldName)) |
| return contentType; |
| |
| if (_focusedElementInformation.isAutofillableUsernameField) |
| return UITextContentTypeUsername; |
| |
| return nil; |
| } |
| } |
| |
| #pragma mark - PUICQuickboardViewControllerDelegate |
| |
| - (void)quickboard:(PUICQuickboardViewController *)quickboard textEntered:(NSAttributedString *)attributedText |
| { |
| if (attributedText) |
| _page->setTextAsync(attributedText.string); |
| |
| [self dismissQuickboardViewControllerAndRevealFocusedFormOverlayIfNecessary:quickboard]; |
| } |
| |
| - (void)quickboardInputCancelled:(PUICQuickboardViewController *)quickboard |
| { |
| [self dismissQuickboardViewControllerAndRevealFocusedFormOverlayIfNecessary:quickboard]; |
| } |
| |
| #pragma mark - WKQuickboardViewControllerDelegate |
| |
| - (BOOL)allowsLanguageSelectionForListViewController:(PUICQuickboardViewController *)controller |
| { |
| switch (_focusedElementInformation.elementType) { |
| case WebKit::InputType::ContentEditable: |
| case WebKit::InputType::Text: |
| case WebKit::InputType::TextArea: |
| case WebKit::InputType::Search: |
| case WebKit::InputType::Email: |
| case WebKit::InputType::URL: |
| return YES; |
| default: |
| return NO; |
| } |
| } |
| |
| - (UIView *)inputContextViewForViewController:(PUICQuickboardViewController *)controller |
| { |
| id <_WKInputDelegate> delegate = self.webView._inputDelegate; |
| if (![delegate respondsToSelector:@selector(_webView:focusedElementContextViewForInputSession:)]) |
| return nil; |
| |
| return [delegate _webView:self.webView focusedElementContextViewForInputSession:_formInputSession.get()]; |
| } |
| |
| - (NSString *)inputLabelTextForViewController:(PUICQuickboardViewController *)controller |
| { |
| return [self inputLabelText]; |
| } |
| |
| - (NSString *)initialValueForViewController:(PUICQuickboardViewController *)controller |
| { |
| return _focusedElementInformation.value; |
| } |
| |
| - (BOOL)shouldDisplayInputContextViewForListViewController:(PUICQuickboardViewController *)controller |
| { |
| switch (_focusedElementInformation.elementType) { |
| case WebKit::InputType::ContentEditable: |
| case WebKit::InputType::Text: |
| case WebKit::InputType::Password: |
| case WebKit::InputType::TextArea: |
| case WebKit::InputType::Search: |
| case WebKit::InputType::Email: |
| case WebKit::InputType::URL: |
| case WebKit::InputType::Phone: |
| return YES; |
| default: |
| return NO; |
| } |
| } |
| |
| #pragma mark - WKTextInputListViewControllerDelegate |
| |
| - (WKNumberPadInputMode)numericInputModeForListViewController:(WKTextInputListViewController *)controller |
| { |
| switch (_focusedElementInformation.elementType) { |
| case WebKit::InputType::Phone: |
| return WKNumberPadInputModeTelephone; |
| case WebKit::InputType::Number: |
| return WKNumberPadInputModeNumbersAndSymbols; |
| case WebKit::InputType::NumberPad: |
| return WKNumberPadInputModeNumbersOnly; |
| default: |
| return WKNumberPadInputModeNone; |
| } |
| } |
| |
| - (PUICTextInputContext *)textInputContextForListViewController:(WKTextInputListViewController *)controller |
| { |
| return self.createQuickboardTextInputContext.autorelease(); |
| } |
| |
| - (BOOL)allowsDictationInputForListViewController:(PUICQuickboardViewController *)controller |
| { |
| return _focusedElementInformation.elementType != WebKit::InputType::Password; |
| } |
| |
| #endif // HAVE(PEPPER_UI_CORE) |
| |
| #if HAVE(LOOKUP_GESTURE_RECOGNIZER) |
| - (void)_lookupGestureRecognized:(UIGestureRecognizer *)gestureRecognizer |
| { |
| NSPoint locationInViewCoordinates = [gestureRecognizer locationInView:self]; |
| _page->performDictionaryLookupAtLocation(WebCore::FloatPoint(locationInViewCoordinates)); |
| } |
| #endif |
| |
| #if ENABLE(APP_HIGHLIGHTS) |
| |
| - (void)setUpAppHighlightMenusIfNeeded |
| { |
| if (!_page->preferences().appHighlightsEnabled() || !self.window || !_page->editorState().selectionIsRange) |
| return; |
| |
| for (UIMenuItem *menuItem in [[UIMenuController sharedMenuController] menuItems]) { |
| if ([menuItem action] == @selector(createHighlightForCurrentQuickNoteWithRange:) || [menuItem action] == @selector(createHighlightForNewQuickNoteWithRange:)) |
| return; |
| } |
| |
| auto addHighlightCurrentQuickNoteItem = adoptNS([[UIMenuItem alloc] initWithTitle:WebCore::contextMenuItemTagAddHighlightToCurrentQuickNote() action:@selector(createHighlightForCurrentQuickNoteWithRange:)]); |
| auto addHighlightNewQuickNoteItem = adoptNS([[UIMenuItem alloc] initWithTitle:WebCore::contextMenuItemTagAddHighlightToNewQuickNote() action:@selector(createHighlightForNewQuickNoteWithRange:)]); |
| [[UIMenuController sharedMenuController] setMenuItems:@[ addHighlightCurrentQuickNoteItem.get(), addHighlightNewQuickNoteItem.get() ]]; |
| } |
| |
| - (void)createHighlightForCurrentQuickNoteWithRange:(id)sender |
| { |
| _page->createAppHighlightInSelectedRange(WebCore::CreateNewGroupForHighlight::No, WebCore::HighlightRequestOriginatedInApp::No); |
| } |
| |
| - (void)createHighlightForNewQuickNoteWithRange:(id)sender |
| { |
| _page->createAppHighlightInSelectedRange(WebCore::CreateNewGroupForHighlight::Yes, WebCore::HighlightRequestOriginatedInApp::No); |
| } |
| |
| #endif // ENABLE(APP_HIGHLIGHTS) |
| |
| - (void)setContinuousSpellCheckingEnabled:(BOOL)enabled |
| { |
| if (WebKit::TextChecker::setContinuousSpellCheckingEnabled(enabled)) |
| _page->process().updateTextCheckerState(); |
| } |
| |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) |
| |
| static BOOL applicationIsKnownToIgnoreMouseEvents(const char* &warningVersion) |
| { |
| // System apps will always be linked on the current OS, so |
| // check them before the linked-on-or-after. |
| |
| // <rdar://problem/59521967> iAd Video does not respond to mouse events, only touch events |
| if (WebCore::IOSApplication::isNews() || WebCore::IOSApplication::isStocks()) { |
| warningVersion = nullptr; |
| return YES; |
| } |
| |
| if (!linkedOnOrAfter(SDKVersion::FirstVersionWithiOSAppsOnMacOS)) { |
| if (WebCore::IOSApplication::isFIFACompanion() // <rdar://problem/67093487> |
| || WebCore::IOSApplication::isNoggin() // <rdar://problem/64830335> |
| || WebCore::IOSApplication::isOKCupid() // <rdar://problem/65698496> |
| || WebCore::IOSApplication::isJWLibrary() // <rdar://problem/68104852> |
| || WebCore::IOSApplication::isPaperIO() // <rdar://problem/68738585> |
| || WebCore::IOSApplication::isCrunchyroll()) { // <rdar://problem/66362029> |
| warningVersion = "14.2"; |
| return YES; |
| } |
| } |
| |
| if (!linkedOnOrAfter(SDKVersion::FirstThatSendsNativeMouseEvents)) { |
| if (WebCore::IOSApplication::isPocketCity() // <rdar://problem/62273077> |
| || WebCore::IOSApplication::isEssentialSkeleton() // <rdar://problem/62694519> |
| || WebCore::IOSApplication::isESPNFantasySports() // <rdar://problem/64671543> |
| || WebCore::IOSApplication::isDoubleDown()) { // <rdar://problem/64668138> |
| warningVersion = "13.4"; |
| return YES; |
| } |
| } |
| |
| return NO; |
| } |
| |
| - (BOOL)shouldUseMouseGestureRecognizer |
| { |
| static const BOOL shouldUseMouseGestureRecognizer = []() -> BOOL { |
| const char* warningVersion = nullptr; |
| BOOL knownToIgnoreMouseEvents = applicationIsKnownToIgnoreMouseEvents(warningVersion); |
| |
| if (knownToIgnoreMouseEvents && warningVersion) |
| os_log_error(OS_LOG_DEFAULT, "WARNING: This application has been observed to ignore mouse events in web content; touch events will be sent until it is built against the iOS %s SDK, but after that, the web content must respect mouse or pointer events in addition to touch events in order to behave correctly when a trackpad or mouse is used.", warningVersion); |
| |
| return !knownToIgnoreMouseEvents; |
| }(); |
| |
| switch (_mouseEventPolicy) { |
| case WebCore::MouseEventPolicy::Default: |
| break; |
| #if ENABLE(IOS_TOUCH_EVENTS) |
| case WebCore::MouseEventPolicy::SynthesizeTouchEvents: |
| return NO; |
| #endif |
| } |
| |
| return shouldUseMouseGestureRecognizer; |
| } |
| |
| - (void)setUpMouseGestureRecognizer |
| { |
| _mouseGestureRecognizer = adoptNS([[WKMouseGestureRecognizer alloc] initWithTarget:self action:@selector(mouseGestureRecognizerChanged:)]); |
| [_mouseGestureRecognizer setDelegate:self]; |
| [self _configureMouseGestureRecognizer]; |
| [self addGestureRecognizer:_mouseGestureRecognizer.get()]; |
| } |
| |
| - (void)mouseGestureRecognizerChanged:(WKMouseGestureRecognizer *)gestureRecognizer |
| { |
| if (!_page->hasRunningProcess()) |
| return; |
| |
| auto event = gestureRecognizer.lastMouseEvent; |
| if (!event) |
| return; |
| |
| if (event->type() == WebKit::WebEvent::MouseDown) { |
| _layerTreeTransactionIdAtLastInteractionStart = downcast<WebKit::RemoteLayerTreeDrawingAreaProxy>(*_page->drawingArea()).lastCommittedLayerTreeTransactionID(); |
| |
| if (auto lastMouseLocation = gestureRecognizer.lastMouseLocation) |
| _lastInteractionLocation = *lastMouseLocation; |
| } |
| |
| if (event->type() == WebKit::WebEvent::MouseUp && self.hasHiddenContentEditable && self._hasFocusedElement && !self.window.keyWindow) |
| [self.window makeKeyWindow]; |
| |
| _page->handleMouseEvent(*event); |
| if (WKHoverPlatterDomain.rootSettings.platterEnabledForMouse) |
| [_hoverPlatter setHoverPoint:event->position()]; |
| } |
| |
| - (void)_configureMouseGestureRecognizer |
| { |
| [_mouseGestureRecognizer setEnabled:[self shouldUseMouseGestureRecognizer]]; |
| } |
| |
| - (void)_setMouseEventPolicy:(WebCore::MouseEventPolicy)policy |
| { |
| _mouseEventPolicy = policy; |
| [self _configureMouseGestureRecognizer]; |
| } |
| |
| #endif // HAVE(UIKIT_WITH_MOUSE_SUPPORT) |
| |
| #if ENABLE(HOVER_GESTURE_RECOGNIZER) |
| |
| - (void)setUpHoverGestureRecognizer |
| { |
| _hoverGestureRecognizer = adoptNS([[WKHoverGestureRecognizer alloc] initWithTarget:self action:@selector(hoverGestureRecognizerChanged:)]); |
| [_hoverGestureRecognizer setDelegate:self]; |
| [self addGestureRecognizer:_hoverGestureRecognizer.get()]; |
| } |
| |
| - (void)hoverGestureRecognizerChanged:(WKHoverGestureRecognizer *)gestureRecognizer |
| { |
| if (!_page->hasRunningProcess()) |
| return; |
| |
| auto event = gestureRecognizer.lastMouseEvent; |
| if (!event) |
| return; |
| |
| _page->handleMouseEvent(*event); |
| if (WKHoverPlatterDomain.rootSettings.platterEnabledForHover) |
| [_hoverPlatter setHoverPoint:event->position()]; |
| } |
| |
| #endif // ENABLE(HOVER_GESTURE_RECOGNIZER) |
| |
| #if HAVE(UIKIT_WITH_MOUSE_SUPPORT) || ENABLE(HOVER_GESTURE_RECOGNIZER) |
| |
| - (void)positionInformationForHoverPlatter:(WKHoverPlatter *)hoverPlatter withRequest:(WebKit::InteractionInformationRequest&)request completionHandler:(void (^)(WebKit::InteractionInformationAtPosition))completionHandler |
| { |
| [self doAfterPositionInformationUpdate:completionHandler forRequest:request]; |
| } |
| |
| #endif |
| |
| #if ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) && USE(UICONTEXTMENU) |
| |
| - (void)_showMediaControlsContextMenu:(WebCore::FloatRect&&)targetFrame items:(Vector<WebCore::MediaControlsContextMenuItem>&&)items completionHandler:(CompletionHandler<void(WebCore::MediaControlsContextMenuItem::ID)>&&)completionHandler |
| { |
| [_actionSheetAssistant showMediaControlsContextMenu:WTFMove(targetFrame) items:WTFMove(items) completionHandler:WTFMove(completionHandler)]; |
| } |
| |
| #endif // ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) && USE(UICONTEXTMENU) |
| |
| #if HAVE(UI_POINTER_INTERACTION) |
| |
| - (void)setUpPointerInteraction |
| { |
| _pointerInteraction = adoptNS([[UIPointerInteraction alloc] initWithDelegate:self]); |
| [_pointerInteraction _setPausesPointerUpdatesWhilePanning:NO]; |
| |
| [self addInteraction:_pointerInteraction.get()]; |
| } |
| |
| - (void)_pointerInteraction:(UIPointerInteraction *)interaction regionForRequest:(UIPointerRegionRequest *)request defaultRegion:(UIPointerRegion *)defaultRegion completion:(void(^)(UIPointerRegion *region))completion |
| { |
| WebKit::InteractionInformationRequest interactionInformationRequest; |
| interactionInformationRequest.point = WebCore::roundedIntPoint(request.location); |
| interactionInformationRequest.includeCaretContext = true; |
| interactionInformationRequest.includeHasDoubleClickHandler = false; |
| |
| BOOL didSynchronouslyReplyWithApproximation = false; |
| if (![self _currentPositionInformationIsValidForRequest:interactionInformationRequest] && self.webView._editable && !_positionInformation.shouldNotUseIBeamInEditableContent) { |
| didSynchronouslyReplyWithApproximation = true; |
| completion([UIPointerRegion regionWithRect:self.bounds identifier:editablePointerRegionIdentifier]); |
| } |
| |
| // If we already have an outstanding interaction information request, defer this one until |
| // we hear back, so that requests don't pile up if the Web Content process is slow. |
| if (_hasOutstandingPointerInteractionRequest) { |
| _deferredPointerInteractionRequest = std::make_pair(interactionInformationRequest, makeBlockPtr(completion)); |
| return; |
| } |
| |
| _hasOutstandingPointerInteractionRequest = YES; |
| |
| __block BlockPtr<void(WebKit::InteractionInformationAtPosition, void(^)(UIPointerRegion *))> replyHandler; |
| replyHandler = ^(WebKit::InteractionInformationAtPosition interactionInformation, void(^completion)(UIPointerRegion *region)) { |
| if (!_deferredPointerInteractionRequest) |
| _hasOutstandingPointerInteractionRequest = NO; |
| |
| if (didSynchronouslyReplyWithApproximation) { |
| [interaction invalidate]; |
| return; |
| } |
| |
| completion([self pointerRegionForPositionInformation:interactionInformation point:request.location]); |
| |
| if (_deferredPointerInteractionRequest) { |
| auto deferredRequest = std::exchange(_deferredPointerInteractionRequest, std::nullopt); |
| [self doAfterPositionInformationUpdate:^(WebKit::InteractionInformationAtPosition interactionInformation) { |
| replyHandler(interactionInformation, deferredRequest->second.get()); |
| } forRequest:deferredRequest->first]; |
| return; |
| } |
| }; |
| |
| [self doAfterPositionInformationUpdate:^(WebKit::InteractionInformationAtPosition interactionInformation) { |
| replyHandler(interactionInformation, completion); |
| } forRequest:interactionInformationRequest]; |
| } |
| |
| - (UIPointerRegion *)pointerRegionForPositionInformation:(WebKit::InteractionInformationAtPosition&)interactionInformation point:(CGPoint)location |
| { |
| WebCore::FloatRect expandedLineRect = enclosingIntRect(interactionInformation.lineCaretExtent); |
| |
| // Pad lines of text in order to avoid switching back to the dot cursor between lines. |
| // This matches the value that UIKit uses. |
| expandedLineRect.inflateY(10); |
| |
| if (interactionInformation.cursor) { |
| WebCore::Cursor::Type cursorType = interactionInformation.cursor->type(); |
| if (cursorType == WebCore::Cursor::Hand) |
| return [UIPointerRegion regionWithRect:interactionInformation.bounds identifier:pointerRegionIdentifier]; |
| |
| if (cursorType == WebCore::Cursor::IBeam && expandedLineRect.contains(location)) |
| return [UIPointerRegion regionWithRect:expandedLineRect identifier:pointerRegionIdentifier]; |
| } |
| |
| if (self.webView._editable) { |
| if (expandedLineRect.contains(location)) |
| return [UIPointerRegion regionWithRect:expandedLineRect identifier:pointerRegionIdentifier]; |
| return [UIPointerRegion regionWithRect:self.bounds identifier:editablePointerRegionIdentifier]; |
| } |
| |
| return nil; |
| } |
| |
| - (UIPointerStyle *)pointerInteraction:(UIPointerInteraction *)interaction styleForRegion:(UIPointerRegion *)region |
| { |
| double scaleFactor = self._contentZoomScale; |
| |
| UIPointerStyle *(^iBeamCursor)(void) = ^{ |
| float beamLength = _positionInformation.caretLength * scaleFactor; |
| auto axisOrientation = _positionInformation.isVerticalWritingMode ? UIAxisHorizontal : UIAxisVertical; |
| UIAxis iBeamConstraintAxes = _positionInformation.isVerticalWritingMode ? UIAxisHorizontal : UIAxisVertical; |
| |
| // If the I-beam is so large that the magnetism is hard to fight, we should not apply any magnetism. |
| if (beamLength > [UITextInteraction _maximumBeamSnappingLength]) |
| iBeamConstraintAxes = UIAxisNeither; |
| |
| // If the region is the size of the view, we should not apply any magnetism. |
| if ([region.identifier isEqual:editablePointerRegionIdentifier]) |
| iBeamConstraintAxes = UIAxisNeither; |
| |
| return [UIPointerStyle styleWithShape:[UIPointerShape beamWithPreferredLength:beamLength axis:axisOrientation] constrainedAxes:iBeamConstraintAxes]; |
| }; |
| |
| if (self.webView._editable) { |
| if (_positionInformation.shouldNotUseIBeamInEditableContent) |
| return nil; |
| return iBeamCursor(); |
| } |
| |
| if (_positionInformation.cursor && [region.identifier isEqual:pointerRegionIdentifier]) { |
| WebCore::Cursor::Type cursorType = _positionInformation.cursor->type(); |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if (cursorType == WebCore::Cursor::Hand) |
| return [UIPointerStyle _systemPointerStyle]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| if (cursorType == WebCore::Cursor::IBeam && _positionInformation.lineCaretExtent.contains(_positionInformation.request.point)) |
| return iBeamCursor(); |
| } |
| |
| ASSERT_NOT_REACHED(); |
| return nil; |
| } |
| |
| #endif // HAVE(UI_POINTER_INTERACTION) |
| |
| #if HAVE(PENCILKIT_TEXT_INPUT) |
| |
| - (void)setUpScribbleInteraction |
| { |
| _scribbleInteraction = adoptNS([[UIIndirectScribbleInteraction alloc] initWithDelegate:self]); |
| [self addInteraction:_scribbleInteraction.get()]; |
| } |
| |
| - (void)cleanUpScribbleInteraction |
| { |
| [self removeInteraction:_scribbleInteraction.get()]; |
| _scribbleInteraction = nil; |
| } |
| |
| - (_WKTextInputContext *)_textInputContextByScribbleIdentifier:(UIScribbleElementIdentifier)identifier |
| { |
| _WKTextInputContext *textInputContext = (_WKTextInputContext *)identifier; |
| if (![textInputContext isKindOfClass:_WKTextInputContext.class]) |
| return nil; |
| auto elementContext = textInputContext._textInputContext; |
| if (elementContext.webPageIdentifier != _page->webPageID()) |
| return nil; |
| return textInputContext; |
| } |
| |
| - (BOOL)_elementForTextInputContextIsFocused:(_WKTextInputContext *)context |
| { |
| // We ignore bounding rect changes as the bounding rect of the focused element is not kept up-to-date. |
| return self._hasFocusedElement && context && context._textInputContext.isSameElement(_focusedElementInformation.elementContext); |
| } |
| |
| - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction *)interaction requestElementsInRect:(CGRect)rect completion:(void(^)(NSArray<UIScribbleElementIdentifier> *))completion |
| { |
| ASSERT(_scribbleInteraction.get() == interaction); |
| [self _requestTextInputContextsInRect:rect completionHandler:completion]; |
| } |
| |
| - (BOOL)indirectScribbleInteraction:(UIIndirectScribbleInteraction *)interaction isElementFocused:(UIScribbleElementIdentifier)identifier |
| { |
| ASSERT(_scribbleInteraction.get() == interaction); |
| return [self _elementForTextInputContextIsFocused:[self _textInputContextByScribbleIdentifier:identifier]]; |
| } |
| |
| - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction *)interaction focusElementIfNeeded:(UIScribbleElementIdentifier)identifier referencePoint:(CGPoint)initialPoint completion:(void (^)(UIResponder<UITextInput> *))completionBlock |
| { |
| ASSERT(_scribbleInteraction.get() == interaction); |
| if (auto *textInputContext = [self _textInputContextByScribbleIdentifier:identifier]) |
| [self _focusTextInputContext:textInputContext placeCaretAt:initialPoint completionHandler:completionBlock]; |
| else |
| completionBlock(nil); |
| } |
| |
| - (CGRect)indirectScribbleInteraction:(UIIndirectScribbleInteraction *)interaction frameForElement:(UIScribbleElementIdentifier)identifier |
| { |
| ASSERT(_scribbleInteraction.get() == interaction); |
| auto *textInputContext = [self _textInputContextByScribbleIdentifier:identifier]; |
| return textInputContext ? textInputContext.boundingRect : CGRectNull; |
| } |
| |
| - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction *)interaction willBeginWritingInElement:(UIScribbleElementIdentifier)identifier |
| { |
| ASSERT(_scribbleInteraction.get() == interaction); |
| if (auto *textInputContext = [self _textInputContextByScribbleIdentifier:identifier]) |
| [self _willBeginTextInteractionInTextInputContext:textInputContext]; |
| } |
| |
| - (void)indirectScribbleInteraction:(UIIndirectScribbleInteraction *)interaction didFinishWritingInElement:(UIScribbleElementIdentifier)identifier |
| { |
| ASSERT(_scribbleInteraction.get() == interaction); |
| if (auto *textInputContext = [self _textInputContextByScribbleIdentifier:identifier]) |
| [self _didFinishTextInteractionInTextInputContext:textInputContext]; |
| } |
| |
| #endif // HAVE(PENCILKIT_TEXT_INPUT) |
| |
| #if ENABLE(ATTACHMENT_ELEMENT) |
| |
| #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) |
| |
| static RetainPtr<NSItemProvider> createItemProvider(const WebKit::WebPageProxy& page, const WebCore::PromisedAttachmentInfo& info) |
| { |
| auto numberOfAdditionalTypes = info.additionalTypes.size(); |
| ASSERT(numberOfAdditionalTypes == info.additionalData.size()); |
| |
| auto attachment = page.attachmentForIdentifier(info.attachmentIdentifier); |
| if (!attachment) |
| return { }; |
| |
| NSString *utiType = attachment->utiType(); |
| if (![utiType length]) |
| return { }; |
| |
| auto fileWrapper = retainPtr(attachment->fileWrapper()); |
| if (!fileWrapper) |
| return { }; |
| |
| auto item = adoptNS([[NSItemProvider alloc] init]); |
| [item setPreferredPresentationStyle:UIPreferredPresentationStyleAttachment]; |
| |
| NSString *fileName = attachment->fileName(); |
| if ([fileName length]) |
| [item setSuggestedName:fileName]; |
| |
| if (numberOfAdditionalTypes == info.additionalData.size() && numberOfAdditionalTypes) { |
| for (size_t index = 0; index < numberOfAdditionalTypes; ++index) { |
| auto nsData = info.additionalData[index]->createNSData(); |
| [item registerDataRepresentationForTypeIdentifier:info.additionalTypes[index] visibility:NSItemProviderRepresentationVisibilityAll loadHandler:[nsData](void (^completionHandler)(NSData *, NSError *)) -> NSProgress * { |
| completionHandler(nsData.get(), nil); |
| return nil; |
| }]; |
| } |
| } |
| |
| [item registerDataRepresentationForTypeIdentifier:utiType visibility:NSItemProviderRepresentationVisibilityAll loadHandler:[fileWrapper](void (^completionHandler)(NSData *, NSError *)) -> NSProgress * { |
| if (auto nsData = retainPtr([fileWrapper serializedRepresentation])) |
| completionHandler(nsData.get(), nil); |
| else |
| completionHandler(nil, [NSError errorWithDomain:WKErrorDomain code:WKErrorUnknown userInfo:nil]); |
| return nil; |
| }]; |
| |
| return item; |
| } |
| |
| #endif // !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) |
| |
| - (void)_writePromisedAttachmentToPasteboard:(WebCore::PromisedAttachmentInfo&&)info |
| { |
| #if !PLATFORM(WATCHOS) && !PLATFORM(APPLETV) |
| if (auto item = createItemProvider(*_page, WTFMove(info))) |
| UIPasteboard.generalPasteboard.itemProviders = @[ item.get() ]; |
| #else |
| UNUSED_PARAM(info); |
| #endif |
| } |
| |
| #endif // ENABLE(ATTACHMENT_ELEMENT) |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| |
| - (void)_endImageAnalysisGestureDeferral:(WebKit::ShouldPreventGestures)shouldPreventGestures |
| { |
| [_imageAnalysisDeferringGestureRecognizer endDeferral:shouldPreventGestures]; |
| } |
| |
| - (void)_doAfterPendingImageAnalysis:(void(^)(WebKit::ProceedWithTextSelectionInImage))block |
| { |
| if (self.hasPendingImageAnalysisRequest) |
| _actionsToPerformAfterPendingImageAnalysis.append(makeBlockPtr(block)); |
| else |
| block(WebKit::ProceedWithTextSelectionInImage::No); |
| } |
| |
| - (void)_invokeAllActionsToPerformAfterPendingImageAnalysis:(WebKit::ProceedWithTextSelectionInImage)proceedWithTextSelectionInImage |
| { |
| _pendingImageAnalysisRequestIdentifier = std::nullopt; |
| _elementPendingImageAnalysis = std::nullopt; |
| for (auto block : std::exchange(_actionsToPerformAfterPendingImageAnalysis, { })) |
| block(proceedWithTextSelectionInImage); |
| } |
| |
| #endif // ENABLE(IMAGE_ANALYSIS) |
| |
| - (void)setUpTextIndicator:(Ref<WebCore::TextIndicator>)textIndicator |
| { |
| if (_textIndicator == textIndicator.ptr()) |
| return; |
| |
| [self teardownTextIndicatorLayer]; |
| [NSObject cancelPreviousPerformRequestsWithTarget:self selector:@selector(startFadeOut) object:nil]; |
| |
| _textIndicator = textIndicator.ptr(); |
| |
| CGRect frame = _textIndicator->textBoundingRectInRootViewCoordinates(); |
| _textIndicatorLayer = adoptNS([[WebTextIndicatorLayer alloc] initWithFrame:frame |
| textIndicator:textIndicator margin:CGSizeZero offset:CGPointZero]); |
| |
| [[self layer] addSublayer:_textIndicatorLayer.get()]; |
| |
| if (_textIndicator->presentationTransition() != WebCore::TextIndicatorPresentationTransition::None) |
| [_textIndicatorLayer present]; |
| |
| [self performSelector:@selector(startFadeOut) withObject:self afterDelay:WebCore::timeBeforeFadeStarts.value()]; |
| } |
| |
| - (void)clearTextIndicator:(WebCore::TextIndicatorDismissalAnimation)animation |
| { |
| RefPtr<WebCore::TextIndicator> textIndicator = WTFMove(_textIndicator); |
| |
| if ([_textIndicatorLayer isFadingOut]) |
| return; |
| |
| if (textIndicator && [_textIndicatorLayer indicatorWantsManualAnimation:*textIndicator] && [_textIndicatorLayer hasCompletedAnimation] && animation == WebCore::TextIndicatorDismissalAnimation::FadeOut) { |
| [self startFadeOut]; |
| return; |
| } |
| |
| [self teardownTextIndicatorLayer]; |
| } |
| |
| - (void)setTextIndicatorAnimationProgress:(float)animationProgress |
| { |
| if (!_textIndicator) |
| return; |
| |
| [_textIndicatorLayer setAnimationProgress:animationProgress]; |
| } |
| |
| - (void)teardownTextIndicatorLayer |
| { |
| [_textIndicatorLayer removeFromSuperlayer]; |
| _textIndicatorLayer = nil; |
| } |
| |
| - (void)startFadeOut |
| { |
| [_textIndicatorLayer setFadingOut:YES]; |
| |
| [_textIndicatorLayer hideWithCompletionHandler:[weakSelf = WeakObjCPtr<WKContentView>(self)] { |
| auto strongSelf = weakSelf.get(); |
| [strongSelf teardownTextIndicatorLayer]; |
| }]; |
| } |
| |
| #if HAVE(UIFINDINTERACTION) |
| |
| - (void)findForWebView:(id)sender |
| { |
| [self.webView._findInteraction presentFindNavigatorShowingReplace:NO]; |
| } |
| |
| - (void)findNextForWebView:(id)sender |
| { |
| [self.webView._findInteraction findNext]; |
| } |
| |
| - (void)findPreviousForWebView:(id)sender |
| { |
| [self.webView._findInteraction findPrevious]; |
| } |
| |
| - (void)performTextSearchWithQueryString:(NSString *)string usingOptions:(_UITextSearchOptions *)options resultAggregator:(id<_UITextSearchAggregator>)aggregator |
| { |
| OptionSet<WebKit::FindOptions> findOptions; |
| findOptions.add(WebKit::FindOptions::ShowOverlay); |
| |
| switch (options.wordMatchMethod) { |
| case _UITextSearchMatchMethodStartsWith: |
| findOptions.add(WebKit::FindOptions::AtWordStarts); |
| break; |
| case _UITextSearchMatchMethodFullWord: |
| findOptions.add({ WebKit::FindOptions::AtWordStarts, WebKit::FindOptions::AtWordEnds }); |
| break; |
| default: |
| break; |
| } |
| |
| if (options.stringCompareOptions & NSCaseInsensitiveSearch) |
| findOptions.add(WebKit::FindOptions::CaseInsensitive); |
| |
| _page->findRectsForStringMatches(string, findOptions, 1000, [string, aggregator = retainPtr(aggregator)](const Vector<WebCore::FloatRect>& rects) { |
| NSUInteger index = 0; |
| for (auto& rect : rects) { |
| WKFoundTextRange *range = [WKFoundTextRange foundTextRangeWithRect:rect index:index]; |
| [aggregator foundRange:range forSearchString:string inDocument:nil]; |
| index++; |
| } |
| |
| [aggregator finishedSearching]; |
| }); |
| } |
| |
| - (void)decorateFoundTextRange:(UITextRange *)range inDocument:(_UITextSearchDocumentIdentifier)document usingStyle:(_UIFoundTextStyle)style |
| { |
| if (![range isKindOfClass:[WKFoundTextRange class]]) |
| return; |
| |
| if (style == _UIFoundTextStyleHighlighted) { |
| _foundHighlightedTextRange = range; |
| WKFoundTextRange *foundRange = (WKFoundTextRange *)range; |
| _page->indicateFindMatch(foundRange.index); |
| } else if (style == _UIFoundTextStyleFound && _foundHighlightedTextRange == range) |
| _page->hideFindIndicator(); |
| } |
| |
| - (void)clearAllDecoratedFoundText |
| { |
| _foundHighlightedTextRange = nil; |
| _page->hideFindUI(); |
| } |
| |
| - (NSInteger)offsetFromPosition:(UITextPosition *)from toPosition:(UITextPosition *)toPosition inDocument:(_UITextSearchDocumentIdentifier)document |
| { |
| return [self offsetFromPosition:from toPosition:toPosition]; |
| } |
| |
| #endif // HAVE(UIFINDINTERACTION) |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| |
| #if USE(QUICK_LOOK) |
| |
| - (void)presentVisualSearchPreviewControllerForImage:(UIImage *)image imageURL:(NSURL *)imageURL title:(NSString *)title imageBounds:(CGRect)imageBounds appearanceActions:(QLPreviewControllerFirstTimeAppearanceActions)appearanceActions |
| { |
| ASSERT(_hasSelectableTextInImage || _hasVisualSearchResults); |
| |
| ASSERT(!_visualSearchPreviewController); |
| _visualSearchPreviewController = adoptNS([PAL::allocQLPreviewControllerInstance() init]); |
| [_visualSearchPreviewController setDelegate:self]; |
| [_visualSearchPreviewController setDataSource:self]; |
| [_visualSearchPreviewController setAppearanceActions:appearanceActions]; |
| [_visualSearchPreviewController setModalPresentationStyle:UIModalPresentationOverFullScreen]; |
| |
| ASSERT(!_visualSearchPreviewImage); |
| _visualSearchPreviewImage = image; |
| |
| ASSERT(!_visualSearchPreviewTitle); |
| _visualSearchPreviewTitle = title; |
| |
| ASSERT(!_visualSearchPreviewImageURL); |
| _visualSearchPreviewImageURL = imageURL; |
| |
| _visualSearchPreviewImageBounds = imageBounds; |
| |
| UIViewController *currentPresentingViewController = [UIViewController _viewControllerForFullScreenPresentationFromView:self]; |
| [currentPresentingViewController presentViewController:_visualSearchPreviewController.get() animated:YES completion:nil]; |
| } |
| |
| #pragma mark - QLPreviewControllerDelegate |
| |
| - (CGRect)previewController:(QLPreviewController *)controller frameForPreviewItem:(id <QLPreviewItem>)item inSourceView:(UIView **)outView |
| { |
| *outView = self; |
| return _visualSearchPreviewImageBounds; |
| } |
| |
| - (UIImage *)previewController:(QLPreviewController *)controller transitionImageForPreviewItem:(id <QLPreviewItem>)item contentRect:(CGRect *)outContentRect |
| { |
| *outContentRect = { CGPointZero, [self convertRect:_visualSearchPreviewImageBounds toView:nil].size }; |
| return _visualSearchPreviewImage.get(); |
| } |
| |
| - (void)previewControllerDidDismiss:(QLPreviewController *)controller |
| { |
| ASSERT(controller == _visualSearchPreviewController); |
| _visualSearchPreviewController.clear(); |
| _visualSearchPreviewImage.clear(); |
| _visualSearchPreviewTitle.clear(); |
| _visualSearchPreviewImageURL.clear(); |
| } |
| |
| #pragma mark - QLPreviewControllerDataSource |
| |
| - (NSInteger)numberOfPreviewItemsInPreviewController:(QLPreviewController *)controller |
| { |
| ASSERT(controller == _visualSearchPreviewController); |
| return 1; |
| } |
| |
| - (id <QLPreviewItem>)previewController:(QLPreviewController *)controller previewItemAtIndex:(NSInteger)index |
| { |
| ASSERT(controller == _visualSearchPreviewController); |
| ASSERT(!index); |
| auto item = adoptNS([PAL::allocQLItemInstance() initWithDataProvider:self contentType:UTTypeTIFF.identifier previewTitle:_visualSearchPreviewTitle.get()]); |
| if ([item respondsToSelector:@selector(setPreviewOptions:)]) { |
| auto previewOptions = adoptNS([[NSMutableDictionary alloc] initWithCapacity:2]); |
| if (_visualSearchPreviewImageURL) |
| [previewOptions setObject:_visualSearchPreviewImageURL.get() forKey:@"imageURL"]; |
| if (auto pageURL = URL { URL { }, _page->currentURL() }; !pageURL.isEmpty()) |
| [previewOptions setObject:pageURL forKey:@"pageURL"]; |
| if ([previewOptions count]) |
| [item setPreviewOptions:previewOptions.get()]; |
| } |
| return item.autorelease(); |
| } |
| |
| #pragma mark - QLPreviewItemDataProvider |
| |
| - (NSData *)provideDataForItem:(QLItem *)item |
| { |
| ASSERT(_visualSearchPreviewImage); |
| |
| auto data = adoptCF(CFDataCreateMutable(NULL, 0)); |
| auto destination = adoptCF(CGImageDestinationCreateWithData(data.get(), (__bridge CFStringRef)UTTypeTIFF.identifier, 1, NULL)); |
| CGImageDestinationAddImage(destination.get(), [_visualSearchPreviewImage CGImage], nil); |
| if (!CGImageDestinationFinalize(destination.get())) |
| return nil; |
| |
| return data.bridgingAutorelease(); |
| } |
| |
| #pragma mark - WKActionSheetAssistantDelegate |
| |
| - (bool)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeShowTextActionForElement:(_WKActivatedElementInfo *)element |
| { |
| return WebKit::isLiveTextAvailableAndEnabled() && _hasSelectableTextInImage; |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant showTextForImage:(UIImage *)image imageURL:(NSURL *)imageURL title:(NSString *)title imageBounds:(CGRect)imageBounds |
| { |
| [self presentVisualSearchPreviewControllerForImage:image imageURL:imageURL title:title imageBounds:imageBounds appearanceActions:QLPreviewControllerFirstTimeAppearanceActionEnableVisualSearchDataDetection]; |
| } |
| |
| - (bool)actionSheetAssistant:(WKActionSheetAssistant *)assistant shouldIncludeLookUpImageActionForElement:(_WKActivatedElementInfo *)element |
| { |
| return WebKit::isLiveTextAvailableAndEnabled() && _hasVisualSearchResults; |
| } |
| |
| - (void)actionSheetAssistant:(WKActionSheetAssistant *)assistant lookUpImage:(UIImage *)image imageURL:(NSURL *)imageURL title:(NSString *)title imageBounds:(CGRect)imageBounds |
| { |
| [self presentVisualSearchPreviewControllerForImage:image imageURL:imageURL title:title imageBounds:imageBounds appearanceActions:QLPreviewControllerFirstTimeAppearanceActionEnableVisualSearchMode]; |
| } |
| |
| #endif // USE(QUICK_LOOK) |
| |
| #pragma mark - Image Extraction |
| |
| - (VKImageAnalyzer *)imageAnalyzer |
| { |
| if (!_imageAnalyzer) |
| _imageAnalyzer = WebKit::createImageAnalyzer(); |
| return _imageAnalyzer.get(); |
| } |
| |
| - (BOOL)hasPendingImageAnalysisRequest |
| { |
| return !!_pendingImageAnalysisRequestIdentifier; |
| } |
| |
| - (void)_setUpImageAnalysis |
| { |
| if (!WebKit::isLiveTextAvailableAndEnabled()) |
| return; |
| |
| _pendingImageAnalysisRequestIdentifier = std::nullopt; |
| _isProceedingWithTextSelectionInImage = NO; |
| _elementPendingImageAnalysis = std::nullopt; |
| _imageAnalysisGestureRecognizer = adoptNS([[WKImageAnalysisGestureRecognizer alloc] initWithImageAnalysisGestureDelegate:self]); |
| [self addGestureRecognizer:_imageAnalysisGestureRecognizer.get()]; |
| _imageAnalysisTimeoutGestureRecognizer = adoptNS([[UILongPressGestureRecognizer alloc] initWithTarget:self action:@selector(imageAnalysisGestureDidTimeOut:)]); |
| [_imageAnalysisTimeoutGestureRecognizer setMinimumPressDuration:2.0]; |
| [_imageAnalysisTimeoutGestureRecognizer setName:@"Image analysis timeout"]; |
| [_imageAnalysisTimeoutGestureRecognizer setDelegate:self]; |
| [self addGestureRecognizer:_imageAnalysisTimeoutGestureRecognizer.get()]; |
| #if USE(QUICK_LOOK) |
| _hasSelectableTextInImage = NO; |
| _hasVisualSearchResults = NO; |
| #endif // USE(QUICK_LOOK) |
| #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| _contextMenuForMachineReadableCode.clear(); |
| #endif // USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| } |
| |
| - (void)_tearDownImageAnalysis |
| { |
| if (!WebKit::isLiveTextAvailableAndEnabled()) |
| return; |
| |
| [_imageAnalysisGestureRecognizer setDelegate:nil]; |
| [self removeGestureRecognizer:_imageAnalysisGestureRecognizer.get()]; |
| _imageAnalysisGestureRecognizer = nil; |
| [_imageAnalysisTimeoutGestureRecognizer setDelegate:nil]; |
| [self removeGestureRecognizer:_imageAnalysisTimeoutGestureRecognizer.get()]; |
| _imageAnalysisTimeoutGestureRecognizer = nil; |
| _pendingImageAnalysisRequestIdentifier = std::nullopt; |
| _isProceedingWithTextSelectionInImage = NO; |
| _elementPendingImageAnalysis = std::nullopt; |
| [std::exchange(_imageAnalyzer, nil) cancelAllRequests]; |
| #if USE(QUICK_LOOK) |
| _hasSelectableTextInImage = NO; |
| _hasVisualSearchResults = NO; |
| #endif // USE(QUICK_LOOK) |
| #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| _contextMenuForMachineReadableCode.clear(); |
| #endif // USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| [self _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::No]; |
| } |
| |
| - (void)_cancelImageAnalysis |
| { |
| [_imageAnalyzer cancelAllRequests]; |
| RELEASE_LOG_IF(self.hasPendingImageAnalysisRequest, Images, "Image analysis request %" PRIu64 " cancelled.", _pendingImageAnalysisRequestIdentifier->toUInt64()); |
| _pendingImageAnalysisRequestIdentifier = std::nullopt; |
| _isProceedingWithTextSelectionInImage = NO; |
| _elementPendingImageAnalysis = std::nullopt; |
| } |
| |
| - (RetainPtr<VKImageAnalyzerRequest>)createImageAnalyzerRequest:(VKAnalysisTypes)analysisTypes image:(CGImageRef)image imageURL:(NSURL *)imageURL |
| { |
| auto request = WebKit::createImageAnalyzerRequest(image, analysisTypes); |
| [request setImageURL:imageURL]; |
| [request setPageURL:[NSURL _web_URLWithWTFString:_page->currentURL()]]; |
| return request; |
| } |
| |
| - (RetainPtr<VKImageAnalyzerRequest>)createImageAnalyzerRequest:(VKAnalysisTypes)analysisTypes image:(CGImageRef)image |
| { |
| return [self createImageAnalyzerRequest:analysisTypes image:image imageURL:_positionInformation.imageURL]; |
| } |
| |
| #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| |
| - (void)_updateContextMenuForMachineReadableCodeForImageAnalysis:(VKImageAnalysis *)analysis |
| { |
| analysis.presentingViewControllerForMrcAction = [UIViewController _viewControllerForFullScreenPresentationFromView:self]; |
| _contextMenuForMachineReadableCode = [analysis hasResultsForAnalysisTypes:VKAnalysisTypeMachineReadableCode | VKAnalysisTypeAppClip] ? analysis.mrcMenu : nil; |
| } |
| |
| #endif // USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| |
| - (BOOL)validateImageAnalysisRequestIdentifier:(WebKit::ImageAnalysisRequestIdentifier)identifier |
| { |
| if (_pendingImageAnalysisRequestIdentifier == identifier) |
| return YES; |
| |
| if (!self.hasPendingImageAnalysisRequest) { |
| // Only invoke deferred image analysis blocks if there is no ongoing request; otherwise, this would |
| // cause these blocks to be invoked too early (i.e. in the middle of the new image analysis request). |
| [self _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::No]; |
| } |
| |
| RELEASE_LOG(Images, "Image analysis request %" PRIu64 " invalidated.", identifier.toUInt64()); |
| return NO; |
| } |
| |
| - (void)requestTextRecognition:(NSURL *)imageURL imageData:(const WebKit::ShareableBitmap::Handle&)imageData identifier:(NSString *)identifier completionHandler:(CompletionHandler<void(WebCore::TextRecognitionResult&&)>&&)completion |
| { |
| auto imageBitmap = WebKit::ShareableBitmap::create(imageData); |
| if (!imageBitmap) { |
| completion({ }); |
| return; |
| } |
| |
| auto cgImage = imageBitmap->makeCGImage(); |
| if (!cgImage) { |
| completion({ }); |
| return; |
| } |
| |
| #if ENABLE(IMAGE_ANALYSIS_ENHANCEMENTS) |
| if (identifier.length) |
| return WebKit::requestImageAnalysisWithIdentifier(self.imageAnalyzer, identifier, cgImage.get(), WTFMove(completion)); |
| #else |
| UNUSED_PARAM(identifier); |
| #endif |
| |
| auto request = [self createImageAnalyzerRequest:VKAnalysisTypeText image:cgImage.get()]; |
| [self.imageAnalyzer processRequest:request.get() progressHandler:nil completionHandler:makeBlockPtr([completion = WTFMove(completion)] (VKImageAnalysis *result, NSError *) mutable { |
| completion(WebKit::makeTextRecognitionResult(result)); |
| }).get()]; |
| } |
| |
| #pragma mark - WKImageAnalysisGestureRecognizerDelegate |
| |
| - (void)imageAnalysisGestureDidBegin:(WKImageAnalysisGestureRecognizer *)gestureRecognizer |
| { |
| ASSERT(WebKit::isLiveTextAvailableAndEnabled()); |
| |
| auto requestIdentifier = WebKit::ImageAnalysisRequestIdentifier::generate(); |
| |
| [_imageAnalyzer cancelAllRequests]; |
| _pendingImageAnalysisRequestIdentifier = requestIdentifier; |
| _isProceedingWithTextSelectionInImage = NO; |
| _elementPendingImageAnalysis = std::nullopt; |
| |
| #if USE(QUICK_LOOK) |
| _hasSelectableTextInImage = NO; |
| _hasVisualSearchResults = NO; |
| #endif // USE(QUICK_LOOK) |
| |
| #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| _contextMenuForMachineReadableCode.clear(); |
| #endif // USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| |
| WebKit::InteractionInformationRequest request { WebCore::roundedIntPoint([gestureRecognizer locationInView:self]) }; |
| request.includeImageData = true; |
| [self doAfterPositionInformationUpdate:[requestIdentifier = WTFMove(requestIdentifier), weakSelf = WeakObjCPtr<WKContentView>(self), gestureDeferralToken = WebKit::ImageAnalysisGestureDeferralToken::create(self)] (WebKit::InteractionInformationAtPosition information) mutable { |
| auto strongSelf = weakSelf.get(); |
| if (![strongSelf validateImageAnalysisRequestIdentifier:requestIdentifier]) |
| return; |
| |
| bool shouldAnalyzeImageAtLocation = ([&] { |
| if (!information.isImage && !canAttemptTextRecognitionForNonImageElements(information, strongSelf->_page->preferences())) |
| return false; |
| |
| if (!information.image) |
| return false; |
| |
| if (!information.hostImageOrVideoElementContext) |
| return false; |
| |
| if (information.isAnimatedImage) |
| return false; |
| |
| if (information.isContentEditable) |
| return false; |
| |
| return true; |
| })(); |
| |
| if (!strongSelf->_pendingImageAnalysisRequestIdentifier || !shouldAnalyzeImageAtLocation) { |
| [strongSelf _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::No]; |
| return; |
| } |
| |
| auto cgImage = information.image->makeCGImageCopy(); |
| if (!cgImage) { |
| [strongSelf _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::No]; |
| return; |
| } |
| |
| RELEASE_LOG(Images, "Image analysis preflight gesture initiated (request %" PRIu64 ").", requestIdentifier.toUInt64()); |
| |
| strongSelf->_elementPendingImageAnalysis = information.hostImageOrVideoElementContext; |
| |
| auto requestLocation = information.request.point; |
| WebCore::ElementContext elementContext = *information.hostImageOrVideoElementContext; |
| |
| auto requestForTextSelection = [strongSelf createImageAnalyzerRequest:VKAnalysisTypeText image:cgImage.get()]; |
| auto requestForContextMenu = [strongSelf createImageAnalyzerRequest:VKAnalysisTypeVisualSearch | VKAnalysisTypeMachineReadableCode | VKAnalysisTypeAppClip image:cgImage.get()]; |
| |
| if (information.elementContainsImageOverlay) { |
| [strongSelf _completeImageAnalysisRequestForContextMenu:requestForContextMenu.get() requestIdentifier:requestIdentifier hasTextResults:YES]; |
| return; |
| } |
| |
| auto textAnalysisStartTime = MonotonicTime::now(); |
| [[strongSelf imageAnalyzer] processRequest:requestForTextSelection.get() progressHandler:nil completionHandler:[requestIdentifier = WTFMove(requestIdentifier), weakSelf, elementContext, requestLocation, requestForContextMenu, gestureDeferralToken, textAnalysisStartTime] (VKImageAnalysis *result, NSError *error) mutable { |
| auto strongSelf = weakSelf.get(); |
| if (![strongSelf validateImageAnalysisRequestIdentifier:requestIdentifier]) |
| return; |
| |
| BOOL hasTextResults = [result hasResultsForAnalysisTypes:VKAnalysisTypeText]; |
| RELEASE_LOG(Images, "Image analysis completed in %.0f ms (request %" PRIu64 "; found text? %d)", (MonotonicTime::now() - textAnalysisStartTime).milliseconds(), requestIdentifier.toUInt64(), hasTextResults); |
| |
| strongSelf->_page->updateWithTextRecognitionResult(WebKit::makeTextRecognitionResult(result), elementContext, requestLocation, [requestIdentifier = WTFMove(requestIdentifier), weakSelf, hasTextResults, requestForContextMenu, gestureDeferralToken] (WebKit::TextRecognitionUpdateResult updateResult) mutable { |
| auto strongSelf = weakSelf.get(); |
| if (![strongSelf validateImageAnalysisRequestIdentifier:requestIdentifier]) |
| return; |
| |
| if (updateResult == WebKit::TextRecognitionUpdateResult::Text) { |
| strongSelf->_isProceedingWithTextSelectionInImage = YES; |
| [strongSelf _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::Yes]; |
| return; |
| } |
| |
| gestureDeferralToken->setShouldPreventTextSelection(); |
| |
| if (updateResult == WebKit::TextRecognitionUpdateResult::DataDetector) { |
| [strongSelf _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::No]; |
| return; |
| } |
| |
| [strongSelf _completeImageAnalysisRequestForContextMenu:requestForContextMenu.get() requestIdentifier:requestIdentifier hasTextResults:hasTextResults]; |
| }); |
| }]; |
| } forRequest:request]; |
| } |
| |
| - (void)_completeImageAnalysisRequestForContextMenu:(VKImageAnalyzerRequest *)requestForContextMenu requestIdentifier:(WebKit::ImageAnalysisRequestIdentifier)requestIdentifier hasTextResults:(BOOL)hasTextResults |
| { |
| auto visualSearchAnalysisStartTime = MonotonicTime::now(); |
| [self.imageAnalyzer processRequest:requestForContextMenu progressHandler:nil completionHandler:[requestIdentifier = WTFMove(requestIdentifier), weakSelf = WeakObjCPtr<WKContentView>(self), hasTextResults, visualSearchAnalysisStartTime] (VKImageAnalysis *result, NSError *error) mutable { |
| auto strongSelf = weakSelf.get(); |
| if (![strongSelf validateImageAnalysisRequestIdentifier:requestIdentifier]) |
| return; |
| |
| #if USE(QUICK_LOOK) |
| BOOL hasVisualSearchResults = [result hasResultsForAnalysisTypes:VKAnalysisTypeVisualSearch]; |
| RELEASE_LOG(Images, "Image analysis completed in %.0f ms (request %" PRIu64 "; found visual search results? %d)", (MonotonicTime::now() - visualSearchAnalysisStartTime).milliseconds(), requestIdentifier.toUInt64(), hasVisualSearchResults); |
| #else |
| UNUSED_PARAM(visualSearchAnalysisStartTime); |
| #endif |
| if (!result || error) { |
| [strongSelf _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::No]; |
| return; |
| } |
| |
| #if USE(QUICK_LOOK) |
| strongSelf->_hasSelectableTextInImage = hasTextResults; |
| strongSelf->_hasVisualSearchResults = hasVisualSearchResults; |
| #else |
| UNUSED_PARAM(hasTextResults); |
| #endif |
| #if USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| [strongSelf _updateContextMenuForMachineReadableCodeForImageAnalysis:result]; |
| #endif // USE(UICONTEXTMENU) && ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| [strongSelf _invokeAllActionsToPerformAfterPendingImageAnalysis:WebKit::ProceedWithTextSelectionInImage::No]; |
| }]; |
| } |
| |
| - (void)imageAnalysisGestureDidFail:(WKImageAnalysisGestureRecognizer *)gestureRecognizer |
| { |
| [self _endImageAnalysisGestureDeferral:WebKit::ShouldPreventGestures::No]; |
| } |
| |
| - (void)imageAnalysisGestureDidTimeOut:(WKImageAnalysisGestureRecognizer *)gestureRecognizer |
| { |
| if (!self._shouldUseContextMenus) |
| return; |
| |
| if (self.hasPendingImageAnalysisRequest) |
| return; |
| |
| if (gestureRecognizer.state != UIGestureRecognizerStateBegan) |
| return; |
| |
| auto location = WebCore::roundedIntPoint([gestureRecognizer locationInView:self]); |
| WebKit::InteractionInformationRequest request { location }; |
| request.includeImageData = true; |
| [self doAfterPositionInformationUpdate:[weakSelf = WeakObjCPtr<WKContentView>(self), location] (WebKit::InteractionInformationAtPosition info) { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) |
| return; |
| |
| if (!info.image) |
| return; |
| |
| if ((!info.isImageOverlayText || !info.isSelected) && !strongSelf->_isProceedingWithTextSelectionInImage) |
| return; |
| |
| auto cgImage = info.image->makeCGImageCopy(); |
| if (!cgImage) |
| return; |
| |
| RELEASE_LOG(Images, "Image analysis timeout gesture initiated."); |
| // FIXME: We need to implement some way to cache image analysis results per element, so that we don't end up |
| // making redundant image analysis requests for the same image data. |
| |
| auto visualSearchAnalysisStartTime = MonotonicTime::now(); |
| auto requestForContextMenu = [strongSelf createImageAnalyzerRequest:VKAnalysisTypeVisualSearch | VKAnalysisTypeMachineReadableCode | VKAnalysisTypeAppClip image:cgImage.get()]; |
| [[strongSelf imageAnalyzer] processRequest:requestForContextMenu.get() progressHandler:nil completionHandler:[weakSelf, location, visualSearchAnalysisStartTime] (VKImageAnalysis *result, NSError *error) { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) |
| return; |
| |
| #if USE(QUICK_LOOK) |
| BOOL hasVisualSearchResults = [result hasResultsForAnalysisTypes:VKAnalysisTypeVisualSearch]; |
| RELEASE_LOG(Images, "Image analysis completed in %.0f ms (found visual search results? %d)", (MonotonicTime::now() - visualSearchAnalysisStartTime).milliseconds(), hasVisualSearchResults); |
| strongSelf->_hasSelectableTextInImage = YES; |
| strongSelf->_hasVisualSearchResults = hasVisualSearchResults; |
| #else |
| UNUSED_PARAM(visualSearchAnalysisStartTime); |
| #endif |
| |
| #if USE(UICONTEXTMENU) |
| #if ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| [strongSelf _updateContextMenuForMachineReadableCodeForImageAnalysis:result]; |
| #endif // ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| strongSelf->_contextMenuWasTriggeredByImageAnalysisTimeout = YES; |
| [strongSelf presentContextMenu:strongSelf->_contextMenuInteraction.get() atLocation:location]; |
| #else |
| UNUSED_PARAM(location); |
| #endif // USE(UICONTEXTMENU) |
| }]; |
| } forRequest:request]; |
| } |
| |
| - (void)captureTextFromCameraForWebView:(id)sender |
| { |
| [super captureTextFromCamera:sender]; |
| } |
| |
| #endif // ENABLE(IMAGE_ANALYSIS) |
| |
| @end |
| |
| @implementation WKContentView (WKTesting) |
| |
| - (void)_doAfterReceivingEditDragSnapshotForTesting:(dispatch_block_t)action |
| { |
| #if ENABLE(DRAG_SUPPORT) |
| ASSERT(!_actionToPerformAfterReceivingEditDragSnapshot); |
| if (_waitingForEditDragSnapshot) { |
| _actionToPerformAfterReceivingEditDragSnapshot = action; |
| return; |
| } |
| #endif |
| action(); |
| } |
| |
| #if !PLATFORM(WATCHOS) |
| - (WKDateTimeInputControl *)dateTimeInputControl |
| { |
| if ([_inputPeripheral isKindOfClass:WKDateTimeInputControl.class]) |
| return (WKDateTimeInputControl *)_inputPeripheral.get(); |
| return nil; |
| } |
| #endif |
| |
| - (WKFormSelectControl *)selectControl |
| { |
| if ([_inputPeripheral isKindOfClass:WKFormSelectControl.class]) |
| return (WKFormSelectControl *)_inputPeripheral.get(); |
| return nil; |
| } |
| |
| #if ENABLE(DRAG_SUPPORT) |
| |
| - (BOOL)isAnimatingDragCancel |
| { |
| return _isAnimatingDragCancel; |
| } |
| |
| #endif // ENABLE(DRAG_SUPPORT) |
| |
| - (void)_simulateTextEntered:(NSString *)text |
| { |
| #if HAVE(PEPPER_UI_CORE) |
| if ([_presentedFullScreenInputViewController isKindOfClass:[WKTextInputListViewController class]]) { |
| [(WKTextInputListViewController *)_presentedFullScreenInputViewController.get() enterText:text]; |
| return; |
| } |
| |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| if (_presentedQuickboardController) { |
| id <PUICQuickboardControllerDelegate> delegate = [_presentedQuickboardController delegate]; |
| ASSERT(delegate == self); |
| auto string = adoptNS([[NSAttributedString alloc] initWithString:text]); |
| [delegate quickboardController:_presentedQuickboardController.get() textInputValueDidChange:string.get()]; |
| return; |
| } |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| #endif // HAVE(PEPPER_UI_CORE) |
| |
| [self insertText:text]; |
| } |
| |
| - (void)_simulateElementAction:(_WKElementActionType)actionType atLocation:(CGPoint)location |
| { |
| _layerTreeTransactionIdAtLastInteractionStart = downcast<WebKit::RemoteLayerTreeDrawingAreaProxy>(*_page->drawingArea()).lastCommittedLayerTreeTransactionID(); |
| [self doAfterPositionInformationUpdate:[actionType, self, protectedSelf = retainPtr(self)] (WebKit::InteractionInformationAtPosition info) { |
| _WKElementAction *action = [_WKElementAction _elementActionWithType:actionType assistant:_actionSheetAssistant.get()]; |
| _WKActivatedElementInfo *elementInfo = [_WKActivatedElementInfo activatedElementInfoWithInteractionInformationAtPosition:info userInfo:nil]; |
| [action runActionWithElementInfo:elementInfo]; |
| } forRequest:WebKit::InteractionInformationRequest(WebCore::roundedIntPoint(location))]; |
| } |
| |
| - (void)_simulateLongPressActionAtLocation:(CGPoint)location |
| { |
| RetainPtr<WKContentView> protectedSelf = self; |
| [self doAfterPositionInformationUpdate:[protectedSelf] (WebKit::InteractionInformationAtPosition) { |
| if (SEL action = [protectedSelf _actionForLongPress]) |
| [protectedSelf performSelector:action]; |
| } forRequest:WebKit::InteractionInformationRequest(WebCore::roundedIntPoint(location))]; |
| } |
| |
| - (void)selectFormAccessoryPickerRow:(NSInteger)rowIndex |
| { |
| #if HAVE(PEPPER_UI_CORE) |
| if ([_presentedFullScreenInputViewController isKindOfClass:[WKSelectMenuListViewController class]]) |
| [(WKSelectMenuListViewController *)_presentedFullScreenInputViewController.get() selectItemAtIndex:rowIndex]; |
| #else |
| if ([_inputPeripheral isKindOfClass:[WKFormSelectControl class]]) |
| [(WKFormSelectControl *)_inputPeripheral selectRow:rowIndex inComponent:0 extendingSelection:NO]; |
| #endif |
| } |
| |
| - (BOOL)selectFormAccessoryHasCheckedItemAtRow:(long)rowIndex |
| { |
| #if !HAVE(PEPPER_UI_CORE) |
| if ([_inputPeripheral isKindOfClass:[WKFormSelectControl self]]) |
| return [(WKFormSelectControl *)_inputPeripheral selectFormAccessoryHasCheckedItemAtRow:rowIndex]; |
| #endif |
| return NO; |
| } |
| |
| - (void)setSelectedColorForColorPicker:(UIColor *)color |
| { |
| if ([_inputPeripheral isKindOfClass:[WKFormColorControl class]]) |
| [(WKFormColorControl *)_inputPeripheral selectColor:color]; |
| } |
| |
| - (NSString *)textContentTypeForTesting |
| { |
| #if HAVE(PEPPER_UI_CORE) |
| if ([_presentedFullScreenInputViewController isKindOfClass:[WKTextInputListViewController class]]) |
| return [(WKTextInputListViewController *)_presentedFullScreenInputViewController textInputContext].textContentType; |
| #if HAVE(QUICKBOARD_CONTROLLER) |
| if (_presentedQuickboardController) |
| return [_presentedQuickboardController textInputContext].textContentType; |
| #endif // HAVE(QUICKBOARD_CONTROLLER) |
| #endif // HAVE(PEPPER_UI_CORE) |
| return self.textInputTraits.textContentType; |
| } |
| |
| - (NSString *)selectFormPopoverTitle |
| { |
| if (![_inputPeripheral isKindOfClass:[WKFormSelectControl class]]) |
| return nil; |
| |
| return [(WKFormSelectControl *)_inputPeripheral selectFormPopoverTitle]; |
| } |
| |
| - (NSString *)formInputLabel |
| { |
| #if HAVE(PEPPER_UI_CORE) |
| return [self inputLabelTextForViewController:_presentedFullScreenInputViewController.get()]; |
| #else |
| return nil; |
| #endif |
| } |
| |
| - (void)setTimePickerValueToHour:(NSInteger)hour minute:(NSInteger)minute |
| { |
| #if HAVE(PEPPER_UI_CORE) |
| if ([_presentedFullScreenInputViewController isKindOfClass:[WKTimePickerViewController class]]) |
| [(WKTimePickerViewController *)_presentedFullScreenInputViewController.get() setHour:hour minute:minute]; |
| #else |
| if ([_inputPeripheral isKindOfClass:[WKDateTimeInputControl class]]) |
| [(WKDateTimeInputControl *)_inputPeripheral.get() setTimePickerHour:hour minute:minute]; |
| #endif |
| } |
| |
| - (double)timePickerValueHour |
| { |
| #if !PLATFORM(WATCHOS) |
| if ([_inputPeripheral isKindOfClass:[WKDateTimeInputControl class]]) |
| return [(WKDateTimeInputControl *)_inputPeripheral.get() timePickerValueHour]; |
| #endif |
| return -1; |
| } |
| |
| - (double)timePickerValueMinute |
| { |
| #if !PLATFORM(WATCHOS) |
| if ([_inputPeripheral isKindOfClass:[WKDateTimeInputControl class]]) |
| return [(WKDateTimeInputControl *)_inputPeripheral.get() timePickerValueMinute]; |
| #endif |
| return -1; |
| } |
| |
| - (NSDictionary *)_contentsOfUserInterfaceItem:(NSString *)userInterfaceItem |
| { |
| if ([userInterfaceItem isEqualToString:@"actionSheet"]) |
| return @{ userInterfaceItem: [_actionSheetAssistant currentlyAvailableActionTitles] }; |
| |
| #if HAVE(LINK_PREVIEW) |
| if ([userInterfaceItem isEqualToString:@"contextMenu"]) { |
| if (self._shouldUseContextMenus) { |
| return @{ userInterfaceItem: @{ |
| @"url": _positionInformation.url.isValid() ? WTF::userVisibleString(_positionInformation.url) : @"", |
| @"isLink": [NSNumber numberWithBool:_positionInformation.isLink], |
| @"isImage": [NSNumber numberWithBool:_positionInformation.isImage], |
| @"imageURL": _positionInformation.imageURL.isValid() ? WTF::userVisibleString(_positionInformation.imageURL) : @"" |
| } }; |
| } |
| NSString *url = [_previewItemController previewData][UIPreviewDataLink]; |
| return @{ userInterfaceItem: @{ |
| @"url": url, |
| @"isLink": [NSNumber numberWithBool:_positionInformation.isLink], |
| @"isImage": [NSNumber numberWithBool:_positionInformation.isImage], |
| @"imageURL": _positionInformation.imageURL.isValid() ? WTF::userVisibleString(_positionInformation.imageURL) : @"" |
| } }; |
| } |
| #endif |
| |
| #if ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) && USE(UICONTEXTMENU) |
| if ([userInterfaceItem isEqualToString:@"mediaControlsContextMenu"]) |
| return @{ userInterfaceItem: [_actionSheetAssistant currentlyAvailableMediaControlsContextMenuItems] }; |
| #endif // ENABLE(MEDIA_CONTROLS_CONTEXT_MENUS) && USE(UICONTEXTMENU) |
| |
| if ([userInterfaceItem isEqualToString:@"fileUploadPanelMenu"]) { |
| if (!_fileUploadPanel) |
| return @{ userInterfaceItem: @[] }; |
| return @{ userInterfaceItem: [_fileUploadPanel currentAvailableActionTitles] }; |
| } |
| |
| return nil; |
| } |
| |
| - (void)_dismissContactPickerWithContacts:(NSArray *)contacts |
| { |
| #if HAVE(CONTACTSUI) |
| [_contactPicker dismissWithContacts:contacts]; |
| #endif |
| } |
| |
| #if ENABLE(DATALIST_ELEMENT) |
| - (void)_selectDataListOption:(NSInteger)optionIndex |
| { |
| [_dataListSuggestionsControl didSelectOptionAtIndex:optionIndex]; |
| } |
| |
| - (void)_setDataListSuggestionsControl:(WKDataListSuggestionsControl *)control |
| { |
| _dataListSuggestionsControl = control; |
| } |
| |
| - (BOOL)isShowingDataListSuggestions |
| { |
| return [_dataListSuggestionsControl isShowingSuggestions]; |
| } |
| #endif |
| |
| - (UIWKTextInteractionAssistant *)textInteractionAssistant |
| { |
| return _textInteractionAssistant.get(); |
| } |
| |
| @end |
| |
| #if HAVE(LINK_PREVIEW) |
| |
| static NSString *previewIdentifierForElementAction(_WKElementAction *action) |
| { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| switch (action.type) { |
| case _WKElementActionTypeOpen: |
| return WKPreviewActionItemIdentifierOpen; |
| case _WKElementActionTypeCopy: |
| return WKPreviewActionItemIdentifierCopy; |
| #if !defined(TARGET_OS_IOS) || TARGET_OS_IOS |
| case _WKElementActionTypeAddToReadingList: |
| return WKPreviewActionItemIdentifierAddToReadingList; |
| #endif |
| case _WKElementActionTypeShare: |
| return WKPreviewActionItemIdentifierShare; |
| default: |
| return nil; |
| } |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| ASSERT_NOT_REACHED(); |
| return nil; |
| } |
| |
| @implementation WKContentView (WKInteractionPreview) |
| |
| - (void)_registerPreview |
| { |
| if (!self.webView.allowsLinkPreview) |
| return; |
| |
| #if USE(UICONTEXTMENU) |
| if (self._shouldUseContextMenus) { |
| _contextMenuInteraction = adoptNS([[UIContextMenuInteraction alloc] initWithDelegate:self]); |
| _contextMenuHasRequestedLegacyData = NO; |
| [self addInteraction:_contextMenuInteraction.get()]; |
| |
| if (id<_UIClickInteractionDriving> driver = self.webView.configuration._clickInteractionDriverForTesting) |
| [_contextMenuInteraction presentationInteraction].overrideDrivers = @[driver]; |
| return; |
| } |
| #endif |
| |
| _previewItemController = adoptNS([[UIPreviewItemController alloc] initWithView:self]); |
| [_previewItemController setDelegate:self]; |
| _previewGestureRecognizer = _previewItemController.get().presentationGestureRecognizer; |
| if ([_previewItemController respondsToSelector:@selector(presentationSecondaryGestureRecognizer)]) |
| _previewSecondaryGestureRecognizer = _previewItemController.get().presentationSecondaryGestureRecognizer; |
| } |
| |
| - (void)_unregisterPreview |
| { |
| #if USE(UICONTEXTMENU) |
| if (self._shouldUseContextMenus) { |
| if (!_contextMenuInteraction) |
| return; |
| |
| [self removeInteraction:_contextMenuInteraction.get()]; |
| _contextMenuInteraction = nil; |
| return; |
| } |
| #endif |
| |
| [_previewItemController setDelegate:nil]; |
| _previewGestureRecognizer = nil; |
| _previewSecondaryGestureRecognizer = nil; |
| _previewItemController = nil; |
| } |
| |
| #if USE(UICONTEXTMENU) |
| |
| static bool needsDeprecatedPreviewAPI(id<WKUIDelegate> delegate) |
| { |
| // FIXME: Replace these with booleans in UIDelegate.h. |
| // Note that we explicitly do not test for @selector(_webView:contextMenuDidEndForElement:) |
| // and @selector(webView:contextMenuWillPresentForElement) since the methods are used by MobileSafari |
| // to manage state despite the app not moving to the new API. |
| |
| return delegate |
| && ![delegate respondsToSelector:@selector(_webView:contextMenuConfigurationForElement:completionHandler:)] |
| && ![delegate respondsToSelector:@selector(webView:contextMenuConfigurationForElement:completionHandler:)] |
| && ![delegate respondsToSelector:@selector(_webView:contextMenuForElement:willCommitWithAnimator:)] |
| && ![delegate respondsToSelector:@selector(webView:contextMenuForElement:willCommitWithAnimator:)] |
| && ![delegate respondsToSelector:@selector(_webView:contextMenuWillPresentForElement:)] |
| && ![delegate respondsToSelector:@selector(webView:contextMenuDidEndForElement:)] |
| && ([delegate respondsToSelector:@selector(webView:shouldPreviewElement:)] |
| || [delegate respondsToSelector:@selector(webView:previewingViewControllerForElement:defaultActions:)] |
| || [delegate respondsToSelector:@selector(webView:commitPreviewingViewController:)] |
| || [delegate respondsToSelector:@selector(_webView:previewViewControllerForURL:defaultActions:elementInfo:)] |
| || [delegate respondsToSelector:@selector(_webView:previewViewControllerForURL:)] |
| || [delegate respondsToSelector:@selector(_webView:previewViewControllerForImage:alternateURL:defaultActions:elementInfo:)]); |
| } |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| static NSArray<WKPreviewAction *> *wkLegacyPreviewActionsFromElementActions(NSArray<_WKElementAction *> *elementActions, _WKActivatedElementInfo *elementInfo) |
| { |
| NSMutableArray<WKPreviewAction *> *previewActions = [NSMutableArray arrayWithCapacity:[elementActions count]]; |
| for (_WKElementAction *elementAction in elementActions) { |
| WKPreviewAction *previewAction = [WKPreviewAction actionWithIdentifier:previewIdentifierForElementAction(elementAction) title:elementAction.title style:UIPreviewActionStyleDefault handler:^(UIPreviewAction *, UIViewController *) { |
| [elementAction runActionWithElementInfo:elementInfo]; |
| }]; |
| previewAction.image = [_WKElementAction imageForElementActionType:elementAction.type]; |
| [previewActions addObject:previewAction]; |
| } |
| return previewActions; |
| } |
| |
| static UIAction *uiActionForLegacyPreviewAction(UIPreviewAction *previewAction, UIViewController *previewViewController) |
| { |
| // UIPreviewActionItem.image is SPI, so no external clients will be able |
| // to provide glyphs for actions <rdar://problem/50151855>. |
| // However, they should migrate to the new API. |
| |
| return [UIAction actionWithTitle:previewAction.title image:previewAction.image identifier:nil handler:^(UIAction *action) { |
| previewAction.handler(previewAction, previewViewController); |
| }]; |
| } |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| static NSArray<UIMenuElement *> *menuElementsFromLegacyPreview(UIViewController *previewViewController) |
| { |
| if (!previewViewController) |
| return nil; |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| NSArray<id<UIPreviewActionItem>> *previewActions = previewViewController.previewActionItems; |
| if (!previewActions || ![previewActions count]) |
| return nil; |
| |
| auto actions = [NSMutableArray arrayWithCapacity:previewActions.count]; |
| |
| for (UIPreviewAction *previewAction in previewActions) |
| [actions addObject:uiActionForLegacyPreviewAction(previewAction, previewViewController)]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| return actions; |
| } |
| |
| static NSMutableArray<UIMenuElement *> *menuElementsFromDefaultActions(const RetainPtr<NSArray>& defaultElementActions, RetainPtr<_WKActivatedElementInfo> elementInfo) |
| { |
| if (!defaultElementActions || !defaultElementActions.get().count) |
| return nil; |
| |
| auto actions = [NSMutableArray arrayWithCapacity:defaultElementActions.get().count]; |
| |
| for (_WKElementAction *elementAction in defaultElementActions.get()) |
| [actions addObject:[elementAction uiActionForElementInfo:elementInfo.get()]]; |
| |
| return actions; |
| } |
| |
| static UIMenu *menuFromLegacyPreviewOrDefaultActions(UIViewController *previewViewController, const RetainPtr<NSArray>& defaultElementActions, RetainPtr<_WKActivatedElementInfo> elementInfo, NSString *title = nil) |
| { |
| auto actions = menuElementsFromLegacyPreview(previewViewController); |
| if (!actions) |
| actions = menuElementsFromDefaultActions(defaultElementActions, elementInfo); |
| |
| return [UIMenu menuWithTitle:title children:actions]; |
| } |
| |
| - (void)assignLegacyDataForContextMenuInteraction |
| { |
| ASSERT(!_contextMenuHasRequestedLegacyData); |
| if (_contextMenuHasRequestedLegacyData) |
| return; |
| _contextMenuHasRequestedLegacyData = YES; |
| |
| if (!_webView) |
| return; |
| auto uiDelegate = static_cast<id<WKUIDelegatePrivate>>(self.webView.UIDelegate); |
| if (!uiDelegate) |
| return; |
| |
| const auto& url = _positionInformation.url; |
| |
| _page->startInteractionWithPositionInformation(_positionInformation); |
| |
| RetainPtr<UIViewController> previewViewController; |
| |
| auto elementInfo = adoptNS([[_WKActivatedElementInfo alloc] _initWithInteractionInformationAtPosition:_positionInformation userInfo:nil]); |
| |
| ASSERT_IMPLIES(_positionInformation.isImage, _positionInformation.image); |
| if (_positionInformation.isLink) { |
| _longPressCanClick = NO; |
| |
| RetainPtr<NSArray<_WKElementAction *>> defaultActionsFromAssistant = [_actionSheetAssistant defaultActionsForLinkSheet:elementInfo.get()]; |
| |
| // FIXME: Animated images go here. |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate respondsToSelector:@selector(webView:previewingViewControllerForElement:defaultActions:)]) { |
| auto defaultActions = wkLegacyPreviewActionsFromElementActions(defaultActionsFromAssistant.get(), elementInfo.get()); |
| auto previewElementInfo = adoptNS([[WKPreviewElementInfo alloc] _initWithLinkURL:url]); |
| // FIXME: Clients using this legacy API will always show their previewViewController and ignore _showLinkPreviews. |
| previewViewController = [uiDelegate webView:self.webView previewingViewControllerForElement:previewElementInfo.get() defaultActions:defaultActions]; |
| } else if ([uiDelegate respondsToSelector:@selector(_webView:previewViewControllerForURL:defaultActions:elementInfo:)]) |
| previewViewController = [uiDelegate _webView:self.webView previewViewControllerForURL:url defaultActions:defaultActionsFromAssistant.get() elementInfo:elementInfo.get()]; |
| else if ([uiDelegate respondsToSelector:@selector(_webView:previewViewControllerForURL:)]) |
| previewViewController = [uiDelegate _webView:self.webView previewViewControllerForURL:url]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| // Previously, UIPreviewItemController would detect the case where there was no previewViewController |
| // and create one. We need to replicate this code for the new API. |
| if (!previewViewController || [(NSURL *)url iTunesStoreURL]) { |
| auto ddContextMenuActionClass = getDDContextMenuActionClass(); |
| BEGIN_BLOCK_OBJC_EXCEPTIONS |
| NSDictionary *context = [self dataDetectionContextForPositionInformation:_positionInformation]; |
| RetainPtr<UIContextMenuConfiguration> dataDetectorsResult = [ddContextMenuActionClass contextMenuConfigurationForURL:url identifier:_positionInformation.dataDetectorIdentifier selectedText:self.selectedText results:_positionInformation.dataDetectorResults.get() inView:self context:context menuIdentifier:nil]; |
| if (_showLinkPreviews && dataDetectorsResult && dataDetectorsResult.get().previewProvider) |
| _contextMenuLegacyPreviewController = dataDetectorsResult.get().previewProvider(); |
| if (dataDetectorsResult && dataDetectorsResult.get().actionProvider) { |
| auto menuElements = menuElementsFromDefaultActions(defaultActionsFromAssistant, elementInfo); |
| _contextMenuLegacyMenu = dataDetectorsResult.get().actionProvider(menuElements); |
| } |
| END_BLOCK_OBJC_EXCEPTIONS |
| return; |
| } |
| |
| _contextMenuLegacyMenu = menuFromLegacyPreviewOrDefaultActions(previewViewController.get(), defaultActionsFromAssistant, elementInfo); |
| |
| } else if (_positionInformation.isImage && _positionInformation.image) { |
| NSURL *nsURL = (NSURL *)url; |
| RetainPtr<NSDictionary> imageInfo; |
| auto cgImage = _positionInformation.image->makeCGImageCopy(); |
| auto uiImage = adoptNS([[UIImage alloc] initWithCGImage:cgImage.get()]); |
| |
| if ([uiDelegate respondsToSelector:@selector(_webView:alternateURLFromImage:userInfo:)]) { |
| NSDictionary *userInfo; |
| nsURL = [uiDelegate _webView:self.webView alternateURLFromImage:uiImage.get() userInfo:&userInfo]; |
| imageInfo = userInfo; |
| } |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate respondsToSelector:@selector(_webView:willPreviewImageWithURL:)]) |
| [uiDelegate _webView:self.webView willPreviewImageWithURL:_positionInformation.imageURL]; |
| |
| RetainPtr<NSArray<_WKElementAction *>> defaultActionsFromAssistant = [_actionSheetAssistant defaultActionsForImageSheet:elementInfo.get()]; |
| |
| if (imageInfo && [uiDelegate respondsToSelector:@selector(_webView:previewViewControllerForImage:alternateURL:defaultActions:elementInfo:)]) |
| previewViewController = [uiDelegate _webView:self.webView previewViewControllerForImage:uiImage.get() alternateURL:nsURL defaultActions:defaultActionsFromAssistant.get() elementInfo:elementInfo.get()]; |
| else |
| previewViewController = adoptNS([[WKImagePreviewViewController alloc] initWithCGImage:cgImage defaultActions:defaultActionsFromAssistant.get() elementInfo:elementInfo.get()]); |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| _contextMenuLegacyMenu = menuFromLegacyPreviewOrDefaultActions(previewViewController.get(), defaultActionsFromAssistant, elementInfo, _positionInformation.title); |
| } |
| |
| _contextMenuLegacyPreviewController = WTFMove(previewViewController); |
| } |
| |
| - (UIContextMenuConfiguration *)contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location |
| { |
| // Required to conform to UIContextMenuInteractionDelegate, but SPI version should be called instead. |
| ASSERT_NOT_REACHED(); |
| return nil; |
| } |
| |
| - (void)_contextMenuInteraction:(UIContextMenuInteraction *)interaction configurationForMenuAtLocation:(CGPoint)location completion:(void(^)(UIContextMenuConfiguration *))completion |
| { |
| #if ENABLE(IMAGE_ANALYSIS) |
| BOOL triggeredByImageAnalysisTimeout = std::exchange(_contextMenuWasTriggeredByImageAnalysisTimeout, NO); |
| #else |
| BOOL triggeredByImageAnalysisTimeout = NO; |
| #endif |
| |
| if (!_webView) |
| return completion(nil); |
| |
| if (!self.webView.configuration._longPressActionsEnabled) |
| return completion(nil); |
| |
| auto getConfigurationAndContinue = [weakSelf = WeakObjCPtr<WKContentView>(self), interaction = retainPtr(interaction), completion = makeBlockPtr(completion), triggeredByImageAnalysisTimeout] (WebKit::ProceedWithTextSelectionInImage proceedWithTextSelectionInImage) { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf || proceedWithTextSelectionInImage == WebKit::ProceedWithTextSelectionInImage::Yes) { |
| completion(nil); |
| return; |
| } |
| |
| [strongSelf->_webView _didShowContextMenu]; |
| |
| strongSelf->_showLinkPreviews = true; |
| if (NSNumber *value = [[NSUserDefaults standardUserDefaults] objectForKey:webkitShowLinkPreviewsPreferenceKey]) |
| strongSelf->_showLinkPreviews = value.boolValue; |
| |
| WebKit::InteractionInformationRequest request { WebCore::roundedIntPoint([interaction locationInView:strongSelf.get()]) }; |
| request.includeSnapshot = true; |
| request.includeLinkIndicator = true; |
| request.disallowUserAgentShadowContent = triggeredByImageAnalysisTimeout; |
| request.linkIndicatorShouldHaveLegacyMargins = ![strongSelf _shouldUseContextMenus]; |
| |
| [strongSelf doAfterPositionInformationUpdate:[weakSelf = WeakObjCPtr<WKContentView>(strongSelf.get()), completion] (WebKit::InteractionInformationAtPosition) { |
| if (auto strongSelf = weakSelf.get()) |
| [strongSelf continueContextMenuInteraction:completion.get()]; |
| else |
| completion(nil); |
| } forRequest:request]; |
| }; |
| |
| #if ENABLE(IMAGE_ANALYSIS) |
| [self _doAfterPendingImageAnalysis:getConfigurationAndContinue]; |
| #else |
| getConfigurationAndContinue(WebKit::ProceedWithTextSelectionInImage::No); |
| #endif |
| } |
| |
| - (void)continueContextMenuInteraction:(void(^)(UIContextMenuConfiguration *))continueWithContextMenuConfiguration |
| { |
| if (!self.window) |
| return continueWithContextMenuConfiguration(nil); |
| |
| if (!_positionInformation.touchCalloutEnabled) |
| return continueWithContextMenuConfiguration(nil); |
| |
| if (!_positionInformation.isLink && !_positionInformation.isImage && !_positionInformation.isAttachment && !self.positionInformationHasImageOverlayDataDetector) |
| return continueWithContextMenuConfiguration(nil); |
| |
| URL linkURL = _positionInformation.url; |
| |
| if (_positionInformation.isLink && linkURL.isEmpty()) |
| return continueWithContextMenuConfiguration(nil); |
| |
| auto uiDelegate = static_cast<id<WKUIDelegatePrivate>>(self.webView.UIDelegate); |
| |
| if (needsDeprecatedPreviewAPI(uiDelegate)) { |
| if (_positionInformation.isLink) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate respondsToSelector:@selector(webView:shouldPreviewElement:)]) { |
| auto previewElementInfo = adoptNS([[WKPreviewElementInfo alloc] _initWithLinkURL:linkURL]); |
| if (![uiDelegate webView:self.webView shouldPreviewElement:previewElementInfo.get()]) |
| return continueWithContextMenuConfiguration(nil); |
| } |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| // FIXME: Support JavaScript urls here. But make sure they don't show a preview. |
| // <rdar://problem/50572283> |
| if (!linkURL.protocolIsInHTTPFamily()) { |
| #if ENABLE(DATA_DETECTION) |
| if (!WebCore::DataDetection::canBePresentedByDataDetectors(linkURL)) |
| return continueWithContextMenuConfiguration(nil); |
| #endif |
| } |
| } |
| |
| _contextMenuLegacyPreviewController = nullptr; |
| _contextMenuLegacyMenu = nullptr; |
| _contextMenuHasRequestedLegacyData = NO; |
| _contextMenuActionProviderDelegateNeedsOverride = NO; |
| |
| UIContextMenuActionProvider actionMenuProvider = [weakSelf = WeakObjCPtr<WKContentView>(self)] (NSArray<UIMenuElement *> *) -> UIMenu * { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) |
| return nil; |
| if (!strongSelf->_contextMenuHasRequestedLegacyData) |
| [strongSelf assignLegacyDataForContextMenuInteraction]; |
| |
| return strongSelf->_contextMenuLegacyMenu.get(); |
| }; |
| |
| UIContextMenuContentPreviewProvider contentPreviewProvider = [weakSelf = WeakObjCPtr<WKContentView>(self)] () -> UIViewController * { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) |
| return nil; |
| if (!strongSelf->_contextMenuHasRequestedLegacyData) |
| [strongSelf assignLegacyDataForContextMenuInteraction]; |
| |
| return strongSelf->_contextMenuLegacyPreviewController.get(); |
| }; |
| |
| _page->startInteractionWithPositionInformation(_positionInformation); |
| |
| continueWithContextMenuConfiguration([UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:contentPreviewProvider actionProvider:actionMenuProvider]); |
| return; |
| } |
| |
| #if ENABLE(DATA_DETECTION) |
| if ([(NSURL *)linkURL iTunesStoreURL]) { |
| [self continueContextMenuInteractionWithDataDetectors:continueWithContextMenuConfiguration]; |
| return; |
| } |
| #endif |
| |
| auto completionBlock = makeBlockPtr([continueWithContextMenuConfiguration = makeBlockPtr(continueWithContextMenuConfiguration), linkURL = WTFMove(linkURL), weakSelf = WeakObjCPtr<WKContentView>(self)] (UIContextMenuConfiguration *configurationFromWKUIDelegate) mutable { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) { |
| continueWithContextMenuConfiguration(nil); |
| return; |
| } |
| |
| if (configurationFromWKUIDelegate) { |
| strongSelf->_page->startInteractionWithPositionInformation(strongSelf->_positionInformation); |
| strongSelf->_contextMenuActionProviderDelegateNeedsOverride = YES; |
| continueWithContextMenuConfiguration(configurationFromWKUIDelegate); |
| return; |
| } |
| |
| bool canShowHTTPLinkOrDataDetectorPreview = ([&] { |
| if (linkURL.protocolIsInHTTPFamily()) |
| return true; |
| |
| if (WebCore::DataDetection::canBePresentedByDataDetectors(linkURL)) |
| return true; |
| |
| if ([strongSelf positionInformationHasImageOverlayDataDetector]) |
| return true; |
| |
| return false; |
| })(); |
| |
| ASSERT_IMPLIES(strongSelf->_positionInformation.isImage, strongSelf->_positionInformation.image); |
| if (strongSelf->_positionInformation.isImage && strongSelf->_positionInformation.image && !canShowHTTPLinkOrDataDetectorPreview) { |
| auto cgImage = strongSelf->_positionInformation.image->makeCGImageCopy(); |
| |
| strongSelf->_contextMenuActionProviderDelegateNeedsOverride = NO; |
| |
| auto elementInfo = adoptNS([[_WKActivatedElementInfo alloc] _initWithInteractionInformationAtPosition:strongSelf->_positionInformation userInfo:nil]); |
| |
| UIContextMenuActionProvider actionMenuProvider = [weakSelf, elementInfo] (NSArray<UIMenuElement *> *) -> UIMenu * { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) |
| return nil; |
| |
| RetainPtr<NSArray<_WKElementAction *>> defaultActionsFromAssistant = [strongSelf->_actionSheetAssistant defaultActionsForImageSheet:elementInfo.get()]; |
| auto actions = menuElementsFromDefaultActions(defaultActionsFromAssistant, elementInfo); |
| #if ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| if (UIMenu *menu = strongSelf->_contextMenuForMachineReadableCode.get()) |
| [actions addObject:menu]; |
| #endif // ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| return [UIMenu menuWithTitle:strongSelf->_positionInformation.title children:actions]; |
| }; |
| |
| UIContextMenuContentPreviewProvider contentPreviewProvider = [weakSelf, cgImage, elementInfo] () -> UIViewController * { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) |
| return nil; |
| |
| auto uiDelegate = static_cast<id<WKUIDelegatePrivate>>([strongSelf webViewUIDelegate]); |
| if ([uiDelegate respondsToSelector:@selector(_webView:contextMenuContentPreviewForElement:)]) { |
| if (UIViewController *previewViewController = [uiDelegate _webView:[strongSelf webView] contextMenuContentPreviewForElement:strongSelf->_contextMenuElementInfo.get()]) |
| return previewViewController; |
| } |
| |
| return adoptNS([[WKImagePreviewViewController alloc] initWithCGImage:cgImage defaultActions:nil elementInfo:elementInfo.get()]).autorelease(); |
| }; |
| |
| continueWithContextMenuConfiguration([UIContextMenuConfiguration configurationWithIdentifier:nil previewProvider:contentPreviewProvider actionProvider:actionMenuProvider]); |
| return; |
| } |
| |
| // At this point we have an object we might want to show a context menu for, but the |
| // client was unable to handle it. Before giving up, we ask DataDetectors. |
| |
| strongSelf->_contextMenuElementInfo = nil; |
| |
| #if ENABLE(DATA_DETECTION) |
| // FIXME: Support JavaScript urls here. But make sure they don't show a preview. |
| // <rdar://problem/50572283> |
| if (!canShowHTTPLinkOrDataDetectorPreview) { |
| continueWithContextMenuConfiguration(nil); |
| return; |
| } |
| |
| [strongSelf continueContextMenuInteractionWithDataDetectors:continueWithContextMenuConfiguration.get()]; |
| return; |
| #else |
| continueWithContextMenuConfiguration(nil); |
| #endif |
| }); |
| |
| _contextMenuActionProviderDelegateNeedsOverride = NO; |
| _contextMenuElementInfo = wrapper(API::ContextMenuElementInfo::create(_positionInformation, nil)); |
| |
| if (_positionInformation.isImage && _positionInformation.url.isNull() && [uiDelegate respondsToSelector:@selector(_webView:alternateURLFromImage:userInfo:)]) { |
| UIImage *uiImage = [[_contextMenuElementInfo _activatedElementInfo] image]; |
| NSDictionary *userInfo = nil; |
| NSURL *nsURL = [uiDelegate _webView:self.webView alternateURLFromImage:uiImage userInfo:&userInfo]; |
| _positionInformation.url = nsURL; |
| _contextMenuElementInfo = wrapper(API::ContextMenuElementInfo::create(_positionInformation, userInfo)); |
| } |
| |
| if (_positionInformation.isLink && [uiDelegate respondsToSelector:@selector(webView:contextMenuConfigurationForElement:completionHandler:)]) { |
| auto checker = WebKit::CompletionHandlerCallChecker::create(uiDelegate, @selector(webView:contextMenuConfigurationForElement:completionHandler:)); |
| [uiDelegate webView:self.webView contextMenuConfigurationForElement:_contextMenuElementInfo.get() completionHandler:makeBlockPtr([completionBlock = WTFMove(completionBlock), checker = WTFMove(checker)] (UIContextMenuConfiguration *configuration) { |
| if (checker->completionHandlerHasBeenCalled()) |
| return; |
| checker->didCallCompletionHandler(); |
| completionBlock(configuration); |
| }).get()]; |
| } else if ([uiDelegate respondsToSelector:@selector(_webView:contextMenuConfigurationForElement:completionHandler:)]) { |
| auto checker = WebKit::CompletionHandlerCallChecker::create(uiDelegate, @selector(_webView:contextMenuConfigurationForElement:completionHandler:)); |
| [uiDelegate _webView:self.webView contextMenuConfigurationForElement:_contextMenuElementInfo.get() completionHandler:makeBlockPtr([completionBlock = WTFMove(completionBlock), checker = WTFMove(checker)] (UIContextMenuConfiguration *configuration) { |
| if (checker->completionHandlerHasBeenCalled()) |
| return; |
| checker->didCallCompletionHandler(); |
| completionBlock(configuration); |
| }).get()]; |
| } else |
| completionBlock(nil); |
| } |
| |
| #if ENABLE(DATA_DETECTION) |
| |
| - (void)continueContextMenuInteractionWithDataDetectors:(void(^)(UIContextMenuConfiguration *))continueWithContextMenuConfiguration |
| { |
| BEGIN_BLOCK_OBJC_EXCEPTIONS |
| auto ddContextMenuActionClass = getDDContextMenuActionClass(); |
| auto context = retainPtr([self dataDetectionContextForPositionInformation:_positionInformation]); |
| RetainPtr<UIContextMenuConfiguration> configurationFromDataDetectors; |
| |
| if (self.positionInformationHasImageOverlayDataDetector) { |
| DDScannerResult *scannerResult = [_positionInformation.dataDetectorResults firstObject]; |
| configurationFromDataDetectors = [ddContextMenuActionClass contextMenuConfigurationWithResult:scannerResult.coreResult inView:self context:context.get() menuIdentifier:nil]; |
| } else { |
| configurationFromDataDetectors = [ddContextMenuActionClass contextMenuConfigurationForURL:_positionInformation.url identifier:_positionInformation.dataDetectorIdentifier selectedText:[self selectedText] results:_positionInformation.dataDetectorResults.get() inView:self context:context.get() menuIdentifier:nil]; |
| _page->startInteractionWithPositionInformation(_positionInformation); |
| } |
| |
| _contextMenuActionProviderDelegateNeedsOverride = YES; |
| continueWithContextMenuConfiguration(configurationFromDataDetectors.get()); |
| END_BLOCK_OBJC_EXCEPTIONS |
| } |
| |
| #endif // ENABLE(DATA_DETECTION) |
| |
| - (NSArray<UIMenuElement *> *)_contextMenuInteraction:(UIContextMenuInteraction *)interaction overrideSuggestedActionsForConfiguration:(UIContextMenuConfiguration *)configuration |
| { |
| // If we're here we're in the legacy path, which ignores the suggested actions anyway. |
| if (!_contextMenuActionProviderDelegateNeedsOverride) |
| return nil; |
| |
| return [_actionSheetAssistant suggestedActionsForContextMenuWithPositionInformation:_positionInformation]; |
| } |
| |
| - (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)interaction previewForHighlightingMenuWithConfiguration:(UIContextMenuConfiguration *)configuration |
| { |
| [self _startSuppressingSelectionAssistantForReason:WebKit::InteractionIsHappening]; |
| [self _cancelTouchEventGestureRecognizer]; |
| return [self _createTargetedContextMenuHintPreviewIfPossible]; |
| } |
| |
| - (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willDisplayMenuForConfiguration:(UIContextMenuConfiguration *)configuration animator:(id<UIContextMenuInteractionAnimating>)animator |
| { |
| if (!_webView) |
| return; |
| |
| _isDisplayingContextMenuWithAnimation = YES; |
| [animator addCompletion:[weakSelf = WeakObjCPtr<WKContentView>(self)] { |
| if (auto strongSelf = weakSelf.get()) { |
| ASSERT_IMPLIES(strongSelf->_isDisplayingContextMenuWithAnimation, [strongSelf->_contextMenuHintContainerView window]); |
| strongSelf->_isDisplayingContextMenuWithAnimation = NO; |
| } |
| }]; |
| |
| auto uiDelegate = static_cast<id<WKUIDelegatePrivate>>(self.webView.UIDelegate); |
| if (!uiDelegate) |
| return; |
| |
| if ([uiDelegate respondsToSelector:@selector(webView:contextMenuWillPresentForElement:)]) |
| [uiDelegate webView:self.webView contextMenuWillPresentForElement:_contextMenuElementInfo.get()]; |
| else if ([uiDelegate respondsToSelector:@selector(_webView:contextMenuWillPresentForElement:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [uiDelegate _webView:self.webView contextMenuWillPresentForElement:_contextMenuElementInfo.get()]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| } |
| |
| - (UITargetedPreview *)contextMenuInteraction:(UIContextMenuInteraction *)interaction previewForDismissingMenuWithConfiguration:(UIContextMenuConfiguration *)configuration |
| { |
| return std::exchange(_contextMenuInteractionTargetedPreview, nil).autorelease(); |
| } |
| |
| - (nullable _UIContextMenuStyle *)_contextMenuInteraction:(UIContextMenuInteraction *)interaction styleForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration |
| { |
| #if defined(DD_CONTEXT_MENU_SPI_VERSION) && DD_CONTEXT_MENU_SPI_VERSION >= 2 |
| if ([configuration isKindOfClass:getDDContextMenuConfigurationClass()]) { |
| DDContextMenuConfiguration *ddConfiguration = static_cast<DDContextMenuConfiguration *>(configuration); |
| |
| if (ddConfiguration.prefersActionMenuStyle) { |
| _UIContextMenuStyle *style = [_UIContextMenuStyle defaultStyle]; |
| style.preferredLayout = _UIContextMenuLayoutActionsOnly; |
| return style; |
| } |
| } |
| #endif |
| |
| return nil; |
| } |
| |
| - (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willPerformPreviewActionForMenuWithConfiguration:(UIContextMenuConfiguration *)configuration animator:(id<UIContextMenuInteractionCommitAnimating>)animator |
| { |
| if (!_webView) |
| return; |
| |
| [self _stopSuppressingSelectionAssistantForReason:WebKit::InteractionIsHappening]; |
| |
| auto uiDelegate = static_cast<id<WKUIDelegatePrivate>>(self.webView.UIDelegate); |
| if (!uiDelegate) |
| return; |
| |
| if (needsDeprecatedPreviewAPI(uiDelegate)) { |
| |
| if (_positionInformation.isImage) { |
| if ([uiDelegate respondsToSelector:@selector(_webView:commitPreviewedImageWithURL:)]) { |
| const auto& imageURL = _positionInformation.imageURL; |
| if (imageURL.isEmpty() || !(imageURL.protocolIsInHTTPFamily() || imageURL.protocolIs("data"))) |
| return; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [uiDelegate _webView:self.webView commitPreviewedImageWithURL:(NSURL *)imageURL]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| return; |
| } |
| |
| if ([uiDelegate respondsToSelector:@selector(webView:commitPreviewingViewController:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if (auto viewController = _contextMenuLegacyPreviewController.get()) |
| [uiDelegate webView:self.webView commitPreviewingViewController:viewController]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| return; |
| } |
| |
| if ([uiDelegate respondsToSelector:@selector(_webView:commitPreviewedViewController:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if (auto viewController = _contextMenuLegacyPreviewController.get()) |
| [uiDelegate _webView:self.webView commitPreviewedViewController:viewController]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| return; |
| } |
| |
| return; |
| } |
| |
| if ([uiDelegate respondsToSelector:@selector(webView:contextMenuForElement:willCommitWithAnimator:)]) |
| [uiDelegate webView:self.webView contextMenuForElement:_contextMenuElementInfo.get() willCommitWithAnimator:animator]; |
| else if ([uiDelegate respondsToSelector:@selector(_webView:contextMenuForElement:willCommitWithAnimator:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [uiDelegate _webView:self.webView contextMenuForElement:_contextMenuElementInfo.get() willCommitWithAnimator:animator]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| |
| #if defined(DD_CONTEXT_MENU_SPI_VERSION) && DD_CONTEXT_MENU_SPI_VERSION >= 2 |
| if ([configuration isKindOfClass:getDDContextMenuConfigurationClass()]) { |
| DDContextMenuConfiguration *ddConfiguration = static_cast<DDContextMenuConfiguration *>(configuration); |
| |
| BOOL shouldExpandPreview = NO; |
| RetainPtr<UIViewController> presentedViewController; |
| |
| #if defined(DD_CONTEXT_MENU_SPI_VERSION) && DD_CONTEXT_MENU_SPI_VERSION >= 3 |
| shouldExpandPreview = !!ddConfiguration.interactionViewControllerProvider; |
| if (shouldExpandPreview) |
| presentedViewController = ddConfiguration.interactionViewControllerProvider(); |
| #else |
| shouldExpandPreview = ddConfiguration.expandPreviewOnInteraction; |
| presentedViewController = animator.previewViewController; |
| #endif |
| |
| if (shouldExpandPreview) { |
| animator.preferredCommitStyle = UIContextMenuInteractionCommitStylePop; |
| |
| // We will re-present modally on the same VC that is currently presenting the preview in a context menu. |
| RetainPtr<UIViewController> presentingViewController = animator.previewViewController.presentingViewController; |
| |
| [animator addAnimations:^{ |
| [presentingViewController presentViewController:presentedViewController.get() animated:NO completion:nil]; |
| }]; |
| return; |
| } |
| |
| if (NSURL *interactionURL = ddConfiguration.interactionURL) { |
| animator.preferredCommitStyle = UIContextMenuInteractionCommitStylePop; |
| |
| [animator addAnimations:^{ |
| [[UIApplication sharedApplication] openURL:interactionURL withCompletionHandler:nil]; |
| }]; |
| return; |
| } |
| } |
| #endif |
| } |
| |
| - (void)contextMenuInteraction:(UIContextMenuInteraction *)interaction willEndForConfiguration:(UIContextMenuConfiguration *)configuration animator:(nullable id<UIContextMenuInteractionAnimating>)animator |
| { |
| if (!_webView) |
| return; |
| |
| [self _stopSuppressingSelectionAssistantForReason:WebKit::InteractionIsHappening]; |
| |
| // FIXME: This delegate is being called more than once by UIKit. <rdar://problem/51550291> |
| // This conditional avoids the WKUIDelegate being called twice too. |
| if (_contextMenuElementInfo) { |
| auto uiDelegate = static_cast<id<WKUIDelegatePrivate>>(self.webView.UIDelegate); |
| if ([uiDelegate respondsToSelector:@selector(webView:contextMenuDidEndForElement:)]) |
| [uiDelegate webView:self.webView contextMenuDidEndForElement:_contextMenuElementInfo.get()]; |
| else if ([uiDelegate respondsToSelector:@selector(_webView:contextMenuDidEndForElement:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [uiDelegate _webView:self.webView contextMenuDidEndForElement:_contextMenuElementInfo.get()]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| } |
| |
| _page->stopInteraction(); |
| |
| _contextMenuLegacyPreviewController = nullptr; |
| _contextMenuLegacyMenu = nullptr; |
| _contextMenuHasRequestedLegacyData = NO; |
| _contextMenuElementInfo = nullptr; |
| |
| #if ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| _contextMenuForMachineReadableCode.clear(); |
| #endif // ENABLE(IMAGE_ANALYSIS_FOR_MACHINE_READABLE_CODES) |
| |
| [animator addCompletion:[weakSelf = WeakObjCPtr<WKContentView>(self)] () { |
| auto strongSelf = weakSelf.get(); |
| if (!strongSelf) |
| return; |
| |
| strongSelf->_isDisplayingContextMenuWithAnimation = NO; |
| [strongSelf _removeContextMenuHintContainerIfPossible]; |
| [strongSelf->_webView _didDismissContextMenu]; |
| }]; |
| } |
| |
| #endif // USE(UICONTEXTMENU) |
| |
| - (BOOL)_interactionShouldBeginFromPreviewItemController:(UIPreviewItemController *)controller forPosition:(CGPoint)position |
| { |
| if (!_longPressCanClick) |
| return NO; |
| |
| WebKit::InteractionInformationRequest request(WebCore::roundedIntPoint(position)); |
| request.includeSnapshot = true; |
| request.includeLinkIndicator = true; |
| request.linkIndicatorShouldHaveLegacyMargins = !self._shouldUseContextMenus; |
| if (![self ensurePositionInformationIsUpToDate:request]) |
| return NO; |
| if (!_positionInformation.isLink && !_positionInformation.isImage && !_positionInformation.isAttachment) |
| return NO; |
| |
| const URL& linkURL = _positionInformation.url; |
| if (_positionInformation.isLink) { |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate respondsToSelector:@selector(webView:shouldPreviewElement:)]) { |
| auto previewElementInfo = adoptNS([[WKPreviewElementInfo alloc] _initWithLinkURL:(NSURL *)linkURL]); |
| return [uiDelegate webView:self.webView shouldPreviewElement:previewElementInfo.get()]; |
| } |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| if (linkURL.isEmpty()) |
| return NO; |
| if (linkURL.protocolIsInHTTPFamily()) |
| return YES; |
| #if ENABLE(DATA_DETECTION) |
| if (WebCore::DataDetection::canBePresentedByDataDetectors(linkURL)) |
| return YES; |
| #endif |
| return NO; |
| } |
| return YES; |
| } |
| |
| - (NSDictionary *)_dataForPreviewItemController:(UIPreviewItemController *)controller atPosition:(CGPoint)position type:(UIPreviewItemType *)type |
| { |
| *type = UIPreviewItemTypeNone; |
| |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| BOOL supportsImagePreview = [uiDelegate respondsToSelector:@selector(_webView:commitPreviewedImageWithURL:)]; |
| BOOL canShowImagePreview = _positionInformation.isImage && supportsImagePreview; |
| BOOL canShowLinkPreview = _positionInformation.isLink || canShowImagePreview; |
| BOOL useImageURLForLink = NO; |
| BOOL respondsToAttachmentListForWebViewSourceIsManaged = [uiDelegate respondsToSelector:@selector(_attachmentListForWebView:sourceIsManaged:)]; |
| BOOL supportsAttachmentPreview = ([uiDelegate respondsToSelector:@selector(_attachmentListForWebView:)] || respondsToAttachmentListForWebViewSourceIsManaged) |
| && [uiDelegate respondsToSelector:@selector(_webView:indexIntoAttachmentListForElement:)]; |
| BOOL canShowAttachmentPreview = (_positionInformation.isAttachment || _positionInformation.isImage) && supportsAttachmentPreview; |
| BOOL isDataDetectorLink = NO; |
| #if ENABLE(DATA_DETECTION) |
| isDataDetectorLink = _positionInformation.isDataDetectorLink; |
| #endif |
| |
| if (canShowImagePreview && _positionInformation.isAnimatedImage) { |
| canShowImagePreview = NO; |
| canShowLinkPreview = YES; |
| useImageURLForLink = YES; |
| } |
| |
| if (!canShowLinkPreview && !canShowImagePreview && !canShowAttachmentPreview) |
| return nil; |
| |
| const URL& linkURL = _positionInformation.url; |
| if (!useImageURLForLink && (linkURL.isEmpty() || (!linkURL.protocolIsInHTTPFamily() && !isDataDetectorLink))) { |
| if (canShowLinkPreview && !canShowImagePreview) |
| return nil; |
| canShowLinkPreview = NO; |
| } |
| |
| auto dataForPreview = adoptNS([[NSMutableDictionary alloc] init]); |
| if (canShowLinkPreview) { |
| *type = UIPreviewItemTypeLink; |
| if (useImageURLForLink) |
| dataForPreview.get()[UIPreviewDataLink] = (NSURL *)_positionInformation.imageURL; |
| else |
| dataForPreview.get()[UIPreviewDataLink] = (NSURL *)linkURL; |
| #if ENABLE(DATA_DETECTION) |
| if (isDataDetectorLink) { |
| NSDictionary *context = nil; |
| if ([uiDelegate respondsToSelector:@selector(_dataDetectionContextForWebView:)]) |
| context = [uiDelegate _dataDetectionContextForWebView:self.webView]; |
| |
| DDDetectionController *controller = [getDDDetectionControllerClass() sharedController]; |
| NSDictionary *newContext = nil; |
| RetainPtr<NSMutableDictionary> extendedContext; |
| DDResultRef ddResult = [controller resultForURL:dataForPreview.get()[UIPreviewDataLink] identifier:_positionInformation.dataDetectorIdentifier selectedText:[self selectedText] results:_positionInformation.dataDetectorResults.get() context:context extendedContext:&newContext]; |
| if (ddResult) |
| dataForPreview.get()[UIPreviewDataDDResult] = (__bridge id)ddResult; |
| if (!_positionInformation.textBefore.isEmpty() || !_positionInformation.textAfter.isEmpty()) { |
| extendedContext = adoptNS([@{ |
| getkDataDetectorsLeadingText() : _positionInformation.textBefore, |
| getkDataDetectorsTrailingText() : _positionInformation.textAfter, |
| } mutableCopy]); |
| |
| if (newContext) |
| [extendedContext addEntriesFromDictionary:newContext]; |
| newContext = extendedContext.get(); |
| } |
| if (newContext) |
| dataForPreview.get()[UIPreviewDataDDContext] = newContext; |
| } |
| #endif // ENABLE(DATA_DETECTION) |
| } else if (canShowImagePreview) { |
| *type = UIPreviewItemTypeImage; |
| dataForPreview.get()[UIPreviewDataLink] = (NSURL *)_positionInformation.imageURL; |
| } else if (canShowAttachmentPreview) { |
| *type = UIPreviewItemTypeAttachment; |
| auto element = adoptNS([[_WKActivatedElementInfo alloc] _initWithType:_WKActivatedElementTypeAttachment URL:(NSURL *)linkURL imageURL:(NSURL *)_positionInformation.imageURL location:_positionInformation.request.point title:_positionInformation.title ID:_positionInformation.idAttribute rect:_positionInformation.bounds image:nil]); |
| NSUInteger index = [uiDelegate _webView:self.webView indexIntoAttachmentListForElement:element.get()]; |
| if (index != NSNotFound) { |
| BOOL sourceIsManaged = NO; |
| if (respondsToAttachmentListForWebViewSourceIsManaged) |
| dataForPreview.get()[UIPreviewDataAttachmentList] = [uiDelegate _attachmentListForWebView:self.webView sourceIsManaged:&sourceIsManaged]; |
| else |
| dataForPreview.get()[UIPreviewDataAttachmentList] = [uiDelegate _attachmentListForWebView:self.webView]; |
| dataForPreview.get()[UIPreviewDataAttachmentIndex] = [NSNumber numberWithUnsignedInteger:index]; |
| dataForPreview.get()[UIPreviewDataAttachmentListIsContentManaged] = [NSNumber numberWithBool:sourceIsManaged]; |
| } |
| } |
| |
| return dataForPreview.autorelease(); |
| } |
| |
| - (CGRect)_presentationRectForPreviewItemController:(UIPreviewItemController *)controller |
| { |
| return _positionInformation.bounds; |
| } |
| |
| - (UIViewController *)_presentedViewControllerForPreviewItemController:(UIPreviewItemController *)controller |
| { |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| |
| [_webView _didShowContextMenu]; |
| |
| NSURL *targetURL = controller.previewData[UIPreviewDataLink]; |
| URL coreTargetURL = targetURL; |
| bool isValidURLForImagePreview = !coreTargetURL.isEmpty() && (coreTargetURL.protocolIsInHTTPFamily() || coreTargetURL.protocolIs("data")); |
| |
| if ([_previewItemController type] == UIPreviewItemTypeLink) { |
| _longPressCanClick = NO; |
| _page->startInteractionWithPositionInformation(_positionInformation); |
| |
| // Treat animated images like a link preview |
| if (isValidURLForImagePreview && _positionInformation.isAnimatedImage) { |
| RetainPtr<_WKActivatedElementInfo> animatedImageElementInfo = adoptNS([[_WKActivatedElementInfo alloc] _initWithType:_WKActivatedElementTypeImage URL:targetURL imageURL:nil location:_positionInformation.request.point title:_positionInformation.title ID:_positionInformation.idAttribute rect:_positionInformation.bounds image:_positionInformation.image.get()]); |
| |
| if ([uiDelegate respondsToSelector:@selector(_webView:previewViewControllerForAnimatedImageAtURL:defaultActions:elementInfo:imageSize:)]) { |
| RetainPtr<NSArray> actions = [_actionSheetAssistant defaultActionsForImageSheet:animatedImageElementInfo.get()]; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| return [uiDelegate _webView:self.webView previewViewControllerForAnimatedImageAtURL:targetURL defaultActions:actions.get() elementInfo:animatedImageElementInfo.get() imageSize:_positionInformation.image->size()]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| } |
| |
| RetainPtr<_WKActivatedElementInfo> elementInfo = adoptNS([[_WKActivatedElementInfo alloc] _initWithType:_WKActivatedElementTypeLink URL:targetURL imageURL:nil location:_positionInformation.request.point title:_positionInformation.title ID:_positionInformation.idAttribute rect:_positionInformation.bounds image:_positionInformation.image.get()]); |
| |
| auto actions = [_actionSheetAssistant defaultActionsForLinkSheet:elementInfo.get()]; |
| if ([uiDelegate respondsToSelector:@selector(webView:previewingViewControllerForElement:defaultActions:)]) { |
| auto previewActions = adoptNS([[NSMutableArray alloc] init]); |
| for (_WKElementAction *elementAction in actions.get()) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| WKPreviewAction *previewAction = [WKPreviewAction actionWithIdentifier:previewIdentifierForElementAction(elementAction) title:[elementAction title] style:UIPreviewActionStyleDefault handler:^(UIPreviewAction *, UIViewController *) { |
| [elementAction runActionWithElementInfo:elementInfo.get()]; |
| }]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| [previewActions addObject:previewAction]; |
| } |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| auto previewElementInfo = adoptNS([[WKPreviewElementInfo alloc] _initWithLinkURL:targetURL]); |
| if (UIViewController *controller = [uiDelegate webView:self.webView previewingViewControllerForElement:previewElementInfo.get() defaultActions:previewActions.get()]) |
| return controller; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| } |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate respondsToSelector:@selector(_webView:previewViewControllerForURL:defaultActions:elementInfo:)]) |
| return [uiDelegate _webView:self.webView previewViewControllerForURL:targetURL defaultActions:actions.get() elementInfo:elementInfo.get()]; |
| |
| if ([uiDelegate respondsToSelector:@selector(_webView:previewViewControllerForURL:)]) |
| return [uiDelegate _webView:self.webView previewViewControllerForURL:targetURL]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| return nil; |
| } |
| |
| if ([_previewItemController type] == UIPreviewItemTypeImage) { |
| if (!isValidURLForImagePreview) |
| return nil; |
| |
| RetainPtr<NSURL> alternateURL = targetURL; |
| RetainPtr<NSDictionary> imageInfo; |
| RetainPtr<CGImageRef> cgImage = _positionInformation.image->makeCGImageCopy(); |
| RetainPtr<UIImage> uiImage = adoptNS([[UIImage alloc] initWithCGImage:cgImage.get()]); |
| if ([uiDelegate respondsToSelector:@selector(_webView:alternateURLFromImage:userInfo:)]) { |
| NSDictionary *userInfo; |
| alternateURL = [uiDelegate _webView:self.webView alternateURLFromImage:uiImage.get() userInfo:&userInfo]; |
| imageInfo = userInfo; |
| } |
| |
| RetainPtr<_WKActivatedElementInfo> elementInfo = adoptNS([[_WKActivatedElementInfo alloc] _initWithType:_WKActivatedElementTypeImage URL:alternateURL.get() imageURL:nil location:_positionInformation.request.point title:_positionInformation.title ID:_positionInformation.idAttribute rect:_positionInformation.bounds image:_positionInformation.image.get() userInfo:imageInfo.get()]); |
| _page->startInteractionWithPositionInformation(_positionInformation); |
| |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate respondsToSelector:@selector(_webView:willPreviewImageWithURL:)]) |
| [uiDelegate _webView:self.webView willPreviewImageWithURL:targetURL]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| auto defaultActions = [_actionSheetAssistant defaultActionsForImageSheet:elementInfo.get()]; |
| if (imageInfo && [uiDelegate respondsToSelector:@selector(_webView:previewViewControllerForImage:alternateURL:defaultActions:elementInfo:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| UIViewController *previewViewController = [uiDelegate _webView:self.webView previewViewControllerForImage:uiImage.get() alternateURL:alternateURL.get() defaultActions:defaultActions.get() elementInfo:elementInfo.get()]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| if (previewViewController) |
| return previewViewController; |
| } |
| |
| return adoptNS([[WKImagePreviewViewController alloc] initWithCGImage:cgImage defaultActions:defaultActions elementInfo:elementInfo]).autorelease(); |
| } |
| |
| return nil; |
| } |
| |
| - (void)_previewItemController:(UIPreviewItemController *)controller commitPreview:(UIViewController *)viewController |
| { |
| id <WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| if ([_previewItemController type] == UIPreviewItemTypeImage) { |
| if ([uiDelegate respondsToSelector:@selector(_webView:commitPreviewedImageWithURL:)]) { |
| const URL& imageURL = _positionInformation.imageURL; |
| if (imageURL.isEmpty() || !(imageURL.protocolIsInHTTPFamily() || imageURL.protocolIs("data"))) |
| return; |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [uiDelegate _webView:self.webView commitPreviewedImageWithURL:(NSURL *)imageURL]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| return; |
| } |
| return; |
| } |
| |
| if ([uiDelegate respondsToSelector:@selector(webView:commitPreviewingViewController:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [uiDelegate webView:self.webView commitPreviewingViewController:viewController]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| return; |
| } |
| |
| if ([uiDelegate respondsToSelector:@selector(_webView:commitPreviewedViewController:)]) { |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| [uiDelegate _webView:self.webView commitPreviewedViewController:viewController]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| return; |
| } |
| |
| } |
| |
| - (void)_interactionStartedFromPreviewItemController:(UIPreviewItemController *)controller |
| { |
| [self _removeDefaultGestureRecognizers]; |
| |
| [self _cancelInteraction]; |
| } |
| |
| - (void)_interactionStoppedFromPreviewItemController:(UIPreviewItemController *)controller |
| { |
| [self _addDefaultGestureRecognizers]; |
| |
| if (![_actionSheetAssistant isShowingSheet]) |
| _page->stopInteraction(); |
| } |
| |
| - (void)_previewItemController:(UIPreviewItemController *)controller didDismissPreview:(UIViewController *)viewController committing:(BOOL)committing |
| { |
| id<WKUIDelegatePrivate> uiDelegate = static_cast<id <WKUIDelegatePrivate>>([_webView UIDelegate]); |
| ALLOW_DEPRECATED_DECLARATIONS_BEGIN |
| if ([uiDelegate respondsToSelector:@selector(_webView:didDismissPreviewViewController:committing:)]) |
| [uiDelegate _webView:self.webView didDismissPreviewViewController:viewController committing:committing]; |
| else if ([uiDelegate respondsToSelector:@selector(_webView:didDismissPreviewViewController:)]) |
| [uiDelegate _webView:self.webView didDismissPreviewViewController:viewController]; |
| ALLOW_DEPRECATED_DECLARATIONS_END |
| |
| [_webView _didDismissContextMenu]; |
| } |
| |
| - (UIImage *)_presentationSnapshotForPreviewItemController:(UIPreviewItemController *)controller |
| { |
| if (!_positionInformation.linkIndicator.contentImage) |
| return nullptr; |
| |
| auto nativeImage = _positionInformation.linkIndicator.contentImage->nativeImage(); |
| if (!nativeImage) |
| return nullptr; |
| |
| return adoptNS([[UIImage alloc] initWithCGImage:nativeImage->platformImage().get()]).autorelease(); |
| } |
| |
| - (NSArray *)_presentationRectsForPreviewItemController:(UIPreviewItemController *)controller |
| { |
| if (_positionInformation.linkIndicator.contentImage) { |
| auto origin = _positionInformation.linkIndicator.textBoundingRectInRootViewCoordinates.location(); |
| return createNSArray(_positionInformation.linkIndicator.textRectsInBoundingRectCoordinates, [&] (CGRect rect) { |
| return [NSValue valueWithCGRect:CGRectOffset(rect, origin.x(), origin.y())]; |
| }).autorelease(); |
| } else { |
| float marginInPx = 4 * _page->deviceScaleFactor(); |
| return @[[NSValue valueWithCGRect:CGRectInset(_positionInformation.bounds, -marginInPx, -marginInPx)]]; |
| } |
| } |
| |
| - (void)_previewItemControllerDidCancelPreview:(UIPreviewItemController *)controller |
| { |
| _longPressCanClick = NO; |
| |
| [_webView _didDismissContextMenu]; |
| } |
| |
| @end |
| |
| #endif // HAVE(LINK_PREVIEW) |
| |
| // UITextRange and UITextPosition implementations for WK2 |
| // FIXME: Move these out into separate files. |
| |
| @implementation WKTextRange (UITextInputAdditions) |
| |
| - (BOOL)_isCaret |
| { |
| return self.empty; |
| } |
| |
| - (BOOL)_isRanged |
| { |
| return !self.empty; |
| } |
| |
| @end |
| |
| @implementation WKTextRange |
| |
| +(WKTextRange *)textRangeWithState:(BOOL)isNone isRange:(BOOL)isRange isEditable:(BOOL)isEditable startRect:(CGRect)startRect endRect:(CGRect)endRect selectionRects:(NSArray *)selectionRects selectedTextLength:(NSUInteger)selectedTextLength |
| { |
| auto range = adoptNS([[WKTextRange alloc] init]); |
| [range setIsNone:isNone]; |
| [range setIsRange:isRange]; |
| [range setIsEditable:isEditable]; |
| [range setStartRect:startRect]; |
| [range setEndRect:endRect]; |
| [range setSelectedTextLength:selectedTextLength]; |
| [range setSelectionRects:selectionRects]; |
| return range.autorelease(); |
| } |
| |
| - (void)dealloc |
| { |
| [_selectionRects release]; |
| [super dealloc]; |
| } |
| |
| - (NSString *)description |
| { |
| return [NSString stringWithFormat:@"%@(%p) - start:%@, end:%@", [self class], self, NSStringFromCGRect(self.startRect), NSStringFromCGRect(self.endRect)]; |
| } |
| |
| - (WKTextPosition *)start |
| { |
| WKTextPosition *pos = [WKTextPosition textPositionWithRect:self.startRect]; |
| return pos; |
| } |
| |
| - (UITextPosition *)end |
| { |
| WKTextPosition *pos = [WKTextPosition textPositionWithRect:self.endRect]; |
| return pos; |
| } |
| |
| - (BOOL)isEmpty |
| { |
| return !self.isRange; |
| } |
| |
| // FIXME: Overriding isEqual: without overriding hash will cause trouble if this ever goes into an NSSet or is the key in an NSDictionary, |
| // since two equal items could have different hashes. |
| - (BOOL)isEqual:(id)other |
| { |
| if (![other isKindOfClass:[WKTextRange class]]) |
| return NO; |
| |
| WKTextRange *otherRange = (WKTextRange *)other; |
| |
| if (self == other) |
| return YES; |
| |
| // FIXME: Probably incorrect for equality to ignore so much of the object state. |
| // It ignores isNone, isEditable, selectedTextLength, and selectionRects. |
| |
| if (self.isRange) { |
| if (!otherRange.isRange) |
| return NO; |
| return CGRectEqualToRect(self.startRect, otherRange.startRect) && CGRectEqualToRect(self.endRect, otherRange.endRect); |
| } else { |
| if (otherRange.isRange) |
| return NO; |
| // FIXME: Do we need to check isNone here? |
| return CGRectEqualToRect(self.startRect, otherRange.startRect); |
| } |
| } |
| |
| @end |
| |
| @implementation WKTextPosition |
| |
| @synthesize positionRect = _positionRect; |
| |
| + (WKTextPosition *)textPositionWithRect:(CGRect)positionRect |
| { |
| auto pos = adoptNS([[WKTextPosition alloc] init]); |
| [pos setPositionRect:positionRect]; |
| return pos.autorelease(); |
| } |
| |
| // FIXME: Overriding isEqual: without overriding hash will cause trouble if this ever goes into a NSSet or is the key in an NSDictionary, |
| // since two equal items could have different hashes. |
| - (BOOL)isEqual:(id)other |
| { |
| if (![other isKindOfClass:[WKTextPosition class]]) |
| return NO; |
| |
| return CGRectEqualToRect(self.positionRect, ((WKTextPosition *)other).positionRect); |
| } |
| |
| - (NSString *)description |
| { |
| return [NSString stringWithFormat:@"<WKTextPosition: %p, {%@}>", self, NSStringFromCGRect(self.positionRect)]; |
| } |
| |
| @end |
| |
| #if HAVE(UIFINDINTERACTION) |
| |
| @implementation WKFoundTextRange |
| |
| + (WKFoundTextRange *)foundTextRangeWithRect:(CGRect)rect index:(NSUInteger)index |
| { |
| auto range = adoptNS([[WKFoundTextRange alloc] init]); |
| [range setRect:rect]; |
| [range setIndex:index]; |
| return range.autorelease(); |
| } |
| |
| - (WKFoundTextPosition *)start |
| { |
| WKFoundTextPosition *position = [WKFoundTextPosition textPositionWithIndex:self.index]; |
| return position; |
| } |
| |
| - (UITextPosition *)end |
| { |
| return self.start; |
| } |
| |
| - (BOOL)isEmpty |
| { |
| return NO; |
| } |
| |
| @end |
| |
| @implementation WKFoundTextPosition |
| |
| + (WKFoundTextPosition *)textPositionWithIndex:(NSUInteger)index |
| { |
| auto pos = adoptNS([[WKFoundTextPosition alloc] init]); |
| [pos setIndex:index]; |
| return pos.autorelease(); |
| } |
| |
| @end |
| |
| #endif |
| |
| @implementation WKAutocorrectionRects |
| |
| + (WKAutocorrectionRects *)autocorrectionRectsWithFirstCGRect:(CGRect)firstRect lastCGRect:(CGRect)lastRect |
| { |
| auto rects = adoptNS([[WKAutocorrectionRects alloc] init]); |
| [rects setFirstRect:firstRect]; |
| [rects setLastRect:lastRect]; |
| return rects.autorelease(); |
| } |
| |
| @end |
| |
| @implementation WKAutocorrectionContext |
| |
| + (WKAutocorrectionContext *)emptyAutocorrectionContext |
| { |
| return [self autocorrectionContextWithWebContext:WebKit::WebAutocorrectionContext { }]; |
| } |
| |
| + (WKAutocorrectionContext *)autocorrectionContextWithWebContext:(const WebKit::WebAutocorrectionContext&)webCorrection |
| { |
| auto correction = adoptNS([[WKAutocorrectionContext alloc] init]); |
| [correction setContextBeforeSelection:nsStringNilIfEmpty(webCorrection.contextBefore)]; |
| [correction setSelectedText:nsStringNilIfEmpty(webCorrection.selectedText)]; |
| [correction setMarkedText:nsStringNilIfEmpty(webCorrection.markedText)]; |
| [correction setContextAfterSelection:nsStringNilIfEmpty(webCorrection.contextAfter)]; |
| [correction setRangeInMarkedText:webCorrection.markedTextRange]; |
| return correction.autorelease(); |
| } |
| |
| @end |
| |
| #endif // PLATFORM(IOS_FAMILY) |