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

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

Issue 2627093003: Reuse context menu in StaticHTMLViewController (Closed)
Patch Set: fix DEPS 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 <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
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698