[iOS] Add a quirk to synthesize mouse events when modifying the selection
authorwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 8 May 2019 19:36:33 +0000 (19:36 +0000)
committerwenson_hsieh@apple.com <wenson_hsieh@apple.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Wed, 8 May 2019 19:36:33 +0000 (19:36 +0000)
https://bugs.webkit.org/show_bug.cgi?id=197683
<rdar://problem/48003980>

Reviewed by Tim Horton.

Source/WebCore:

See WebKit ChangeLog for more details.

Test: editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk.html

* page/EventHandler.cpp:
(WebCore::EventHandler::handleMousePressEvent):
(WebCore::EventHandler::supportsSelectionUpdatesOnMouseDrag const):

Add some platform hooks to prevent mousemove events from updating the selection on iOS.

(WebCore::EventHandler::shouldAllowMouseDownToStartDrag const):

Add some platform hooks to prevent drag and drop from kicking in when sending synthetic mousemove events to the
page on iOS (drag and drop is instead triggered by EventHandler::tryToBeginDragAtPoint).

(WebCore::EventHandler::updateSelectionForMouseDrag):
* page/EventHandler.h:
* page/Quirks.cpp:
(WebCore::Quirks::shouldDispatchSyntheticMouseEventsWhenModifyingSelection const):
* page/Quirks.h:

Add the new site-specific quirk.

* page/Settings.yaml:
* page/ios/EventHandlerIOS.mm:
(WebCore::EventHandler::tryToBeginDragAtPoint):
(WebCore::EventHandler::supportsSelectionUpdatesOnMouseDrag const):
(WebCore::EventHandler::shouldAllowMouseDownToStartDrag const):
* testing/InternalSettings.cpp:
(WebCore::InternalSettings::Backup::Backup):
(WebCore::InternalSettings::Backup::restoreTo):
(WebCore::InternalSettings::setShouldDispatchSyntheticMouseEventsWhenModifyingSelection):
* testing/InternalSettings.h:
* testing/InternalSettings.idl:

Add an internal settings hook to opt into this quirk, for use in layout tests.

Source/WebKit:

Introduces support for dispatching synthetic mouse events when modifying the selection on some websites. See
below for more details.

* UIProcess/WebPageProxy.cpp:
(WebKit::WebPageProxy::selectAll):
* UIProcess/WebPageProxy.h:

Instead of executing a "SelectAll" editing command using the generic WebPage::executeEditCommand method,
introduce a separate method for selectAll that executes the "SelectAll" edit command and then does some
platform-specific work. See platformDidSelectAll.

* UIProcess/ios/WKContentViewInteraction.mm:
(-[WKContentView selectAllForWebView:]):
* WebProcess/WebPage/WebPage.cpp:
(WebKit::WebPage::selectAll):
(WebKit::WebPage::shouldDispatchSyntheticMouseEventsWhenModifyingSelection const):

Add a helper method to determine whether the quirk should be enabled.

(WebKit::WebPage::platformDidSelectAll):
* WebProcess/WebPage/WebPage.h:
* WebProcess/WebPage/WebPage.messages.in:
* WebProcess/WebPage/ios/WebPageIOS.mm:
(WebKit::elementRectInRootViewCoordinates):

Move this function closer to the top of the file so that it can be used in
dispatchSyntheticMouseEventsForSelectionGesture.

(WebKit::WebPage::clearSelection):
(WebKit::WebPage::dispatchSyntheticMouseEventsForSelectionGesture):

Add a helper method to dispatch a synthetic mouse event for a given selection gesture type. Used in several
places in WebPageIOS to synthesize and dispatch mouse events during selection.

(WebKit::WebPage::updateSelectionWithTouches):

When changing the selection with selection handles, fake mousedown when the user first touches down on the
selection handle; mousemove as the user is moving the handle around; and finally, mouseup when the user lets go.

(WebKit::WebPage::extendSelection):
(WebKit::WebPage::platformDidSelectAll):

When tapping "Select All" and/or "Select" in the callout menu, fake a mousedown at the selection start, then a
mousemove at selection end, and finally, a mouseup at selection end.

(WebKit::WebPage::getFocusedElementInformation):

LayoutTests:

Adds a new layout test to enable the site-specific quirk and verify that mouse events are dispatched when
changing selection, both via the callout menu and by moving the selection grabber using gestures.

* editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk-expected.txt: Added.
* editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk.html: Added.
* resources/ui-helper.js:
(window.UIHelper.waitForMenuToHide.return.new.Promise):
(window.UIHelper.waitForMenuToHide):

Introduce a new helper method to wait for the menu to hide (on iOS, this refers to the callout menu).

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@245062 268f45cc-cd09-0410-ab3c-d52691b4dbfc

22 files changed:
LayoutTests/ChangeLog
LayoutTests/editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk-expected.txt [new file with mode: 0644]
LayoutTests/editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk.html [new file with mode: 0644]
LayoutTests/resources/ui-helper.js
Source/WebCore/ChangeLog
Source/WebCore/page/EventHandler.cpp
Source/WebCore/page/EventHandler.h
Source/WebCore/page/Quirks.cpp
Source/WebCore/page/Quirks.h
Source/WebCore/page/Settings.yaml
Source/WebCore/page/ios/EventHandlerIOS.mm
Source/WebCore/testing/InternalSettings.cpp
Source/WebCore/testing/InternalSettings.h
Source/WebCore/testing/InternalSettings.idl
Source/WebKit/ChangeLog
Source/WebKit/UIProcess/WebPageProxy.cpp
Source/WebKit/UIProcess/WebPageProxy.h
Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
Source/WebKit/WebProcess/WebPage/WebPage.cpp
Source/WebKit/WebProcess/WebPage/WebPage.h
Source/WebKit/WebProcess/WebPage/WebPage.messages.in
Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm

index 3035986..f32b5a2 100644 (file)
@@ -1,3 +1,22 @@
+2019-05-08  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Add a quirk to synthesize mouse events when modifying the selection
+        https://bugs.webkit.org/show_bug.cgi?id=197683
+        <rdar://problem/48003980>
+
+        Reviewed by Tim Horton.
+
+        Adds a new layout test to enable the site-specific quirk and verify that mouse events are dispatched when
+        changing selection, both via the callout menu and by moving the selection grabber using gestures.
+
+        * editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk-expected.txt: Added.
+        * editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk.html: Added.
+        * resources/ui-helper.js:
+        (window.UIHelper.waitForMenuToHide.return.new.Promise):
+        (window.UIHelper.waitForMenuToHide):
+
+        Introduce a new helper method to wait for the menu to hide (on iOS, this refers to the callout menu).
+
 2019-05-07  Ryan Haddad  <ryanhaddad@apple.com>
 
         Unreviewed test gardening for Mojave.
diff --git a/LayoutTests/editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk-expected.txt b/LayoutTests/editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk-expected.txt
new file mode 100644 (file)
index 0000000..dfbdfdd
--- /dev/null
@@ -0,0 +1,30 @@
+This test verifies that when the 'mouse event synthesis on selection' quirk is enabled, text selection dispatches mouse events that mimic the user selecting text. To run the test manually, use the callout menu to select text or selection handles, and verify that mousedown, mousemove and mouseup are dispatched and logged in the output area below.
+
+On success, you will see a series of "PASS" messages, followed by "TEST COMPLETE".
+
+
+Attempting to show the callout bar.
+Observed event type: mousedown
+Observed event type: mouseup
+Observed event type: mousedown
+Observed event type: mouseup
+PASS Displayed the callout bar.
+
+Attempting to select the last word.
+Observed event type: mousedown
+Observed event type: mousemove
+Observed event type: mouseup
+PASS Selected the last word.
+
+Attempting to dismiss the callout bar by executing 'SelectAll'.
+PASS Dismissed the callout bar.
+
+Attempting to move the selection grabber.
+Observed event type: mousedown
+Observed event type: mousemove
+Observed event type: mouseup
+PASS Moved the selection grabber.
+PASS successfullyParsed is true
+
+TEST COMPLETE
+
diff --git a/LayoutTests/editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk.html b/LayoutTests/editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk.html
new file mode 100644 (file)
index 0000000..0a2fb95
--- /dev/null
@@ -0,0 +1,98 @@
+<!DOCTYPE html><!-- webkit-test-runner [ useFlexibleViewport=true ] -->
+<html>
+<head>
+<script src="../../../resources/ui-helper.js"></script>
+<script src="../../../resources/basic-gestures.js"></script>
+<script src="../../../resources/js-test.js"></script>
+<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=no">
+<style>
+body, html {
+    margin: 0;
+}
+.editor {
+    border: 2px solid tomato;
+    width: 300px;
+    height: 300px;
+    box-sizing: border-box;
+    font-size: 32px;
+    padding: 10px;
+}
+
+#console, #description {
+    width: 300px;
+    height: 100px;
+    overflow: scroll;
+}
+</style>
+<script>
+if (window.internals)
+    internals.settings.setShouldDispatchSyntheticMouseEventsWhenModifyingSelection(true);
+
+lastEvent = null;
+jsTestIsAsync = true;
+
+function recordEvent() {
+    if (!lastEvent || event.type !== lastEvent.type)
+        debug(`Observed event type: ${event.type}`);
+    lastEvent = event;
+}
+
+addEventListener("mousedown", recordEvent);
+addEventListener("mousemove", recordEvent);
+addEventListener("mouseup", recordEvent);
+
+async function waitForSelectionToAppear()
+{
+    while (true) {
+        const rects = await UIHelper.getUISelectionViewRects();
+        if (rects.length)
+            return rects;
+    }
+}
+
+function midPointOfRect(rect) {
+    return [rect.left + (rect.width / 2), rect.top + (rect.height / 2)];
+}
+
+addEventListener("load", async () => {
+    if (!window.testRunner)
+        return;
+
+    description("This test verifies that when the 'mouse event synthesis on selection' quirk is enabled, text selection dispatches mouse events that mimic the user selecting text. To run the test manually, use the callout menu to select text or selection handles, and verify that mousedown, mousemove and mouseup are dispatched and logged in the output area below.");
+
+    debug("\nAttempting to show the callout bar.");
+    const editor = document.querySelector(".editor");
+    await UIHelper.activateElementAndWaitForInputSession(editor);
+    await UIHelper.activateElement(editor);
+    await UIHelper.waitForMenuToShow();
+    testPassed("Displayed the callout bar.");
+
+    debug("\nAttempting to select the last word.");
+    await UIHelper.chooseMenuAction("Select");
+    const rects = await waitForSelectionToAppear();
+    testPassed("Selected the last word.");
+
+    debug("\nAttempting to dismiss the callout bar by executing 'SelectAll'.");
+    document.execCommand("SelectAll");
+    await UIHelper.waitForMenuToHide();
+    testPassed("Dismissed the callout bar.");
+
+    debug("\nAttempting to move the selection grabber.");
+    const [grabberX, grabberY] = midPointOfRect(await UIHelper.getSelectionEndGrabberViewRect());
+    const touchDestinationX = grabberX - rects[0].width;
+
+    await touchAndDragFromPointToPoint(grabberX, grabberY, touchDestinationX, grabberY);
+    await liftUpAtPoint(touchDestinationX, grabberY);
+    testPassed("Moved the selection grabber.");
+
+    document.querySelector(".editor").remove();
+    finishJSTest();
+});
+</script>
+</head>
+<body>
+<div class="editor" contenteditable>The quick brown fox jumped over the lazy dog.</div>
+<div id="description"></div>
+<div id="console"></div>
+</body>
+</html>
index 30ab8b5..eea193e 100644 (file)
@@ -827,6 +827,19 @@ window.UIHelper = class UIHelper {
         });
     }
 
+    static waitForMenuToHide()
+    {
+        return new Promise(resolve => {
+            testRunner.runUIScript(`
+                (function() {
+                    if (uiController.isShowingMenu)
+                        uiController.didHideMenuCallback = () => uiController.uiScriptComplete();
+                    else
+                        uiController.uiScriptComplete();
+                })()`, resolve);
+        });
+    }
+
     static isShowingMenu()
     {
         return new Promise(resolve => {
index 14147c5..e7cc55d 100644 (file)
@@ -1,3 +1,48 @@
+2019-05-08  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Add a quirk to synthesize mouse events when modifying the selection
+        https://bugs.webkit.org/show_bug.cgi?id=197683
+        <rdar://problem/48003980>
+
+        Reviewed by Tim Horton.
+
+        See WebKit ChangeLog for more details.
+
+        Test: editing/selection/ios/dispatch-mouse-events-when-modifying-selection-quirk.html
+
+        * page/EventHandler.cpp:
+        (WebCore::EventHandler::handleMousePressEvent):
+        (WebCore::EventHandler::supportsSelectionUpdatesOnMouseDrag const):
+
+        Add some platform hooks to prevent mousemove events from updating the selection on iOS.
+
+        (WebCore::EventHandler::shouldAllowMouseDownToStartDrag const):
+
+        Add some platform hooks to prevent drag and drop from kicking in when sending synthetic mousemove events to the
+        page on iOS (drag and drop is instead triggered by EventHandler::tryToBeginDragAtPoint).
+
+        (WebCore::EventHandler::updateSelectionForMouseDrag):
+        * page/EventHandler.h:
+        * page/Quirks.cpp:
+        (WebCore::Quirks::shouldDispatchSyntheticMouseEventsWhenModifyingSelection const):
+        * page/Quirks.h:
+
+        Add the new site-specific quirk.
+
+        * page/Settings.yaml:
+        * page/ios/EventHandlerIOS.mm:
+        (WebCore::EventHandler::tryToBeginDragAtPoint):
+        (WebCore::EventHandler::supportsSelectionUpdatesOnMouseDrag const):
+        (WebCore::EventHandler::shouldAllowMouseDownToStartDrag const):
+        * testing/InternalSettings.cpp:
+        (WebCore::InternalSettings::Backup::Backup):
+        (WebCore::InternalSettings::Backup::restoreTo):
+        (WebCore::InternalSettings::setShouldDispatchSyntheticMouseEventsWhenModifyingSelection):
+        * testing/InternalSettings.h:
+        * testing/InternalSettings.idl:
+
+        Add an internal settings hook to opt into this quirk, for use in layout tests.
+
 2019-05-08  Simon Fraser  <simon.fraser@apple.com>
 
         Implement backing-sharing in compositing layers, allowing overlap layers to paint into the backing store of another layer
index 22f099c..75870d7 100644 (file)
@@ -776,7 +776,7 @@ bool EventHandler::handleMousePressEvent(const MouseEventWithHitTestResults& eve
 
     // Single mouse down on links or images can always trigger drag-n-drop.
     bool isMouseDownOnLinkOrImage = event.isOverLink() || event.hitTestResult().image();
-    m_mouseDownMayStartDrag = singleClick && (!event.event().shiftKey() || isMouseDownOnLinkOrImage);
+    m_mouseDownMayStartDrag = singleClick && (!event.event().shiftKey() || isMouseDownOnLinkOrImage) && shouldAllowMouseDownToStartDrag();
 #endif
 
     m_mouseDownWasSingleClickInSelection = false;
@@ -847,6 +847,21 @@ VisiblePosition EventHandler::selectionExtentRespectingEditingBoundary(const Vis
 }
 
 #if ENABLE(DRAG_SUPPORT)
+
+#if !PLATFORM(IOS_FAMILY)
+
+bool EventHandler::supportsSelectionUpdatesOnMouseDrag() const
+{
+    return true;
+}
+
+bool EventHandler::shouldAllowMouseDownToStartDrag() const
+{
+    return true;
+}
+
+#endif
+
 bool EventHandler::handleMouseDraggedEvent(const MouseEventWithHitTestResults& event, CheckDragHysteresis checkDragHysteresis)
 {
     if (!m_mousePressed)
@@ -926,6 +941,9 @@ bool EventHandler::eventMayStartDrag(const PlatformMouseEvent& event) const
 
 void EventHandler::updateSelectionForMouseDrag()
 {
+    if (!supportsSelectionUpdatesOnMouseDrag())
+        return;
+
     FrameView* view = m_frame.view();
     if (!view)
         return;
@@ -941,6 +959,9 @@ void EventHandler::updateSelectionForMouseDrag()
 
 void EventHandler::updateSelectionForMouseDrag(const HitTestResult& hitTestResult)
 {
+    if (!supportsSelectionUpdatesOnMouseDrag())
+        return;
+
     if (!m_mouseDownMayStartSelect)
         return;
 
index 7fe97a4..f6c3fc8 100644 (file)
@@ -365,6 +365,7 @@ private:
 
 #if ENABLE(DRAG_SUPPORT)
     bool handleMouseDraggedEvent(const MouseEventWithHitTestResults&, CheckDragHysteresis = ShouldCheckDragHysteresis);
+    bool shouldAllowMouseDownToStartDrag() const;
 #endif
 
     WEBCORE_EXPORT bool handleMouseReleaseEvent(const MouseEventWithHitTestResults&);
@@ -457,6 +458,7 @@ private:
 
 #if ENABLE(DRAG_SUPPORT)
     DragSourceAction updateDragSourceActionsAllowed() const;
+    bool supportsSelectionUpdatesOnMouseDrag() const;
 #endif
 
     // The following are called at the beginning of handleMouseUp and handleDrag.  
@@ -616,6 +618,10 @@ private:
     bool m_didStartDrag { false };
     bool m_isHandlingWheelEvent { false };
 
+#if PLATFORM(IOS_FAMILY)
+    bool m_shouldAllowMouseDownToStartDrag { false };
+#endif
+
 #if ENABLE(CURSOR_VISIBILITY)
     Timer m_autoHideCursorTimer;
 #endif
index b1f1ffd..522a33a 100644 (file)
@@ -237,6 +237,24 @@ static bool shouldSuppressAutocorrectionAndAutocaptializationInHiddenEditableAre
 
 #endif
 
+bool Quirks::shouldDispatchSyntheticMouseEventsWhenModifyingSelection() const
+{
+    if (m_document->settings().shouldDispatchSyntheticMouseEventsWhenModifyingSelection())
+        return true;
+
+    if (!needsQuirks())
+        return false;
+
+    auto host = m_document->topDocument().url().host();
+    if (equalLettersIgnoringASCIICase(host, "medium.com") || host.endsWithIgnoringASCIICase(".medium.com"))
+        return true;
+
+    if (equalLettersIgnoringASCIICase(host, "weebly.com") || host.endsWithIgnoringASCIICase(".weebly.com"))
+        return true;
+
+    return false;
+}
+
 bool Quirks::shouldSuppressAutocorrectionAndAutocaptializationInHiddenEditableAreas() const
 {
     if (!needsQuirks())
index e920bc3..7689f70 100644 (file)
@@ -52,6 +52,7 @@ public:
     bool shouldDisablePointerEventsQuirk() const;
     bool needsInputModeNoneImplicitly(const HTMLElement&) const;
 
+    WEBCORE_EXPORT bool shouldDispatchSyntheticMouseEventsWhenModifyingSelection() const;
     WEBCORE_EXPORT bool shouldSuppressAutocorrectionAndAutocaptializationInHiddenEditableAreas() const;
     WEBCORE_EXPORT bool isTouchBarUpdateSupressedForHiddenContentEditable() const;
     WEBCORE_EXPORT bool isNeverRichlyEditableForTouchBar() const;
index d30c815..a34b82d 100644 (file)
@@ -834,6 +834,9 @@ blockingOfSmallPluginsEnabled:
 shouldDecidePolicyBeforeLoadingQuickLookPreview:
   initial: false
 
+shouldDispatchSyntheticMouseEventsWhenModifyingSelection:
+  initial: false
+
 # Deprecated
 
 iceCandidateFilteringEnabled:
index 51a2d5e..f91dd58 100644 (file)
@@ -671,6 +671,8 @@ bool EventHandler::tryToBeginDragAtPoint(const IntPoint& clientPosition, const I
     if (!document)
         return false;
 
+    SetForScope<bool> shouldAllowMouseDownToStartDrag { m_shouldAllowMouseDownToStartDrag, true };
+
     document->updateLayoutIgnorePendingStylesheets();
 
     FloatPoint adjustedClientPositionAsFloatPoint(clientPosition);
@@ -699,7 +701,17 @@ bool EventHandler::tryToBeginDragAtPoint(const IntPoint& clientPosition, const I
     return handledDrag;
 }
 
-#endif
+bool EventHandler::supportsSelectionUpdatesOnMouseDrag() const
+{
+    return false;
+}
+
+bool EventHandler::shouldAllowMouseDownToStartDrag() const
+{
+    return m_shouldAllowMouseDownToStartDrag;
+}
+
+#endif // ENABLE(DRAG_SUPPORT)
 
 }
 
index 13ccd5d..39cffd5 100644 (file)
@@ -100,6 +100,7 @@ InternalSettings::Backup::Backup(Settings& settings)
     , m_deferredCSSParserEnabled(settings.deferredCSSParserEnabled())
     , m_inputEventsEnabled(settings.inputEventsEnabled())
     , m_incompleteImageBorderEnabled(settings.incompleteImageBorderEnabled())
+    , m_shouldDispatchSyntheticMouseEventsWhenModifyingSelection(settings.shouldDispatchSyntheticMouseEventsWhenModifyingSelection())
     , m_shouldDeactivateAudioSession(PlatformMediaSessionManager::shouldDeactivateAudioSession())
     , m_userInterfaceDirectionPolicy(settings.userInterfaceDirectionPolicy())
     , m_systemLayoutDirection(settings.systemLayoutDirection())
@@ -207,6 +208,7 @@ void InternalSettings::Backup::restoreTo(Settings& settings)
     FontCache::singleton().setShouldMockBoldSystemFontForAccessibility(m_shouldMockBoldSystemFontForAccessibility);
     settings.setFrameFlattening(m_frameFlattening);
     settings.setIncompleteImageBorderEnabled(m_incompleteImageBorderEnabled);
+    settings.setShouldDispatchSyntheticMouseEventsWhenModifyingSelection(m_shouldDispatchSyntheticMouseEventsWhenModifyingSelection);
     PlatformMediaSessionManager::setShouldDeactivateAudioSession(m_shouldDeactivateAudioSession);
 
 #if ENABLE(INDEXED_DATABASE_IN_WORKERS)
@@ -926,6 +928,14 @@ ExceptionOr<void> InternalSettings::setIncompleteImageBorderEnabled(bool enabled
     return { };
 }
 
+ExceptionOr<void> InternalSettings::setShouldDispatchSyntheticMouseEventsWhenModifyingSelection(bool shouldDispatch)
+{
+    if (!m_page)
+        return Exception { InvalidAccessError };
+    settings().setShouldDispatchSyntheticMouseEventsWhenModifyingSelection(shouldDispatch);
+    return { };
+}
+
 static InternalSettings::ForcedAccessibilityValue settingsToInternalSettingsValue(Settings::ForcedAccessibilityValue value)
 {
     switch (value) {
index 8ff0f4b..fa73394 100644 (file)
@@ -103,6 +103,7 @@ public:
     ExceptionOr<void> setShouldManageAudioSessionCategory(bool);
     ExceptionOr<void> setCustomPasteboardDataEnabled(bool);
     ExceptionOr<void> setIncompleteImageBorderEnabled(bool);
+    ExceptionOr<void> setShouldDispatchSyntheticMouseEventsWhenModifyingSelection(bool);
 
     using FrameFlatteningValue = FrameFlattening;
     ExceptionOr<void> setFrameFlattening(FrameFlatteningValue);
@@ -198,6 +199,7 @@ private:
         bool m_deferredCSSParserEnabled;
         bool m_inputEventsEnabled;
         bool m_incompleteImageBorderEnabled;
+        bool m_shouldDispatchSyntheticMouseEventsWhenModifyingSelection;
         bool m_shouldDeactivateAudioSession;
         UserInterfaceDirectionPolicy m_userInterfaceDirectionPolicy;
         TextDirection m_systemLayoutDirection;
index 20a3785..aaf24a9 100644 (file)
@@ -88,6 +88,7 @@ enum FontLoadTimingOverride { "Block", "Swap", "Failure" };
     [MayThrowException] void setInlineMediaPlaybackRequiresPlaysInlineAttribute(boolean requires);
     [MayThrowException] void setFrameFlattening(FrameFlatteningValue frameFlattening);
     [MayThrowException] void setIncompleteImageBorderEnabled(boolean enabled);
+    [MayThrowException] void setShouldDispatchSyntheticMouseEventsWhenModifyingSelection(boolean shouldDispatch);
 
     // RuntimeEnabledFeatures.
     void setIndexedDBWorkersEnabled(boolean enabled);
index 4a41917..94d1685 100644 (file)
@@ -1,3 +1,58 @@
+2019-05-08  Wenson Hsieh  <wenson_hsieh@apple.com>
+
+        [iOS] Add a quirk to synthesize mouse events when modifying the selection
+        https://bugs.webkit.org/show_bug.cgi?id=197683
+        <rdar://problem/48003980>
+
+        Reviewed by Tim Horton.
+
+        Introduces support for dispatching synthetic mouse events when modifying the selection on some websites. See
+        below for more details.
+
+        * UIProcess/WebPageProxy.cpp:
+        (WebKit::WebPageProxy::selectAll):
+        * UIProcess/WebPageProxy.h:
+
+        Instead of executing a "SelectAll" editing command using the generic WebPage::executeEditCommand method,
+        introduce a separate method for selectAll that executes the "SelectAll" edit command and then does some
+        platform-specific work. See platformDidSelectAll.
+
+        * UIProcess/ios/WKContentViewInteraction.mm:
+        (-[WKContentView selectAllForWebView:]):
+        * WebProcess/WebPage/WebPage.cpp:
+        (WebKit::WebPage::selectAll):
+        (WebKit::WebPage::shouldDispatchSyntheticMouseEventsWhenModifyingSelection const):
+
+        Add a helper method to determine whether the quirk should be enabled.
+
+        (WebKit::WebPage::platformDidSelectAll):
+        * WebProcess/WebPage/WebPage.h:
+        * WebProcess/WebPage/WebPage.messages.in:
+        * WebProcess/WebPage/ios/WebPageIOS.mm:
+        (WebKit::elementRectInRootViewCoordinates):
+
+        Move this function closer to the top of the file so that it can be used in
+        dispatchSyntheticMouseEventsForSelectionGesture.
+
+        (WebKit::WebPage::clearSelection):
+        (WebKit::WebPage::dispatchSyntheticMouseEventsForSelectionGesture):
+
+        Add a helper method to dispatch a synthetic mouse event for a given selection gesture type. Used in several
+        places in WebPageIOS to synthesize and dispatch mouse events during selection.
+
+        (WebKit::WebPage::updateSelectionWithTouches):
+
+        When changing the selection with selection handles, fake mousedown when the user first touches down on the
+        selection handle; mousemove as the user is moving the handle around; and finally, mouseup when the user lets go.
+
+        (WebKit::WebPage::extendSelection):
+        (WebKit::WebPage::platformDidSelectAll):
+
+        When tapping "Select All" and/or "Select" in the callout menu, fake a mousedown at the selection start, then a
+        mousemove at selection end, and finally, a mouseup at selection end.
+
+        (WebKit::WebPage::getFocusedElementInformation):
+
 2019-05-08  Alexander Mikhaylenko  <exalm7659@gmail.com>
 
         [GTK] Pinch Zooming has no maximum
index 4e1aef9..313a9fa 100644 (file)
@@ -2014,6 +2014,14 @@ void WebPageProxy::scheduleFullEditorStateUpdate()
     m_process->send(Messages::WebPage::ScheduleFullEditorStateUpdate(), m_pageID);
 }
 
+void WebPageProxy::selectAll()
+{
+    if (!hasRunningProcess())
+        return;
+
+    m_process->send(Messages::WebPage::SelectAll(), m_pageID);
+}
+
 void WebPageProxy::executeEditCommand(const String& commandName, const String& argument, WTF::Function<void(CallbackBase::Error)>&& callbackFunction)
 {
     if (!hasRunningProcess()) {
index 8720eeb..f2c4315 100644 (file)
@@ -595,6 +595,7 @@ public:
 
     void addMIMETypeWithCustomContentProvider(const String& mimeType);
 
+    void selectAll();
     void executeEditCommand(const String& commandName, const String& argument = String());
     void validateCommand(const String& commandName, WTF::Function<void (const String&, bool, int32_t, CallbackBase::Error)>&&);
 
index abf883c..aed5449 100644 (file)
@@ -3079,7 +3079,7 @@ WEBCORE_COMMAND_FOR_WEBVIEW(pasteAndMatchStyle);
 - (void)selectAllForWebView:(id)sender
 {
     [_textSelectionAssistant selectAll:sender];
-    _page->executeEditCommand("selectAll"_s);
+    _page->selectAll();
 }
 
 - (void)toggleBoldfaceForWebView:(id)sender
index 8b26395..a5250b7 100644 (file)
@@ -1101,6 +1101,26 @@ void WebPage::executeEditCommandWithCallback(const String& commandName, const St
     send(Messages::WebPageProxy::VoidCallback(callbackID));
 }
 
+void WebPage::selectAll()
+{
+    executeEditingCommand("SelectAll"_s, { });
+    platformDidSelectAll();
+}
+
+bool WebPage::shouldDispatchSyntheticMouseEventsWhenModifyingSelection() const
+{
+    auto* document = m_page->mainFrame().document();
+    return document && document->quirks().shouldDispatchSyntheticMouseEventsWhenModifyingSelection();
+}
+
+#if !PLATFORM(IOS_FAMILY)
+
+void WebPage::platformDidSelectAll()
+{
+}
+
+#endif // !PLATFORM(IOS_FAMILY)
+
 void WebPage::updateEditorStateAfterLayoutIfEditabilityChanged()
 {
     // FIXME: We should update EditorStateIsContentEditable to track whether the state is richly
index 90e408f..6eccb3e 100644 (file)
@@ -599,6 +599,7 @@ public:
     void disabledAdaptationsDidChange(const OptionSet<WebCore::DisabledAdaptations>&);
     void viewportPropertiesDidChange(const WebCore::ViewportArguments&);
     void executeEditCommandWithCallback(const String&, const String& argument, CallbackID);
+    void selectAll();
 
     void textInputContextsInRect(WebCore::FloatRect, CompletionHandler<void(const Vector<WebKit::TextInputContext>&)>&&);
     void focusTextInputContext(const TextInputContext&, CompletionHandler<void(bool)>&&);
@@ -1228,6 +1229,7 @@ private:
     void resetTextAutosizing();
     WebCore::VisiblePosition visiblePositionInFocusedNodeForPoint(const WebCore::Frame&, const WebCore::IntPoint&, bool isInteractingWithFocusedElement);
     RefPtr<WebCore::Range> rangeForGranularityAtPoint(WebCore::Frame&, const WebCore::IntPoint&, uint32_t granularity, bool isInteractingWithFocusedElement);
+    void dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch, const WebCore::IntPoint&);
 
     void sendPositionInformation(InteractionInformationAtPosition&&);
     InteractionInformationAtPosition positionInformation(const InteractionInformationRequest&);
@@ -1466,6 +1468,9 @@ private:
     void capitalizeWord();
 #endif
 
+    bool shouldDispatchSyntheticMouseEventsWhenModifyingSelection() const;
+    void platformDidSelectAll();
+
 #if ENABLE(CONTEXT_MENUS)
     void didSelectItemFromActiveContextMenu(const WebContextMenuItemData&);
 #endif
index 0123fd9..c012631 100644 (file)
@@ -202,6 +202,7 @@ messages -> WebPage LegacyReceiver {
     RunJavaScriptInFrame(uint64_t frameID, String script, bool forceUserGesture, WebKit::CallbackID callbackID)
     ForceRepaint(WebKit::CallbackID callbackID)
 
+    SelectAll()
     ScheduleFullEditorStateUpdate()
 
 #if PLATFORM(COCOA)
index c2db1f5..0d05744 100644 (file)
@@ -1479,12 +1479,61 @@ static RefPtr<Range> rangeAtWordBoundaryForPosition(Frame* frame, const VisibleP
     return (base < extent) ? Range::create(*frame->document(), base, extent) : Range::create(*frame->document(), extent, base);
 }
 
-void WebPage::clearSelection(){
+static IntRect elementRectInRootViewCoordinates(const Element& element)
+{
+    auto* frame = element.document().frame();
+    if (!frame)
+        return { };
+
+    auto* view = frame->view();
+    if (!view)
+        return { };
+
+    auto* renderer = element.renderer();
+    if (!renderer)
+        return { };
+
+    return view->contentsToRootView(renderer->absoluteBoundingBoxRect());
+}
+
+void WebPage::clearSelection()
+{
     m_startingGestureRange = nullptr;
     m_currentBlockSelection = nullptr;
     m_page->focusController().focusedOrMainFrame().selection().clear();
 }
 
+void WebPage::dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch touch, const IntPoint& point)
+{
+    auto frame = makeRef(m_page->focusController().focusedOrMainFrame());
+    if (!frame->selection().selection().isContentEditable())
+        return;
+
+    IntRect focusedElementRect;
+    if (m_focusedElement)
+        focusedElementRect = elementRectInRootViewCoordinates(*m_focusedElement);
+
+    if (focusedElementRect.isEmpty())
+        return;
+
+    auto adjustedPoint = point.constrainedBetween(focusedElementRect.minXMinYCorner(), focusedElementRect.maxXMaxYCorner());
+    auto& eventHandler = m_page->mainFrame().eventHandler();
+    switch (touch) {
+    case SelectionTouch::Started:
+        eventHandler.handleMousePressEvent({ adjustedPoint, adjustedPoint, LeftButton, PlatformEvent::MousePressed, 1, false, false, false, false, WallTime::now(), WebCore::ForceAtClick, NoTap });
+        break;
+    case SelectionTouch::Moved:
+        eventHandler.dispatchSyntheticMouseMove({ adjustedPoint, adjustedPoint, LeftButton, PlatformEvent::MouseMoved, 0, false, false, false, false, WallTime::now(), WebCore::ForceAtClick, NoTap });
+        break;
+    case SelectionTouch::Ended:
+    case SelectionTouch::EndedMovingForward:
+    case SelectionTouch::EndedMovingBackward:
+    case SelectionTouch::EndedNotMoving:
+        eventHandler.handleMouseReleaseEvent({ adjustedPoint, adjustedPoint, LeftButton, PlatformEvent::MouseReleased, 1, false, false, false, false, WallTime::now(), WebCore::ForceAtClick, NoTap });
+        break;
+    }
+}
+
 void WebPage::updateSelectionWithTouches(const IntPoint& point, uint32_t touches, bool baseIsStart, CallbackID callbackID)
 {
     Frame& frame = m_page->focusController().focusedOrMainFrame();
@@ -1499,7 +1548,11 @@ void WebPage::updateSelectionWithTouches(const IntPoint& point, uint32_t touches
     VisiblePosition result;
     SelectionFlags flags = None;
 
-    switch (static_cast<SelectionTouch>(touches)) {
+    auto selectionTouch = static_cast<SelectionTouch>(touches);
+    if (shouldDispatchSyntheticMouseEventsWhenModifyingSelection())
+        dispatchSyntheticMouseEventsForSelectionGesture(selectionTouch, point);
+
+    switch (selectionTouch) {
     case SelectionTouch::Started:
     case SelectionTouch::EndedNotMoving:
         break;
@@ -1557,7 +1610,36 @@ void WebPage::extendSelection(uint32_t granularity)
         return;
 
     VisiblePosition position = frame.selection().selection().start();
-    frame.selection().setSelectedRange(wordRangeFromPosition(position).get(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered);
+    auto wordRange = wordRangeFromPosition(position);
+    if (!wordRange)
+        return;
+
+    IntPoint endLocationForSyntheticMouseEvents;
+    bool shouldDispatchMouseEvents = shouldDispatchSyntheticMouseEventsWhenModifyingSelection();
+    if (shouldDispatchMouseEvents) {
+        auto startLocationForSyntheticMouseEvents = frame.view()->contentsToRootView(VisiblePosition(wordRange->startPosition()).absoluteCaretBounds()).center();
+        endLocationForSyntheticMouseEvents = frame.view()->contentsToRootView(VisiblePosition(wordRange->endPosition()).absoluteCaretBounds()).center();
+        dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Started, startLocationForSyntheticMouseEvents);
+        dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Moved, endLocationForSyntheticMouseEvents);
+    }
+
+    frame.selection().setSelectedRange(wordRange.get(), position.affinity(), WebCore::FrameSelection::ShouldCloseTyping::Yes, UserTriggered);
+
+    if (shouldDispatchMouseEvents)
+        dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Ended, endLocationForSyntheticMouseEvents);
+}
+
+void WebPage::platformDidSelectAll()
+{
+    if (!shouldDispatchSyntheticMouseEventsWhenModifyingSelection())
+        return;
+
+    auto frame = makeRef(m_page->focusController().focusedOrMainFrame());
+    auto startCaretRect = frame->view()->contentsToRootView(VisiblePosition(frame->selection().selection().start()).absoluteCaretBounds());
+    auto endCaretRect = frame->view()->contentsToRootView(VisiblePosition(frame->selection().selection().end()).absoluteCaretBounds());
+    dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Started, startCaretRect.center());
+    dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Moved, endCaretRect.center());
+    dispatchSyntheticMouseEventsForSelectionGesture(SelectionTouch::Ended, endCaretRect.center());
 }
 
 void WebPage::selectWordBackward()
@@ -2584,19 +2666,6 @@ void WebPage::focusNextFocusedElement(bool isForward, CallbackID callbackID)
     send(Messages::WebPageProxy::VoidCallback(callbackID));
 }
 
-static IntRect elementRectInRootViewCoordinates(const Node& node, const Frame& frame)
-{
-    auto* view = frame.view();
-    if (!view)
-        return { };
-
-    auto* renderer = node.renderer();
-    if (!renderer)
-        return { };
-
-    return view->contentsToRootView(renderer->absoluteBoundingBoxRect());
-}
-
 void WebPage::getFocusedElementInformation(FocusedElementInformation& information)
 {
     layoutIfNeeded();
@@ -2604,8 +2673,7 @@ void WebPage::getFocusedElementInformation(FocusedElementInformation& informatio
     information.lastInteractionLocation = m_lastInteractionLocation;
 
     if (auto* renderer = m_focusedElement->renderer()) {
-        auto& elementFrame = m_page->focusController().focusedOrMainFrame();
-        information.elementRect = elementRectInRootViewCoordinates(*m_focusedElement, elementFrame);
+        information.elementRect = elementRectInRootViewCoordinates(*m_focusedElement);
         information.nodeFontSize = renderer->style().fontDescription().computedSize();
 
         bool inFixed = false;
@@ -2621,13 +2689,11 @@ void WebPage::getFocusedElementInformation(FocusedElementInformation& informatio
     information.allowsUserScaling = m_viewportConfiguration.allowsUserScaling();
     information.allowsUserScalingIgnoringAlwaysScalable = m_viewportConfiguration.allowsUserScalingIgnoringAlwaysScalable();
     if (auto* nextElement = nextAssistableElement(m_focusedElement.get(), *m_page, true)) {
-        if (auto* frame = nextElement->document().frame())
-            information.nextNodeRect = elementRectInRootViewCoordinates(*nextElement, *frame);
+        information.nextNodeRect = elementRectInRootViewCoordinates(*nextElement);
         information.hasNextNode = true;
     }
     if (auto* previousElement = nextAssistableElement(m_focusedElement.get(), *m_page, false)) {
-        if (auto* frame = previousElement->document().frame())
-            information.previousNodeRect = elementRectInRootViewCoordinates(*previousElement, *frame);
+        information.previousNodeRect = elementRectInRootViewCoordinates(*previousElement);
         information.hasPreviousNode = true;
     }
     information.focusedElementIdentifier = m_currentFocusedElementIdentifier;