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 |