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

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

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

Powered by Google App Engine
This is Rietveld 408576698