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

Side by Side Diff: ios/web/web_state/ui/crw_context_menu_controller.mm

Issue 2627093003: Reuse context menu in StaticHTMLViewController (Closed)
Patch Set: cleaning 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 unified diff | Download patch
OLDNEW
(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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698