Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(708)

Unified Diff: ios/web/web_state/ui/crw_context_menu_controller.mm

Issue 2627093003: Reuse context menu in StaticHTMLViewController (Closed)
Patch Set: fix DEPS Created 3 years, 11 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View side-by-side diff with in-line comments
Download patch
Index: ios/web/web_state/ui/crw_context_menu_controller.mm
diff --git a/ios/web/web_state/ui/crw_context_menu_controller.mm b/ios/web/web_state/ui/crw_context_menu_controller.mm
new file mode 100644
index 0000000000000000000000000000000000000000..f6d7f18e64a74d39871e4ec46ee331f73bda0a49
--- /dev/null
+++ b/ios/web/web_state/ui/crw_context_menu_controller.mm
@@ -0,0 +1,336 @@
+// Copyright 2017 The Chromium Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#import <objc/runtime.h>
+#include <stddef.h>
jif 2017/01/12 17:13:12 This group of #imports/#includes should be after t
Olivier 2017/01/13 10:20:26 Done.
+
+#import "ios/web/public/web_state/ui/crw_context_menu_controller.h"
+
+#import "base/ios/weak_nsobject.h"
+#include "base/mac/foundation_util.h"
+#include "base/metrics/histogram.h"
+#include "base/strings/sys_string_conversions.h"
+#include "components/url_formatter/url_formatter.h"
+#include "ios/web/public/referrer_util.h"
+
+namespace {
+// The long press detection duration must be shorter than the UIWebView's
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 s/UIWebView/WKWebView
Olivier 2017/01/13 10:20:25 Done.
+// 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;
+
+// If there is a movement bigger than |kLongPressMoveDeltaPixels|, the context
+// menu will not be triggered.
+const CGFloat kLongPressMoveDeltaPixels = 10.0;
+
+// 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;
+ }
+}
+}
jif 2017/01/12 17:13:12 } // namespace
Olivier 2017/01/13 10:20:25 Done.
+
+@interface CRWContextMenuController ()<UIGestureRecognizerDelegate>
+
+// The scroll view of |webView|.
+@property(nonatomic, readonly) UIScrollView* webScrollView;
+// The |webView|.
+@property(nonatomic, readonly) id<CRWContextMenuControllerWebView> webView;
+// The scroll view of |webView|.
+@property(nonatomic, readonly) id<CRWContextMenuControllerDelegate> delegate;
+// Returns the x, y offset the content has been scrolled.
+@property(nonatomic, readonly) CGPoint scrollPosition;
+
+// Called when the window has determined there was a long-press and context menu
+// must be shown.
+- (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer;
+// Extracts context menu information from the given DOM element.
+- (web::ContextMenuParams)contextMenuParamsForElement:(NSDictionary*)element;
+// Cancels all touch events in the web view (long presses, tapping, scrolling).
+- (void)cancelAllTouches;
+// 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;
+// Sets the value of |_DOMElementForLastTouch|.
+- (void)setDOMElementForLastTouch:(NSDictionary*)element;
+@end
+
+@implementation CRWContextMenuController {
+ id<CRWContextMenuControllerDelegate> _delegate;
+ id<CRWContextMenuControllerWebView> _webView;
+
+ // 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;
+}
+
+@synthesize delegate = _delegate;
+@synthesize webView = _webView;
+
+- (instancetype)initWithWebView:(id<CRWContextMenuControllerWebView>)webView
+ delegate:(id<CRWContextMenuControllerDelegate>)delegate {
+ self = [super init];
+ if (self) {
+ _webView = webView;
+ _delegate = delegate;
+
+ // On iOS 4.x, there are two gesture recognizers on the UIWebView
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 Could you please update this comment.
Olivier 2017/01/13 10:20:25 Done.
+ // 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 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.
+ [CRWContextMenuController
+ requireGestureRecognizerToFail:_contextMenuRecognizer
+ inView:[_webView webView]
+ containingDescription:
+ @"action=_highlightLongPressRecognized:"];
+ }
+ return self;
+}
+
+- (UIScrollView*)webScrollView {
+ return [_webView webScrollView];
+}
+
+- (CGPoint)scrollPosition {
+ CGPoint position = CGPointMake(0.0, 0.0);
+ if (!self.webScrollView)
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 Can this be nil?
Olivier 2017/01/13 10:20:26 I don't think so but documentation on this propert
+ return position;
+ return self.webScrollView.contentOffset;
+}
+
+- (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer {
+ // We don't want ongoing notification that the long press is held.
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 nit: Could you please remove "we"
Olivier 2017/01/13 10:20:25 Done.
+ if ([gestureRecognizer state] != UIGestureRecognizerStateBegan)
+ return;
+
+ if (![_DOMElementForLastTouch count])
+ return;
+
+ web::ContextMenuParams params =
+ [self contextMenuParamsForElement:_DOMElementForLastTouch.get()];
+ params.view.reset([[_webView webView] retain]);
+ params.location = [gestureRecognizer locationInView:[_webView webView]];
+ if ([_delegate handleContextMenu:params]) {
+ // Cancelling all touches has the intended side effect of suppressing the
+ // system's context menu.
+ [self cancelAllTouches];
+ }
+}
+
+// Set the specified recognizer to take priority over any recognizers in the
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 If this method is needed (I personally think it's
Olivier 2017/01/13 10:20:25 I checked. There seems to be no gesture recognizer
Olivier 2017/01/13 10:20:26 Done.
+// 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];
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 Do we even get here on iOS9/10?
Olivier 2017/01/13 10:20:25 Tested and apparently no (iphone iOS9/10).
+ // 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);
+ }
+ }
+ }
+}
+
+- (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;
+}
+
+- (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()";
+ [_webView executeJavaScript:suppressNextClick completionHandler:nil];
+}
+
+- (void)setDOMElementForLastTouch:(NSDictionary*)element {
+ _DOMElementForLastTouch.reset([element copy]);
+}
+
+#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<CRWContextMenuController> weakSelf(self);
+ [self fetchDOMElementAtPoint:[touch locationInView:[_webView 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.
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 Is this early return necessary? Comments seems to
Olivier 2017/01/13 10:20:26 No, removed.
+ 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 Web Page Features
+
+- (void)fetchWebPageWidthWithCompletionHandler:(void (^)(CGFloat))handler {
+ if (!_webView) {
Eugene But (OOO till 7-30) 2017/01/13 07:16:27 Can this be nil?
Olivier 2017/01/13 10:20:25 Done.
+ handler(0);
+ return;
+ }
+
+ [_webView
+ 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<CRWContextMenuController> 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 webView]
+ executeJavaScript:kGetElementScript
+ completionHandler:^(id element, NSError*) {
+ handler(base::mac::ObjCCastStrict<NSDictionary>(element));
+ }];
+ }];
+}
+
+@end

Powered by Google App Engine
This is Rietveld 408576698