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