Chromium Code Reviews| OLD | NEW |
|---|---|
| (Empty) | |
| 1 // Copyright 2017 The Chromium Authors. All rights reserved. | |
| 2 // Use of this source code is governed by a BSD-style license that can be | |
| 3 // found in the LICENSE file. | |
| 4 | |
| 5 #import <objc/runtime.h> | |
| 6 #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.
| |
| 7 | |
| 8 #import "ios/web/public/web_state/ui/crw_context_menu_controller.h" | |
| 9 | |
| 10 #import "base/ios/weak_nsobject.h" | |
| 11 #include "base/mac/foundation_util.h" | |
| 12 #include "base/metrics/histogram.h" | |
| 13 #include "base/strings/sys_string_conversions.h" | |
| 14 #include "components/url_formatter/url_formatter.h" | |
| 15 #include "ios/web/public/referrer_util.h" | |
| 16 | |
| 17 namespace { | |
| 18 // 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.
| |
| 19 // long click gesture recognizer's minimum duration. That is 0.55s. | |
| 20 // If our detection duration is shorter, our gesture recognizer will fire | |
| 21 // first, and if it fails the long click gesture (processed simultaneously) | |
| 22 // still is able to complete. | |
| 23 const NSTimeInterval kLongPressDurationSeconds = 0.55 - 0.1; | |
| 24 | |
| 25 // If there is a movement bigger than |kLongPressMoveDeltaPixels|, the context | |
| 26 // menu will not be triggered. | |
| 27 const CGFloat kLongPressMoveDeltaPixels = 10.0; | |
| 28 | |
| 29 // Cancels touch events for the given gesture recognizer. | |
| 30 void CancelTouches(UIGestureRecognizer* gesture_recognizer) { | |
| 31 if (gesture_recognizer.enabled) { | |
| 32 gesture_recognizer.enabled = NO; | |
| 33 gesture_recognizer.enabled = YES; | |
| 34 } | |
| 35 } | |
| 36 } | |
|
jif
2017/01/12 17:13:12
} // namespace
Olivier
2017/01/13 10:20:25
Done.
| |
| 37 | |
| 38 @interface CRWContextMenuController ()<UIGestureRecognizerDelegate> | |
| 39 | |
| 40 // The scroll view of |webView|. | |
| 41 @property(nonatomic, readonly) UIScrollView* webScrollView; | |
| 42 // The |webView|. | |
| 43 @property(nonatomic, readonly) id<CRWContextMenuControllerWebView> webView; | |
| 44 // The scroll view of |webView|. | |
| 45 @property(nonatomic, readonly) id<CRWContextMenuControllerDelegate> delegate; | |
| 46 // Returns the x, y offset the content has been scrolled. | |
| 47 @property(nonatomic, readonly) CGPoint scrollPosition; | |
| 48 | |
| 49 // Called when the window has determined there was a long-press and context menu | |
| 50 // must be shown. | |
| 51 - (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer; | |
| 52 // Extracts context menu information from the given DOM element. | |
| 53 - (web::ContextMenuParams)contextMenuParamsForElement:(NSDictionary*)element; | |
| 54 // Cancels all touch events in the web view (long presses, tapping, scrolling). | |
| 55 - (void)cancelAllTouches; | |
| 56 // Asynchronously fetches full width of the rendered web page. | |
| 57 - (void)fetchWebPageWidthWithCompletionHandler:(void (^)(CGFloat))handler; | |
| 58 // Asynchronously fetches information about DOM element for the given point (in | |
| 59 // UIView coordinates). |handler| can not be nil. See |_DOMElementForLastTouch| | |
| 60 // for element format description. | |
| 61 - (void)fetchDOMElementAtPoint:(CGPoint)point | |
| 62 completionHandler:(void (^)(NSDictionary*))handler; | |
| 63 // Sets the value of |_DOMElementForLastTouch|. | |
| 64 - (void)setDOMElementForLastTouch:(NSDictionary*)element; | |
| 65 @end | |
| 66 | |
| 67 @implementation CRWContextMenuController { | |
| 68 id<CRWContextMenuControllerDelegate> _delegate; | |
| 69 id<CRWContextMenuControllerWebView> _webView; | |
| 70 | |
| 71 // Long press recognizer that allows showing context menus. | |
| 72 base::scoped_nsobject<UILongPressGestureRecognizer> _contextMenuRecognizer; | |
| 73 // DOM element information for the point where the user made the last touch. | |
| 74 // Can be nil if has not been calculated yet. Precalculation is necessary | |
| 75 // because retreiving DOM element relies on async API so element info can not | |
| 76 // be built on demand. May contain the following keys: @"href", @"src", | |
| 77 // @"title", @"referrerPolicy". All values are strings. Used for showing | |
| 78 // context menus. | |
| 79 base::scoped_nsobject<NSDictionary> _DOMElementForLastTouch; | |
| 80 } | |
| 81 | |
| 82 @synthesize delegate = _delegate; | |
| 83 @synthesize webView = _webView; | |
| 84 | |
| 85 - (instancetype)initWithWebView:(id<CRWContextMenuControllerWebView>)webView | |
| 86 delegate:(id<CRWContextMenuControllerDelegate>)delegate { | |
| 87 self = [super init]; | |
| 88 if (self) { | |
| 89 _webView = webView; | |
| 90 _delegate = delegate; | |
| 91 | |
| 92 // 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.
| |
| 93 // subclasses, | |
| 94 // that have a minimum tap threshold of 0.12s and 0.75s. | |
| 95 // | |
| 96 // My theory is that the shorter threshold recognizer performs the link | |
| 97 // highlight (grey highlight around links when it is tapped and held) while | |
| 98 // the longer threshold one pops up the context menu. | |
| 99 // | |
| 100 // To override the context menu, this recognizer needs to react faster than | |
| 101 // the 0.75s one. The below gesture recognizer is initialized with a | |
| 102 // detection duration a little lower than that (see | |
| 103 // kLongPressDurationSeconds). It also points the delegate to this class | |
| 104 // that | |
| 105 // allows simultaneously operate along with the other recognizers. | |
| 106 _contextMenuRecognizer.reset([[UILongPressGestureRecognizer alloc] | |
| 107 initWithTarget:self | |
| 108 action:@selector(showContextMenu:)]); | |
| 109 [_contextMenuRecognizer setMinimumPressDuration:kLongPressDurationSeconds]; | |
| 110 [_contextMenuRecognizer setAllowableMovement:kLongPressMoveDeltaPixels]; | |
| 111 [_contextMenuRecognizer setDelegate:self]; | |
| 112 [[_webView webView] addGestureRecognizer:_contextMenuRecognizer]; | |
| 113 // Certain system gesture handlers are known to conflict with our context | |
| 114 // menu handler, causing extra events to fire when the context menu is | |
| 115 // active. | |
| 116 | |
| 117 // A number of solutions have been investigated. The lowest-risk solution | |
| 118 // appears to be to recurse through the web controller's recognizers, | |
| 119 // looking | |
| 120 // for fingerprints of the recognizers known to cause problems, which are | |
| 121 // then | |
| 122 // de-prioritized (below our own long click handler). | |
| 123 // Hunting for description fragments of system recognizers is undeniably | |
| 124 // brittle for future versions of iOS. If it does break the context menu | |
| 125 // events may leak (regressing b/5310177), but the app will otherwise work. | |
| 126 [CRWContextMenuController | |
| 127 requireGestureRecognizerToFail:_contextMenuRecognizer | |
| 128 inView:[_webView webView] | |
| 129 containingDescription: | |
| 130 @"action=_highlightLongPressRecognized:"]; | |
| 131 } | |
| 132 return self; | |
| 133 } | |
| 134 | |
| 135 - (UIScrollView*)webScrollView { | |
| 136 return [_webView webScrollView]; | |
| 137 } | |
| 138 | |
| 139 - (CGPoint)scrollPosition { | |
| 140 CGPoint position = CGPointMake(0.0, 0.0); | |
| 141 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
| |
| 142 return position; | |
| 143 return self.webScrollView.contentOffset; | |
| 144 } | |
| 145 | |
| 146 - (void)showContextMenu:(UIGestureRecognizer*)gestureRecognizer { | |
| 147 // 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.
| |
| 148 if ([gestureRecognizer state] != UIGestureRecognizerStateBegan) | |
| 149 return; | |
| 150 | |
| 151 if (![_DOMElementForLastTouch count]) | |
| 152 return; | |
| 153 | |
| 154 web::ContextMenuParams params = | |
| 155 [self contextMenuParamsForElement:_DOMElementForLastTouch.get()]; | |
| 156 params.view.reset([[_webView webView] retain]); | |
| 157 params.location = [gestureRecognizer locationInView:[_webView webView]]; | |
| 158 if ([_delegate handleContextMenu:params]) { | |
| 159 // Cancelling all touches has the intended side effect of suppressing the | |
| 160 // system's context menu. | |
| 161 [self cancelAllTouches]; | |
| 162 } | |
| 163 } | |
| 164 | |
| 165 // 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.
| |
| 166 // view that have a description containing the specified text fragment. | |
| 167 + (void)requireGestureRecognizerToFail:(UIGestureRecognizer*)recognizer | |
| 168 inView:(UIView*)view | |
| 169 containingDescription:(NSString*)fragment { | |
| 170 for (UIGestureRecognizer* iRecognizer in [view gestureRecognizers]) { | |
| 171 if (iRecognizer != recognizer) { | |
| 172 NSString* description = [iRecognizer description]; | |
| 173 if ([description rangeOfString:fragment].length) { | |
| 174 [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).
| |
| 175 // requireGestureRecognizerToFail: doesn't retain the recognizer, so it | |
| 176 // is possible for |iRecognizer| to outlive |recognizer| and end up with | |
| 177 // a dangling pointer. Add a retaining associative reference to ensure | |
| 178 // that the lifetimes work out. | |
| 179 // Note that normally using the value as the key wouldn't make any | |
| 180 // sense, but here it's fine since nothing needs to look up the value. | |
| 181 objc_setAssociatedObject(view, recognizer, recognizer, | |
| 182 OBJC_ASSOCIATION_RETAIN_NONATOMIC); | |
| 183 } | |
| 184 } | |
| 185 } | |
| 186 } | |
| 187 | |
| 188 - (web::ContextMenuParams)contextMenuParamsForElement:(NSDictionary*)element { | |
| 189 web::ContextMenuParams params; | |
| 190 NSString* title = nil; | |
| 191 NSString* href = element[@"href"]; | |
| 192 if (href) { | |
| 193 params.link_url = GURL(base::SysNSStringToUTF8(href)); | |
| 194 if (params.link_url.SchemeIs(url::kJavaScriptScheme)) { | |
| 195 title = @"JavaScript"; | |
| 196 } else { | |
| 197 base::string16 URLText = url_formatter::FormatUrl(params.link_url); | |
| 198 title = base::SysUTF16ToNSString(URLText); | |
| 199 } | |
| 200 } | |
| 201 NSString* src = element[@"src"]; | |
| 202 if (src) { | |
| 203 params.src_url = GURL(base::SysNSStringToUTF8(src)); | |
| 204 if (!title) | |
| 205 title = [[src copy] autorelease]; | |
| 206 if ([title hasPrefix:base::SysUTF8ToNSString(url::kDataScheme)]) | |
| 207 title = nil; | |
| 208 } | |
| 209 NSString* titleAttribute = element[@"title"]; | |
| 210 if (titleAttribute) | |
| 211 title = titleAttribute; | |
| 212 if (title) { | |
| 213 params.menu_title.reset([title copy]); | |
| 214 } | |
| 215 NSString* referrerPolicy = element[@"referrerPolicy"]; | |
| 216 if (referrerPolicy) { | |
| 217 params.referrer_policy = | |
| 218 web::ReferrerPolicyFromString(base::SysNSStringToUTF8(referrerPolicy)); | |
| 219 } | |
| 220 NSString* innerText = element[@"innerText"]; | |
| 221 if ([innerText length] > 0) { | |
| 222 params.link_text.reset([innerText copy]); | |
| 223 } | |
| 224 return params; | |
| 225 } | |
| 226 | |
| 227 - (void)cancelAllTouches { | |
| 228 // Disable web view scrolling. | |
| 229 CancelTouches(self.webScrollView.panGestureRecognizer); | |
| 230 | |
| 231 // All user gestures are handled by a subview of web view scroll view | |
| 232 // (WKContentView). | |
| 233 for (UIView* subview in self.webScrollView.subviews) { | |
| 234 for (UIGestureRecognizer* recognizer in subview.gestureRecognizers) { | |
| 235 CancelTouches(recognizer); | |
| 236 } | |
| 237 } | |
| 238 | |
| 239 // Just disabling/enabling the gesture recognizers is not enough to suppress | |
| 240 // the click handlers on the JS side. This JS performs the function of | |
| 241 // suppressing these handlers on the JS side. | |
| 242 NSString* suppressNextClick = @"__gCrWeb.suppressNextClick()"; | |
| 243 [_webView executeJavaScript:suppressNextClick completionHandler:nil]; | |
| 244 } | |
| 245 | |
| 246 - (void)setDOMElementForLastTouch:(NSDictionary*)element { | |
| 247 _DOMElementForLastTouch.reset([element copy]); | |
| 248 } | |
| 249 | |
| 250 #pragma mark - | |
| 251 #pragma mark UIGestureRecognizerDelegate | |
| 252 | |
| 253 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer | |
| 254 shouldRecognizeSimultaneouslyWithGestureRecognizer: | |
| 255 (UIGestureRecognizer*)otherGestureRecognizer { | |
| 256 // Allows the custom UILongPressGestureRecognizer to fire simultaneously with | |
| 257 // other recognizers. | |
| 258 return YES; | |
| 259 } | |
| 260 | |
| 261 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer | |
| 262 shouldReceiveTouch:(UITouch*)touch { | |
| 263 // Expect only _contextMenuRecognizer. | |
| 264 DCHECK([gestureRecognizer isEqual:_contextMenuRecognizer]); | |
| 265 | |
| 266 // This is custom long press gesture recognizer. By the time the gesture is | |
| 267 // recognized the web controller needs to know if there is a link under the | |
| 268 // touch. If there a link, the web controller will reject system's context | |
| 269 // menu and show another one. If for some reason context menu info is not | |
| 270 // fetched - system context menu will be shown. | |
| 271 [self setDOMElementForLastTouch:nil]; | |
| 272 base::WeakNSObject<CRWContextMenuController> weakSelf(self); | |
| 273 [self fetchDOMElementAtPoint:[touch locationInView:[_webView webView]] | |
| 274 completionHandler:^(NSDictionary* element) { | |
| 275 [weakSelf setDOMElementForLastTouch:element]; | |
| 276 }]; | |
| 277 return YES; | |
| 278 } | |
| 279 | |
| 280 - (BOOL)gestureRecognizerShouldBegin:(UIGestureRecognizer*)gestureRecognizer { | |
| 281 // Expect only _contextMenuRecognizer. | |
| 282 DCHECK([gestureRecognizer isEqual:_contextMenuRecognizer]); | |
| 283 if (!_webView) { | |
| 284 // Show the context menu iff currently displaying a web view. | |
| 285 // 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.
| |
| 286 return NO; | |
| 287 } | |
| 288 | |
| 289 // Fetching is considered as successful even if |_DOMElementForLastTouch| is | |
| 290 // empty. However if |_DOMElementForLastTouch| is empty then custom context | |
| 291 // menu should not be shown. | |
| 292 UMA_HISTOGRAM_BOOLEAN("WebController.FetchContextMenuInfoAsyncSucceeded", | |
| 293 !!_DOMElementForLastTouch); | |
| 294 return [_DOMElementForLastTouch count]; | |
| 295 } | |
| 296 | |
| 297 #pragma mark - | |
| 298 #pragma mark Web Page Features | |
| 299 | |
| 300 - (void)fetchWebPageWidthWithCompletionHandler:(void (^)(CGFloat))handler { | |
| 301 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.
| |
| 302 handler(0); | |
| 303 return; | |
| 304 } | |
| 305 | |
| 306 [_webView | |
| 307 executeJavaScript:@"__gCrWeb.getPageWidth();" | |
| 308 completionHandler:^(id pageWidth, NSError*) { | |
| 309 handler([base::mac::ObjCCastStrict<NSNumber>(pageWidth) floatValue]); | |
| 310 }]; | |
| 311 } | |
| 312 | |
| 313 - (void)fetchDOMElementAtPoint:(CGPoint)point | |
| 314 completionHandler:(void (^)(NSDictionary*))handler { | |
| 315 DCHECK(handler); | |
| 316 // Convert point into web page's coordinate system (which may be scaled and/or | |
| 317 // scrolled). | |
| 318 CGPoint scrollOffset = self.scrollPosition; | |
| 319 CGFloat webViewContentWidth = self.webScrollView.contentSize.width; | |
| 320 base::WeakNSObject<CRWContextMenuController> weakSelf(self); | |
| 321 [self fetchWebPageWidthWithCompletionHandler:^(CGFloat pageWidth) { | |
| 322 CGFloat scale = pageWidth / webViewContentWidth; | |
| 323 CGPoint localPoint = CGPointMake((point.x + scrollOffset.x) * scale, | |
| 324 (point.y + scrollOffset.y) * scale); | |
| 325 NSString* const kGetElementScript = | |
| 326 [NSString stringWithFormat:@"__gCrWeb.getElementFromPoint(%g, %g);", | |
| 327 localPoint.x, localPoint.y]; | |
| 328 [[weakSelf webView] | |
| 329 executeJavaScript:kGetElementScript | |
| 330 completionHandler:^(id element, NSError*) { | |
| 331 handler(base::mac::ObjCCastStrict<NSDictionary>(element)); | |
| 332 }]; | |
| 333 }]; | |
| 334 } | |
| 335 | |
| 336 @end | |
| OLD | NEW |