Index: ios/web/web_state/ui/crw_web_controller.mm |
diff --git a/ios/web/web_state/ui/crw_web_controller.mm b/ios/web/web_state/ui/crw_web_controller.mm |
index 0786c951cc97174e75b826167bc78b2134735ec1..ee3d0d122224a803df08cefc3ec9ae68fba82b52 100644 |
--- a/ios/web/web_state/ui/crw_web_controller.mm |
+++ b/ios/web/web_state/ui/crw_web_controller.mm |
@@ -37,7 +37,6 @@ |
#include "base/strings/utf_string_conversions.h" |
#include "base/time/time.h" |
#include "base/values.h" |
-#include "components/url_formatter/url_formatter.h" |
#import "ios/net/http_response_headers_util.h" |
#import "ios/net/nsurlrequest_util.h" |
#include "ios/web/history_state_util.h" |
@@ -73,6 +72,7 @@ |
#import "ios/web/public/web_state/js/crw_js_injection_receiver.h" |
#include "ios/web/public/web_state/page_display_state.h" |
#import "ios/web/public/web_state/ui/crw_content_view.h" |
+#import "ios/web/public/web_state/ui/crw_context_menu_delegate.h" |
#import "ios/web/public/web_state/ui/crw_native_content.h" |
#import "ios/web/public/web_state/ui/crw_native_content_provider.h" |
#import "ios/web/public/web_state/ui/crw_web_view_content_view.h" |
@@ -87,6 +87,7 @@ |
#import "ios/web/web_state/js/crw_js_post_request_loader.h" |
#import "ios/web/web_state/js/crw_js_window_id_manager.h" |
#import "ios/web/web_state/page_viewport_state.h" |
+#import "ios/web/web_state/ui/crw_context_menu_controller.h" |
#import "ios/web/web_state/ui/crw_swipe_recognizer_provider.h" |
#import "ios/web/web_state/ui/crw_web_controller.h" |
#import "ios/web/web_state/ui/crw_web_controller_container_view.h" |
@@ -217,14 +218,6 @@ enum ExternalURLRequestStatus { |
NUM_EXTERNAL_URL_REQUEST_STATUS |
}; |
-// Cancels touch events for the given gesture recognizer. |
-void CancelTouches(UIGestureRecognizer* gesture_recognizer) { |
- if (gesture_recognizer.enabled) { |
- gesture_recognizer.enabled = NO; |
- gesture_recognizer.enabled = YES; |
- } |
-} |
- |
// Utility function for getting the source of NSErrors received by WKWebViews. |
WKWebViewErrorSource WKWebViewErrorSourceFromError(NSError* error) { |
DCHECK(error); |
@@ -284,7 +277,8 @@ NSError* WKWebViewErrorWithSource(NSError* error, WKWebViewErrorSource source) { |
} |
@end |
-@interface CRWWebController ()<CRWNativeContentDelegate, |
+@interface CRWWebController ()<CRWContextMenuDelegate, |
+ CRWNativeContentDelegate, |
CRWSSLStatusUpdaterDataSource, |
CRWSSLStatusUpdaterDelegate, |
CRWWebControllerContainerViewDelegate, |
@@ -363,15 +357,8 @@ NSError* WKWebViewErrorWithSource(NSError* error, WKWebViewErrorSource source) { |
// The touch tracking recognizer allowing us to decide if a navigation is |
// started by the user. |
base::scoped_nsobject<CRWTouchTrackingRecognizer> _touchTrackingRecognizer; |
- // Long press recognizer that allows showing context menus. |
- base::scoped_nsobject<UILongPressGestureRecognizer> _contextMenuRecognizer; |
- // DOM element information for the point where the user made the last touch. |
- // Can be nil if has not been calculated yet. Precalculation is necessary |
- // because retreiving DOM element relies on async API so element info can not |
- // be built on demand. May contain the following keys: @"href", @"src", |
- // @"title", @"referrerPolicy". All values are strings. Used for showing |
- // context menus. |
- base::scoped_nsobject<NSDictionary> _DOMElementForLastTouch; |
+ // The controller that tracks long press and check context menu trigger. |
+ base::scoped_nsobject<CRWContextMenuController> _contextMenuController; |
// Whether a click is in progress. |
BOOL _clickInProgress; |
// Data on the recorded last user interaction. |
@@ -735,22 +722,6 @@ typedef void (^ViewportStateCompletion)(const web::PageViewportState*); |
// Sets scroll offset value for webview scroll view from |scrollState|. |
- (void)applyWebViewScrollOffsetFromScrollState: |
(const web::PageScrollState&)scrollState; |
-// Asynchronously fetches full width of the rendered web page. |
-- (void)fetchWebPageWidthWithCompletionHandler:(void (^)(CGFloat))handler; |
-// Asynchronously fetches information about DOM element for the given point (in |
-// UIView coordinates). |handler| can not be nil. See |_DOMElementForLastTouch| |
-// for element format description. |
-- (void)fetchDOMElementAtPoint:(CGPoint)point |
- completionHandler:(void (^)(NSDictionary*))handler; |
-// Extracts context menu information from the given DOM element. |
-- (web::ContextMenuParams)contextMenuParamsForElement:(NSDictionary*)element; |
-// Sets the value of |_DOMElementForLastTouch|. |
-- (void)setDOMElementForLastTouch:(NSDictionary*)element; |
-// Called when the window has determined there was a long-press and context menu |
-// must be shown. |
-- (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer; |
-// Cancels all touch events in the web view (long presses, tapping, scrolling). |
-- (void)cancelAllTouches; |
// Returns the referrer for the current page. |
- (web::Referrer)currentReferrer; |
// Asynchronously returns the referrer policy for the current page. |
@@ -989,14 +960,6 @@ namespace { |
NSString* const kReferrerHeaderName = @"Referer"; // [sic] |
-// The long press detection duration must be shorter than the UIWebView's |
-// long click gesture recognizer's minimum duration. That is 0.55s. |
-// If our detection duration is shorter, our gesture recognizer will fire |
-// first, and if it fails the long click gesture (processed simultaneously) |
-// still is able to complete. |
-const NSTimeInterval kLongPressDurationSeconds = 0.55 - 0.1; |
-const CGFloat kLongPressMoveDeltaPixels = 10.0; |
- |
// The duration of the period following a screen touch during which the user is |
// still considered to be interacting with the page. |
const NSTimeInterval kMaximumDelayForUserInteractionInSeconds = 2; |
@@ -1304,48 +1267,6 @@ const NSTimeInterval kSnapshotOverlayTransition = 0.5; |
_webStateImpl = nullptr; |
} |
-- (void)setDOMElementForLastTouch:(NSDictionary*)element { |
- _DOMElementForLastTouch.reset([element copy]); |
-} |
- |
-- (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer { |
- // We don't want ongoing notification that the long press is held. |
- if ([gestureRecognizer state] != UIGestureRecognizerStateBegan) |
- return; |
- |
- if (![_DOMElementForLastTouch count]) |
- return; |
- |
- web::ContextMenuParams params = |
- [self contextMenuParamsForElement:_DOMElementForLastTouch.get()]; |
- params.view.reset([_webView retain]); |
- params.location = [gestureRecognizer locationInView:_webView]; |
- if (self.webStateImpl->HandleContextMenu(params)) { |
- // Cancelling all touches has the intended side effect of suppressing the |
- // system's context menu. |
- [self cancelAllTouches]; |
- } |
-} |
- |
-- (void)cancelAllTouches { |
- // Disable web view scrolling. |
- CancelTouches(self.webScrollView.panGestureRecognizer); |
- |
- // All user gestures are handled by a subview of web view scroll view |
- // (WKContentView). |
- for (UIView* subview in self.webScrollView.subviews) { |
- for (UIGestureRecognizer* recognizer in subview.gestureRecognizers) { |
- CancelTouches(recognizer); |
- } |
- } |
- |
- // Just disabling/enabling the gesture recognizers is not enough to suppress |
- // the click handlers on the JS side. This JS performs the function of |
- // suppressing these handlers on the JS side. |
- NSString* suppressNextClick = @"__gCrWeb.suppressNextClick()"; |
- [self executeJavaScript:suppressNextClick completionHandler:nil]; |
-} |
- |
// TODO(shreyasv): This code is shared with SnapshotManager. Remove this and add |
// it as part of WebDelegate delegate API such that a default image is returned |
// immediately. |
@@ -1583,29 +1504,6 @@ const NSTimeInterval kSnapshotOverlayTransition = 0.5; |
[_windowIDJSManager inject]; |
} |
-// Set the specified recognizer to take priority over any recognizers in the |
-// view that have a description containing the specified text fragment. |
-+ (void)requireGestureRecognizerToFail:(UIGestureRecognizer*)recognizer |
- inView:(UIView*)view |
- containingDescription:(NSString*)fragment { |
- for (UIGestureRecognizer* iRecognizer in [view gestureRecognizers]) { |
- if (iRecognizer != recognizer) { |
- NSString* description = [iRecognizer description]; |
- if ([description rangeOfString:fragment].length) { |
- [iRecognizer requireGestureRecognizerToFail:recognizer]; |
- // requireGestureRecognizerToFail: doesn't retain the recognizer, so it |
- // is possible for |iRecognizer| to outlive |recognizer| and end up with |
- // a dangling pointer. Add a retaining associative reference to ensure |
- // that the lifetimes work out. |
- // Note that normally using the value as the key wouldn't make any |
- // sense, but here it's fine since nothing needs to look up the value. |
- objc_setAssociatedObject(view, recognizer, recognizer, |
- OBJC_ASSOCIATION_RETAIN_NONATOMIC); |
- } |
- } |
- } |
-} |
- |
- (CRWWebController*)createChildWebController { |
CRWWebController* result = [self.delegate webPageOrderedOpen]; |
DCHECK(!result || result.sessionController.openedByDOM); |
@@ -3606,53 +3504,6 @@ const NSTimeInterval kSnapshotOverlayTransition = 0.5; |
} |
#pragma mark - |
-#pragma mark UIGestureRecognizerDelegate |
- |
-- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer |
- shouldRecognizeSimultaneouslyWithGestureRecognizer: |
- (UIGestureRecognizer*)otherGestureRecognizer { |
- // Allows the custom UILongPressGestureRecognizer to fire simultaneously with |
- // other recognizers. |
- return YES; |
-} |
- |
-- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer |
- shouldReceiveTouch:(UITouch*)touch { |
- // Expect only _contextMenuRecognizer. |
- DCHECK([gestureRecognizer isEqual:_contextMenuRecognizer]); |
- |
- // This is custom long press gesture recognizer. By the time the gesture is |
- // recognized the web controller needs to know if there is a link under the |
- // touch. If there a link, the web controller will reject system's context |
- // menu and show another one. If for some reason context menu info is not |
- // fetched - system context menu will be shown. |
- [self setDOMElementForLastTouch:nil]; |
- base::WeakNSObject<CRWWebController> weakSelf(self); |
- [self fetchDOMElementAtPoint:[touch locationInView:_webView] |
- completionHandler:^(NSDictionary* element) { |
- [weakSelf setDOMElementForLastTouch:element]; |
- }]; |
- return YES; |
-} |
- |
-- (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer { |
- // Expect only _contextMenuRecognizer. |
- DCHECK([gestureRecognizer isEqual:_contextMenuRecognizer]); |
- if (!_webView) { |
- // Show the context menu iff currently displaying a web view. |
- // Do nothing for native views. |
- return NO; |
- } |
- |
- // Fetching is considered as successful even if |_DOMElementForLastTouch| is |
- // empty. However if |_DOMElementForLastTouch| is empty then custom context |
- // menu should not be shown. |
- UMA_HISTOGRAM_BOOLEAN("WebController.FetchContextMenuInfoAsyncSucceeded", |
- !!_DOMElementForLastTouch); |
- return [_DOMElementForLastTouch count]; |
-} |
- |
-#pragma mark - |
#pragma mark CRWRequestTrackerDelegate |
- (BOOL)isForStaticFileRequests { |
@@ -4316,82 +4167,6 @@ const NSTimeInterval kSnapshotOverlayTransition = 0.5; |
} |
#pragma mark - |
-#pragma mark Web Page Features |
- |
-- (void)fetchWebPageWidthWithCompletionHandler:(void (^)(CGFloat))handler { |
- if (!_webView) { |
- handler(0); |
- return; |
- } |
- |
- [self executeJavaScript:@"__gCrWeb.getPageWidth();" |
- completionHandler:^(id pageWidth, NSError*) { |
- handler([base::mac::ObjCCastStrict<NSNumber>(pageWidth) floatValue]); |
- }]; |
-} |
- |
-- (void)fetchDOMElementAtPoint:(CGPoint)point |
- completionHandler:(void (^)(NSDictionary*))handler { |
- DCHECK(handler); |
- // Convert point into web page's coordinate system (which may be scaled and/or |
- // scrolled). |
- CGPoint scrollOffset = self.scrollPosition; |
- CGFloat webViewContentWidth = self.webScrollView.contentSize.width; |
- base::WeakNSObject<CRWWebController> weakSelf(self); |
- [self fetchWebPageWidthWithCompletionHandler:^(CGFloat pageWidth) { |
- CGFloat scale = pageWidth / webViewContentWidth; |
- CGPoint localPoint = CGPointMake((point.x + scrollOffset.x) * scale, |
- (point.y + scrollOffset.y) * scale); |
- NSString* const kGetElementScript = |
- [NSString stringWithFormat:@"__gCrWeb.getElementFromPoint(%g, %g);", |
- localPoint.x, localPoint.y]; |
- [weakSelf executeJavaScript:kGetElementScript |
- completionHandler:^(id element, NSError*) { |
- handler(base::mac::ObjCCastStrict<NSDictionary>(element)); |
- }]; |
- }]; |
-} |
- |
-- (web::ContextMenuParams)contextMenuParamsForElement:(NSDictionary*)element { |
- web::ContextMenuParams params; |
- NSString* title = nil; |
- NSString* href = element[@"href"]; |
- if (href) { |
- params.link_url = GURL(base::SysNSStringToUTF8(href)); |
- if (params.link_url.SchemeIs(url::kJavaScriptScheme)) { |
- title = @"JavaScript"; |
- } else { |
- base::string16 URLText = url_formatter::FormatUrl(params.link_url); |
- title = base::SysUTF16ToNSString(URLText); |
- } |
- } |
- NSString* src = element[@"src"]; |
- if (src) { |
- params.src_url = GURL(base::SysNSStringToUTF8(src)); |
- if (!title) |
- title = [[src copy] autorelease]; |
- if ([title hasPrefix:base::SysUTF8ToNSString(url::kDataScheme)]) |
- title = nil; |
- } |
- NSString* titleAttribute = element[@"title"]; |
- if (titleAttribute) |
- title = titleAttribute; |
- if (title) { |
- params.menu_title.reset([title copy]); |
- } |
- NSString* referrerPolicy = element[@"referrerPolicy"]; |
- if (referrerPolicy) { |
- params.referrer_policy = |
- web::ReferrerPolicyFromString(base::SysNSStringToUTF8(referrerPolicy)); |
- } |
- NSString* innerText = element[@"innerText"]; |
- if ([innerText length] > 0) { |
- params.link_text.reset([innerText copy]); |
- } |
- return params; |
-} |
- |
-#pragma mark - |
#pragma mark Fullscreen |
- (CGRect)visibleFrame { |
@@ -4674,44 +4449,10 @@ const NSTimeInterval kSnapshotOverlayTransition = 0.5; |
requireGestureRecognizerToFail:swipeRecognizer]; |
} |
- // On iOS 4.x, there are two gesture recognizers on the UIWebView |
- // subclasses, |
- // that have a minimum tap threshold of 0.12s and 0.75s. |
- // |
- // My theory is that the shorter threshold recognizer performs the link |
- // highlight (grey highlight around links when it is tapped and held) while |
- // the longer threshold one pops up the context menu. |
- // |
- // To override the context menu, this recognizer needs to react faster than |
- // the 0.75s one. The below gesture recognizer is initialized with a |
- // detection duration a little lower than that (see |
- // kLongPressDurationSeconds). It also points the delegate to this class |
- // that |
- // allows simultaneously operate along with the other recognizers. |
- _contextMenuRecognizer.reset([[UILongPressGestureRecognizer alloc] |
- initWithTarget:self |
- action:@selector(showContextMenu:)]); |
- [_contextMenuRecognizer setMinimumPressDuration:kLongPressDurationSeconds]; |
- [_contextMenuRecognizer setAllowableMovement:kLongPressMoveDeltaPixels]; |
- [_contextMenuRecognizer setDelegate:self]; |
- [_webView addGestureRecognizer:_contextMenuRecognizer]; |
- // Certain system gesture handlers are known to conflict with our context |
- // menu handler, causing extra events to fire when the context menu is |
- // active. |
- |
- // A number of solutions have been investigated. The lowest-risk solution |
- // appears to be to recurse through the web controller's recognizers, |
- // looking |
- // for fingerprints of the recognizers known to cause problems, which are |
- // then |
- // de-prioritized (below our own long click handler). |
- // Hunting for description fragments of system recognizers is undeniably |
- // brittle for future versions of iOS. If it does break the context menu |
- // events may leak (regressing b/5310177), but the app will otherwise work. |
- [CRWWebController requireGestureRecognizerToFail:_contextMenuRecognizer |
- inView:_webView |
- containingDescription: |
- @"action=_highlightLongPressRecognized:"]; |
+ _contextMenuController.reset([[CRWContextMenuController alloc] |
+ initWithWebView:_webView |
+ injectionEvaluator:self |
+ delegate:self]); |
// Add all additional gesture recognizers to the web view. |
for (UIGestureRecognizer* recognizer in _gestureRecognizers.get()) { |
@@ -4731,6 +4472,8 @@ const NSTimeInterval kSnapshotOverlayTransition = 0.5; |
} |
- (WKWebView*)webViewWithConfiguration:(WKWebViewConfiguration*)config { |
+ // Do not attach the context menu controller immediately as the JavaScript |
+ // delegate must be specified. |
return web::BuildWKWebView(CGRectZero, config, |
self.webStateImpl->GetBrowserState(), |
[self useDesktopUserAgent]); |
@@ -5365,6 +5108,29 @@ const NSTimeInterval kSnapshotOverlayTransition = 0.5; |
} |
#pragma mark - |
+#pragma mark CRWWebContextMenuControllerDelegate methods |
+ |
+- (BOOL)webView:(WKWebView*)webView |
+ handleContextMenu:(const web::ContextMenuParams&)params { |
+ DCHECK(webView == _webView); |
+ if (_isBeingDestroyed) { |
+ return NO; |
+ } |
+ return self.webStateImpl->HandleContextMenu(params); |
+} |
+ |
+#pragma mark - |
+#pragma mark CRWNativeContentDelegate methods |
+ |
+- (BOOL)nativeContent:(id)content |
+ handleContextMenu:(const web::ContextMenuParams&)params { |
+ if (_isBeingDestroyed) { |
+ return NO; |
+ } |
+ return self.webStateImpl->HandleContextMenu(params); |
+} |
+ |
+#pragma mark - |
#pragma mark KVO Observation |
- (void)observeValueForKeyPath:(NSString*)keyPath |