Changeset 241971 in webkit
- Timestamp:
- Feb 22, 2019 4:48:16 PM (5 years ago)
- Location:
- trunk
- Files:
-
- 2 added
- 17 edited
Legend:
- Unmodified
- Added
- Removed
-
trunk/LayoutTests/ChangeLog
r241949 r241971 1 2019-02-22 Wenson Hsieh <wenson_hsieh@apple.com> 2 3 [iOS] Callout menu overlaps in-page controls when editing a comment in github.com's issue tracker 4 https://bugs.webkit.org/show_bug.cgi?id=194873 5 <rdar://problem/46701974> 6 7 Reviewed by Tim Horton. 8 9 Add a test to ensure that the we dodge clickable elements when showing the callout bar. 10 11 * editing/selection/ios/avoid-showing-callout-menu-over-controls-expected.txt: Added. 12 * editing/selection/ios/avoid-showing-callout-menu-over-controls.html: Added. 13 * resources/ui-helper.js: 14 (window.UIHelper.waitForMenuToShow.return.new.Promise): 15 (window.UIHelper.waitForMenuToShow): 16 (window.UIHelper.menuRect): 17 (window.UIHelper): 18 1 19 2019-02-22 Wenson Hsieh <wenson_hsieh@apple.com> 2 20 -
trunk/LayoutTests/resources/ui-helper.js
r241719 r241971 666 666 return new Promise(resolve => testRunner.runUIScript(script, result => resolve(JSON.parse(result)))); 667 667 } 668 669 static waitForMenuToShow() 670 { 671 return new Promise(resolve => { 672 testRunner.runUIScript(` 673 (function() { 674 if (!uiController.isShowingMenu) 675 uiController.didShowMenuCallback = () => uiController.uiScriptComplete(); 676 else 677 uiController.uiScriptComplete(); 678 })()`, resolve); 679 }); 680 } 681 682 static menuRect() 683 { 684 return new Promise(resolve => { 685 testRunner.runUIScript("JSON.stringify(uiController.menuRect)", result => resolve(JSON.parse(result))); 686 }); 687 } 668 688 } -
trunk/Source/WebKit/ChangeLog
r241969 r241971 1 2019-02-22 Wenson Hsieh <wenson_hsieh@apple.com> 2 3 [iOS] Callout menu overlaps in-page controls when editing a comment in github.com's issue tracker 4 https://bugs.webkit.org/show_bug.cgi?id=194873 5 <rdar://problem/46701974> 6 7 Reviewed by Tim Horton. 8 9 On the topic of supporting web-based rich text editors on iOS, one problematic area has always been handling 10 conflicts between platform UI (i.e., the system callout menu) and in-page text editing controls. This issue 11 comes up in websites that don't use the "hidden contenteditable" approach to rich text editing, but also show 12 additional controls in a toolbar or contextual menu above the selection. In these cases, what often happens is 13 that system controls overlap controls in the page. 14 15 Luckily, the iOS callout menu (i.e. the private UICalloutBar) is capable of presenting with a list of "evasion 16 rects" to avoid; if the callout bar would normally intersect with one of these rects, then a different 17 orientation that does not intersect with one of these rects is chosen instead. Currently, the only rect added 18 here by UIKit when presenting the callout menu is the bounding rect of the on-screen keyboard, but after 19 <rdar://problem/48128337>, we now have a generalized mechanism for offering additional evasion rects before 20 UIKit presents the callout menu. 21 22 This patch adopts the mechanism introduced in <rdar://problem/48128337>, and introduces a heuristic for 23 determining the approximate location of controls in the page which might overlap the callout menu. This 24 heuristic works by hit-testing for clickable (but non-editable) nodes above the bounds of the selection, which 25 are additionally not hit-tested by advancing outwards from any of the other edges of the selection bounds. 26 Additionally, any hit-tested nodes whose bounding rects are very large (relative to the content view size) are 27 ignored (this deals with scenarios where the body or a large container element has a click handler). We then add 28 the bounding rects of each of the nodes that fit this criteria to the list of rects for UIKit to avoid when 29 presenting the system callout menu. 30 31 The result is that WebKit will, by default, avoid overlapping anything that looks like controls in the page when 32 showing a callout menu in editable content. In practice, this fixes overlapping controls on most websites that 33 roll their own context menu or toolbar in their rich text editor. 34 35 Test: editing/selection/ios/avoid-showing-callout-menu-over-controls.html 36 37 * Platform/spi/ios/UIKitSPI.h: 38 * UIProcess/WebPageProxy.h: 39 * UIProcess/ios/WKContentViewInteraction.mm: 40 (-[WKContentView requestAutocorrectionRectsForString:withCompletionHandler:]): 41 (-[WKContentView requestRectsToEvadeForSelectionCommandsWithCompletionHandler:]): 42 (-[WKContentView requestAutocorrectionContextWithCompletionHandler:]): 43 44 Drive-by: handle null completion handler arguments more gracefully, by raising an NSException and bailing before 45 attempting to invoke a nil block. 46 47 * UIProcess/ios/WebPageProxyIOS.mm: 48 (WebKit::WebPageProxy::requestEvasionRectsAboveSelection): 49 50 See above for more detail. 51 52 * WebProcess/WebPage/WebPage.h: 53 * WebProcess/WebPage/WebPage.messages.in: 54 * WebProcess/WebPage/ios/WebPageIOS.mm: 55 (WebKit::WebPage::requestEvasionRectsAboveSelection): 56 1 57 2019-02-22 Simon Fraser <simon.fraser@apple.com> 2 58 -
trunk/Source/WebKit/Platform/spi/ios/UIKitSPI.h
r241865 r241971 989 989 990 990 @interface UICalloutBar : UIView 991 + (UICalloutBar *)activeCalloutBar; 991 992 + (void)fadeSharedCalloutBar; 992 993 @end -
trunk/Source/WebKit/UIProcess/WebPageProxy.h
r241963 r241971 688 688 void hardwareKeyboardAvailabilityChanged(); 689 689 bool isScrollingOrZooming() const { return m_isScrollingOrZooming; } 690 void requestEvasionRectsAboveSelection(CompletionHandler<void(const Vector<WebCore::FloatRect>&)>&&); 690 691 #if ENABLE(DATA_INTERACTION) 691 692 void didHandleDragStartRequest(bool started); -
trunk/Source/WebKit/UIProcess/ios/WKContentViewInteraction.mm
r241900 r241971 3354 3354 - (void)requestAutocorrectionRectsForString:(NSString *)input withCompletionHandler:(void (^)(UIWKAutocorrectionRects *rectsForInput))completionHandler 3355 3355 { 3356 if (!completionHandler) { 3357 [NSException raise:NSInvalidArgumentException format:@"Expected a nonnull completion handler in %s.", __PRETTY_FUNCTION__]; 3358 return; 3359 } 3360 3356 3361 if (!input || ![input length]) { 3357 if (completionHandler) 3358 completionHandler(nil); 3362 completionHandler(nil); 3359 3363 return; 3360 3364 } … … 3374 3378 view->_autocorrectionData.textLastRect = lastRect; 3375 3379 3376 if (completion) 3377 completion(rects.size() ? [WKAutocorrectionRects autocorrectionRectsWithRects:firstRect lastRect:lastRect] : nil); 3380 completion(rects.size() ? [WKAutocorrectionRects autocorrectionRectsWithRects:firstRect lastRect:lastRect] : nil); 3381 }); 3382 } 3383 3384 - (void)requestRectsToEvadeForSelectionCommandsWithCompletionHandler:(void(^)(NSArray<NSValue *> *rects))completionHandler 3385 { 3386 if (!completionHandler) { 3387 [NSException raise:NSInvalidArgumentException format:@"Expected a nonnull completion handler in %s.", __PRETTY_FUNCTION__]; 3388 return; 3389 } 3390 3391 if ([self _shouldSuppressSelectionCommands] || _webView._editable) { 3392 completionHandler(@[ ]); 3393 return; 3394 } 3395 3396 if (_focusedElementInformation.elementType != WebKit::InputType::ContentEditable && _focusedElementInformation.elementType != WebKit::InputType::TextArea) { 3397 completionHandler(@[ ]); 3398 return; 3399 } 3400 3401 // Give the page some time to present custom editing UI before attempting to detect and evade it. 3402 auto delayBeforeShowingCalloutBar = (0.25_s).nanoseconds(); 3403 dispatch_after(dispatch_time(DISPATCH_TIME_NOW, delayBeforeShowingCalloutBar), dispatch_get_main_queue(), [completion = makeBlockPtr(completionHandler), weakSelf = WeakObjCPtr<WKContentView>(self)] () mutable { 3404 if (!weakSelf) { 3405 completion(@[ ]); 3406 return; 3407 } 3408 3409 auto strongSelf = weakSelf.get(); 3410 if (!strongSelf->_page) { 3411 completion(@[ ]); 3412 return; 3413 } 3414 3415 strongSelf->_page->requestEvasionRectsAboveSelection([completion = WTFMove(completion)] (auto& rects) { 3416 auto rectsAsValues = adoptNS([[NSMutableArray alloc] initWithCapacity:rects.size()]); 3417 for (auto& floatRect : rects) 3418 [rectsAsValues addObject:[NSValue valueWithCGRect:floatRect]]; 3419 completion(rectsAsValues.get()); 3420 }); 3378 3421 }); 3379 3422 } … … 3538 3581 - (void)requestAutocorrectionContextWithCompletionHandler:(void (^)(UIWKAutocorrectionContext *autocorrectionContext))completionHandler 3539 3582 { 3540 if (!completionHandler) 3541 return; 3583 if (!completionHandler) { 3584 [NSException raise:NSInvalidArgumentException format:@"Expected a nonnull completion handler in %s.", __PRETTY_FUNCTION__]; 3585 return; 3586 } 3542 3587 3543 3588 #if USE(UIKIT_KEYBOARD_ADDITIONS) -
trunk/Source/WebKit/UIProcess/ios/WebPageProxyIOS.mm
r241963 r241971 30 30 31 31 #import "APIUIClient.h" 32 #import "Connection.h" 32 33 #import "DataReference.h" 33 34 #import "EditingRange.h" … … 1113 1114 } 1114 1115 1116 void WebPageProxy::requestEvasionRectsAboveSelection(CompletionHandler<void(const Vector<WebCore::FloatRect>&)>&& callback) 1117 { 1118 if (!isValid()) { 1119 callback({ }); 1120 return; 1121 } 1122 1123 m_process->connection()->sendWithAsyncReply(Messages::WebPage::RequestEvasionRectsAboveSelection(), WTFMove(callback), m_pageID); 1124 } 1125 1115 1126 #if ENABLE(DATA_INTERACTION) 1116 1127 -
trunk/Source/WebKit/WebProcess/WebPage/WebPage.h
r241721 r241971 665 665 void startAutoscrollAtPosition(const WebCore::FloatPoint&); 666 666 void cancelAutoscroll(); 667 void requestEvasionRectsAboveSelection(CompletionHandler<void(const Vector<WebCore::FloatRect>&)>&&); 667 668 668 669 void contentSizeCategoryDidChange(const String&); -
trunk/Source/WebKit/WebProcess/WebPage/WebPage.messages.in
r241282 r241971 81 81 RequestAutocorrectionContext(WebKit::CallbackID callbackID) 82 82 AutocorrectionContextSync() -> (struct WebKit::WebAutocorrectionContext context) Delayed 83 RequestEvasionRectsAboveSelection() -> (Vector<WebCore::FloatRect> rects) Async 83 84 GetPositionInformation(struct WebKit::InteractionInformationRequest request) -> (struct WebKit::InteractionInformationAtPosition information) Delayed 84 85 RequestPositionInformation(struct WebKit::InteractionInformationRequest request) -
trunk/Source/WebKit/WebProcess/WebPage/ios/WebPageIOS.mm
r241934 r241971 1505 1505 } 1506 1506 1507 void WebPage::requestEvasionRectsAboveSelection(CompletionHandler<void(const Vector<FloatRect>&)>&& reply) 1508 { 1509 auto& frame = m_page->focusController().focusedOrMainFrame(); 1510 auto frameView = makeRefPtr(frame.view()); 1511 if (!frameView) { 1512 reply({ }); 1513 return; 1514 } 1515 1516 auto& selection = frame.selection().selection(); 1517 if (selection.isNone()) { 1518 reply({ }); 1519 return; 1520 } 1521 1522 auto selectedRange = selection.toNormalizedRange(); 1523 if (!selectedRange) { 1524 reply({ }); 1525 return; 1526 } 1527 1528 if (!m_focusedElement || !m_focusedElement->renderer() || m_focusedElement->renderer()->isTransparentOrFullyClippedRespectingParentFrames()) { 1529 reply({ }); 1530 return; 1531 } 1532 1533 float scaleFactor = pageScaleFactor(); 1534 const double factorOfContentArea = 0.5; 1535 auto unobscuredContentArea = m_page->mainFrame().view()->unobscuredContentRect().area(); 1536 if (unobscuredContentArea.hasOverflowed()) { 1537 reply({ }); 1538 return; 1539 } 1540 1541 double contextMenuAreaLimit = factorOfContentArea * scaleFactor * unobscuredContentArea.unsafeGet(); 1542 1543 FloatRect selectionBoundsInRootViewCoordinates; 1544 if (selection.isRange()) 1545 selectionBoundsInRootViewCoordinates = frameView->contentsToRootView(selectedRange->absoluteBoundingBox()); 1546 else 1547 selectionBoundsInRootViewCoordinates = frameView->contentsToRootView(frame.selection().absoluteCaretBounds()); 1548 1549 auto centerOfTargetBounds = selectionBoundsInRootViewCoordinates.center(); 1550 FloatPoint centerTopInRootViewCoordinates { centerOfTargetBounds.x(), selectionBoundsInRootViewCoordinates.y() }; 1551 1552 auto clickableNonEditableNode = [&] (const FloatPoint& locationInRootViewCoordinates) -> Node* { 1553 FloatPoint adjustedPoint; 1554 auto* hitNode = m_page->mainFrame().nodeRespondingToClickEvents(locationInRootViewCoordinates, adjustedPoint); 1555 if (!hitNode || is<HTMLBodyElement>(hitNode) || is<Document>(hitNode) || hitNode->hasEditableStyle()) 1556 return nullptr; 1557 1558 return hitNode; 1559 }; 1560 1561 // This heuristic attempts to find a list of rects to avoid when showing the callout menu on iOS. 1562 // First, hit-test several points above the bounds of the selection rect in search of clickable nodes that are not editable. 1563 // Secondly, hit-test several points around the edges of the selection rect and exclude any nodes found in the first round of 1564 // hit-testing if these nodes are also reachable by moving outwards from the left, right, or bottom edges of the selection. 1565 // Additionally, exclude any hit-tested nodes that are either very large relative to the size of the root view, or completely 1566 // encompass the selection bounds. The resulting rects are the bounds of these hit-tested nodes in root view coordinates. 1567 HashSet<Ref<Node>> hitTestedNodes; 1568 Vector<FloatRect> rectsToAvoidInRootViewCoordinates; 1569 const Vector<FloatPoint, 5> offsetsForHitTesting {{ -30, -50 }, { 30, -50 }, { -60, -35 }, { 60, -35 }, { 0, -20 }}; 1570 for (auto offset : offsetsForHitTesting) { 1571 offset.scale(1 / scaleFactor); 1572 if (auto* hitNode = clickableNonEditableNode(centerTopInRootViewCoordinates + offset)) 1573 hitTestedNodes.add(*hitNode); 1574 } 1575 1576 const float marginForHitTestingSurroundingNodes = 80 / scaleFactor; 1577 Vector<FloatPoint, 3> exclusionHitTestLocations { 1578 { selectionBoundsInRootViewCoordinates.x() - marginForHitTestingSurroundingNodes, centerOfTargetBounds.y() }, 1579 { centerOfTargetBounds.x(), selectionBoundsInRootViewCoordinates.maxY() + marginForHitTestingSurroundingNodes }, 1580 { selectionBoundsInRootViewCoordinates.maxX() + marginForHitTestingSurroundingNodes, centerOfTargetBounds.y() } 1581 }; 1582 1583 for (auto& location : exclusionHitTestLocations) { 1584 if (auto* nodeToExclude = clickableNonEditableNode(location)) 1585 hitTestedNodes.remove(*nodeToExclude); 1586 } 1587 1588 for (auto& node : hitTestedNodes) { 1589 auto frameView = makeRefPtr(node->document().view()); 1590 auto* renderer = node->renderer(); 1591 if (!renderer || !frameView) 1592 continue; 1593 1594 auto bounds = frameView->contentsToRootView(renderer->absoluteBoundingBoxRect()); 1595 auto area = bounds.area(); 1596 if (area.hasOverflowed() || area.unsafeGet() > contextMenuAreaLimit) 1597 continue; 1598 1599 if (bounds.contains(enclosingIntRect(selectionBoundsInRootViewCoordinates))) 1600 continue; 1601 1602 rectsToAvoidInRootViewCoordinates.append(WTFMove(bounds)); 1603 } 1604 1605 reply(WTFMove(rectsToAvoidInRootViewCoordinates)); 1606 } 1607 1507 1608 void WebPage::getRectsForGranularityWithSelectionOffset(uint32_t granularity, int32_t offset, CallbackID callbackID) 1508 1609 { -
trunk/Tools/ChangeLog
r241963 r241971 1 2019-02-22 Wenson Hsieh <wenson_hsieh@apple.com> 2 3 [iOS] Callout menu overlaps in-page controls when editing a comment in github.com's issue tracker 4 https://bugs.webkit.org/show_bug.cgi?id=194873 5 <rdar://problem/46701974> 6 7 Reviewed by Tim Horton. 8 9 Add a couple of UIScriptController methods to make callout menu testing on iOS easier (see below). 10 11 * DumpRenderTree/ios/UIScriptControllerIOS.mm: 12 (WTR::UIScriptController::menuRect const): 13 (WTR::UIScriptController::isShowingMenu const): 14 * TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl: 15 * TestRunnerShared/UIScriptContext/UIScriptController.cpp: 16 (WTR::UIScriptController::menuRect const): 17 18 Add a function to query the bounds of the callout menu in content coordinates. 19 20 (WTR::UIScriptController::isShowingMenu const): 21 22 Add a function to query whether the callout menu is shown (i.e., has finished its appearance animation). 23 24 * TestRunnerShared/UIScriptContext/UIScriptController.h: 25 * WebKitTestRunner/cocoa/TestRunnerWKWebView.h: 26 * WebKitTestRunner/ios/UIScriptControllerIOS.mm: 27 (WTR::UIScriptController::rectForMenuAction const): 28 (WTR::UIScriptController::menuRect const): 29 (WTR::UIScriptController::isShowingMenu const): 30 (WTR::findViewInHierarchyOfType): Deleted. 31 1 32 2019-02-22 Chris Dumez <cdumez@apple.com> 2 33 -
trunk/Tools/DumpRenderTree/ios/UIScriptControllerIOS.mm
r241322 r241971 338 338 } 339 339 340 JSObjectRef UIScriptController::menuRect() const 341 { 342 return nullptr; 343 } 344 345 bool UIScriptController::isShowingMenu() const 346 { 347 return false; 348 } 349 340 350 void UIScriptController::platformSetDidEndScrollingCallback() 341 351 { -
trunk/Tools/TestRunnerShared/UIScriptContext/Bindings/UIScriptController.idl
r241322 r241971 226 226 attribute object didShowMenuCallback; 227 227 attribute object didHideMenuCallback; 228 readonly attribute boolean isShowingMenu; 229 readonly attribute object menuRect; 228 230 object rectForMenuAction(DOMString action); 229 231 -
trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.cpp
r241322 r241971 533 533 } 534 534 535 JSObjectRef UIScriptController::menuRect() const 536 { 537 return nullptr; 538 } 539 535 540 JSObjectRef UIScriptController::rectForMenuAction(JSStringRef) const 536 541 { 537 542 return nullptr; 543 } 544 545 bool UIScriptController::isShowingMenu() const 546 { 547 return false; 538 548 } 539 549 -
trunk/Tools/TestRunnerShared/UIScriptContext/UIScriptController.h
r241322 r241971 165 165 JSValueRef didShowMenuCallback() const; 166 166 167 bool isShowingMenu() const; 167 168 JSObjectRef rectForMenuAction(JSStringRef action) const; 169 JSObjectRef menuRect() const; 168 170 169 171 void setDidEndScrollingCallback(JSValueRef); -
trunk/Tools/WebKitTestRunner/cocoa/TestRunnerWKWebView.h
r241322 r241971 57 57 58 58 @property (nonatomic, readonly, getter=isShowingKeyboard) BOOL showingKeyboard; 59 @property (nonatomic, readonly, getter=isShowingMenu) BOOL showingMenu; 59 60 @property (nonatomic, assign) BOOL usesSafariLikeRotation; 60 61 @property (nonatomic, readonly, getter=isInteractingWithFormControl) BOOL interactingWithFormControl; -
trunk/Tools/WebKitTestRunner/ios/UIScriptControllerIOS.mm
r241573 r241971 118 118 } 119 119 120 static UIView *findViewInHierarchyOfType(UIView *view, Class viewClass)121 {122 __block RetainPtr<UIView> foundView;123 forEachViewInHierarchy(view, ^(UIView *subview, BOOL *stop) {124 if (![subview isKindOfClass:viewClass])125 return;126 127 foundView = subview;128 *stop = YES;129 });130 return foundView.autorelease();131 }132 133 120 static NSArray<UIView *> *findAllViewsInHierarchyOfType(UIView *view, Class viewClass) 134 121 { … … 884 871 UIWindow *windowForButton = nil; 885 872 UIButton *buttonForAction = nil; 886 for (UIWindow *window in UIApplication.sharedApplication.windows) { 887 if (![window isKindOfClass:UITextEffectsWindow.class]) 873 UIView *calloutBar = UICalloutBar.activeCalloutBar; 874 if (!calloutBar.window) 875 return nullptr; 876 877 for (UIButton *button in findAllViewsInHierarchyOfType(calloutBar, UIButton.class)) { 878 NSString *buttonTitle = [button titleForState:UIControlStateNormal]; 879 if (!buttonTitle.length) 888 880 continue; 889 881 890 UIView *calloutBar = findViewInHierarchyOfType(window, UICalloutBar.class); 891 if (!calloutBar) 882 if (![buttonTitle isEqualToString:(__bridge NSString *)action.get()]) 892 883 continue; 893 884 894 for (UIButton *button in findAllViewsInHierarchyOfType(calloutBar, UIButton.class)) { 895 NSString *buttonTitle = [button titleForState:UIControlStateNormal]; 896 if (!buttonTitle.length) 897 continue; 898 899 if (![buttonTitle isEqualToString:(__bridge NSString *)action.get()]) 900 continue; 901 902 buttonForAction = button; 903 windowForButton = window; 904 } 885 buttonForAction = button; 886 windowForButton = calloutBar.window; 887 break; 905 888 } 906 889 … … 910 893 CGRect rectInRootViewCoordinates = [buttonForAction convertRect:buttonForAction.bounds toView:platformContentView()]; 911 894 return m_context->objectFromRect(WebCore::FloatRect(rectInRootViewCoordinates.origin.x, rectInRootViewCoordinates.origin.y, rectInRootViewCoordinates.size.width, rectInRootViewCoordinates.size.height)); 895 } 896 897 JSObjectRef UIScriptController::menuRect() const 898 { 899 UIView *calloutBar = UICalloutBar.activeCalloutBar; 900 if (!calloutBar.window) 901 return nullptr; 902 903 CGRect rectInRootViewCoordinates = [calloutBar convertRect:calloutBar.bounds toView:platformContentView()]; 904 return m_context->objectFromRect(WebCore::FloatRect(rectInRootViewCoordinates.origin.x, rectInRootViewCoordinates.origin.y, rectInRootViewCoordinates.size.width, rectInRootViewCoordinates.size.height)); 905 } 906 907 bool UIScriptController::isShowingMenu() const 908 { 909 return TestController::singleton().mainWebView()->platformView().showingMenu; 912 910 } 913 911
Note: See TracChangeset
for help on using the changeset viewer.