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

Side by Side Diff: ios/chrome/browser/ui/contextual_search/contextual_search_controller.mm

Issue 2588713002: Upstream Chrome on iOS source code [4/11]. (Closed)
Patch Set: Created 4 years 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 2014 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/chrome/browser/ui/contextual_search/contextual_search_controller.h"
6
7 #include <memory>
8 #include <utility>
9
10 #include "base/ios/ios_util.h"
11 #import "base/ios/weak_nsobject.h"
12 #include "base/json/json_reader.h"
13 #include "base/logging.h"
14 #import "base/mac/bind_objc_block.h"
15 #include "base/mac/foundation_util.h"
16 #include "base/mac/scoped_block.h"
17 #include "base/mac/scoped_nsobject.h"
18 #include "base/strings/sys_string_conversions.h"
19 #include "base/strings/utf_string_conversions.h"
20 #include "base/time/time.h"
21 #include "base/values.h"
22 #include "components/google/core/browser/google_util.h"
23 #include "components/search_engines/template_url_service.h"
24 #include "ios/chrome/browser/application_context.h"
25 #import "ios/chrome/browser/procedural_block_types.h"
26 #import "ios/chrome/browser/tabs/tab.h"
27 #import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
28 #import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
29 #include "ios/chrome/browser/ui/commands/ios_command_ids.h"
30 #include "ios/chrome/browser/ui/contextual_search/contextual_search_context.h"
31 #include "ios/chrome/browser/ui/contextual_search/contextual_search_delegate.h"
32 #import "ios/chrome/browser/ui/contextual_search/contextual_search_header_view.h "
33 #import "ios/chrome/browser/ui/contextual_search/contextual_search_highlighter_v iew.h"
34 #import "ios/chrome/browser/ui/contextual_search/contextual_search_metrics.h"
35 #import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_protoco ls.h"
36 #import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.h"
37 #import "ios/chrome/browser/ui/contextual_search/contextual_search_promo_view.h"
38 #import "ios/chrome/browser/ui/contextual_search/contextual_search_results_view. h"
39 #include "ios/chrome/browser/ui/contextual_search/contextual_search_web_state_ob server.h"
40 #import "ios/chrome/browser/ui/contextual_search/js_contextual_search_manager.h"
41 #import "ios/chrome/browser/ui/contextual_search/touch_to_search_permissions_med iator.h"
42 #import "ios/chrome/browser/ui/contextual_search/window_gesture_observer.h"
43 #import "ios/chrome/browser/ui/show_privacy_settings_util.h"
44 #include "ios/chrome/browser/ui/ui_util.h"
45 #import "ios/chrome/browser/ui/uikit_ui_util.h"
46 #import "ios/chrome/browser/web/dom_altering_lock.h"
47 #include "ios/chrome/common/string_util.h"
48 #include "ios/chrome/grit/ios_strings.h"
49 #import "ios/third_party/material_components_ios/src/components/Snackbar/src/Mat erialSnackbar.h"
50 #include "ios/web/public/browser_state.h"
51 #include "ios/web/public/load_committed_details.h"
52 #include "ios/web/public/referrer.h"
53 #import "ios/web/public/web_state/crw_web_view_proxy.h"
54 #import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h"
55 #import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
56 #include "ios/web/public/web_state/web_state.h"
57 #include "ios/web/public/web_state/web_state_observer.h"
58 #include "ui/base/l10n/l10n_util.h"
59 #include "ui/base/l10n/l10n_util_mac.h"
60
61 // Returns |value| clamped so that min <= value <= max
62 #define CLAMP(min, value, max) MAX(min, MIN(value, max))
63
64 namespace {
65 // command prefix for injected JavaScript.
66 const std::string kCommandPrefix = "contextualSearch";
67
68 // Distance from edges of frame when scrolling to show selection.
69 const CGFloat kYScrollMargin = 30.0;
70 const CGFloat kXScrollMargin = 10.0;
71
72 // Delay to check if there is a DOM modification (in second).
73 // If delay is too short, JavaScript won't have time to handle the event and the
74 // DOM tree will be modified after the highlight.
75 // If delay is too long, the user experience will be degraded.
76 // Experiments on some websites (e.g. belgianrails.be) show that delay must be
77 // over 0.35 second.
78 // Timeline is as follow:
79 // t: tap happens,
80 // t + doubleTapDelay (t1): tap is triggered
81 // t1: JavaScript handles tap,
82 // t1 + delta1: DOM may be modified,
83 // t1 + kDOMModificationDelaySeconds: |handleTapFrom:| starts handling tap,
84 // t1 + kDOMModificationDelaySeconds + delta2: JavaScript handleTap is called.
85 //
86 // The delay between DOM mutation and contextual search tap handling is really
87 // kDOMModificationDelaySeconds + delta2 - delta1.
88 // To handle this random delta value, the timeout passed to JavaScript is
89 // doubled
90 // The body touch event timeout must include the double tap delay.
91 const CGFloat kDOMModificationDelaySeconds = 0.1;
92 const CGFloat kDOMModificationDelayForJavaScriptMilliseconds =
93 2 * 1000 * kDOMModificationDelaySeconds;
94
95 // The touch delay disables CS on many sites, so for now it is disabled.
96 // The previous value used was:
97 // kDOMModificationDelayForJavaScriptMilliseconds + 300
98 const CGFloat kBodyTouchDelayForJavaScriptMilliseconds = 0;
99
100 // After a dismiss, do not allow retrigger before a delay to prevent triggering
101 // on double tap, and to prevent retrigger on the same event.
102 const NSTimeInterval kPreventTriggerAfterDismissDelaySeconds = 0.3;
103
104 CGRect StringValueToRect(NSString* rectString) {
105 double rectTop, rectBottom, rectLeft, rectRight;
106 NSArray* items = [rectString componentsSeparatedByString:@" "];
107 if ([items count] != 4) {
108 return CGRectNull;
109 }
110 rectTop = [[items objectAtIndex:0] doubleValue];
111 rectBottom = [[items objectAtIndex:1] doubleValue];
112 rectLeft = [[items objectAtIndex:2] doubleValue];
113 rectRight = [[items objectAtIndex:3] doubleValue];
114 if (isnan(rectTop) || isinf(rectTop) || isnan(rectBottom) ||
115 isinf(rectBottom) || isnan(rectLeft) || isinf(rectLeft) ||
116 isnan(rectRight) || isinf(rectRight) || rectRight <= rectLeft ||
117 rectBottom <= rectTop) {
118 return CGRectNull;
119 }
120 CGRect rect =
121 CGRectMake(rectLeft, rectTop, rectRight - rectLeft, rectBottom - rectTop);
122 return rect;
123 }
124
125 NSArray* StringValueToRectArray(const std::string& list) {
126 NSString* nsList = base::SysUTF8ToNSString(list);
127 NSMutableArray* rectsArray = [[[NSMutableArray alloc] init] autorelease];
128 NSArray* items = [nsList componentsSeparatedByString:@","];
129 for (NSString* item : items) {
130 CGRect rect = StringValueToRect(item);
131 if (CGRectIsNull(rect)) {
132 return nil;
133 }
134 [rectsArray addObject:[NSValue valueWithCGRect:rect]];
135 }
136 return rectsArray;
137 }
138
139 } // namespace
140
141 @interface ContextualSearchController ()<DOMAltering,
142 CRWWebViewScrollViewProxyObserver,
143 UIGestureRecognizerDelegate,
144 ContextualSearchHighlighterDelegate,
145 ContextualSearchPromoViewDelegate,
146 ContextualSearchPanelMotionObserver,
147 ContextualSearchPanelTapHandler,
148 ContextualSearchPreloadChecker,
149 ContextualSearchTabPromoter,
150 ContextualSearchWebStateDelegate,
151 TouchToSearchPermissionsChangeAudience>
152
153 // Controller delegate for the controller to call back to.
154 @property(nonatomic, readwrite, assign) id<ContextualSearchControllerDelegate>
155 controllerDelegate;
156
157 // Permissions interface for this feature. Property is readwrite for testing.
158 @property(nonatomic, readwrite, retain)
159 TouchToSearchPermissionsMediator* permissions;
160
161 // Synchronous method executed by -asynchronouslyEnableContextualSearch:
162 - (void)doEnableContextualSearch:(BOOL)enabled;
163
164 // Handler for injected JavaScript callbacks.
165 - (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand;
166
167 // Handle the selection change event if the DOM lock is acquired.
168 // |selection| is the currently selected text in the webview.
169 // if |updated| is true, then the selection changed by the user moving one of
170 // the selection handles (not making a new selection).
171 // If |selectionValid| is false, the selection contains invalid chars or element
172 // and TTS should be dismissed. If selection is invalid, |selection| is empty.
173 - (void)handleSelectionChanged:(const std::string&)selection
174 selectionUpdated:(BOOL)update
175 selectionValid:(BOOL)selectionValid;
176
177 // Action for the tap gesture recognizer.
178 - (void)handleTapFrom:(UIGestureRecognizer*)gestureRecognizer;
179
180 // Handle a tap on a web view at |point|, extracting contextual search
181 // information from the tapped word and surrounding text.
182 - (void)handleTapAtPoint:(CGPoint)point;
183
184 // Initialize the contextual search JavaScript.
185 - (void)initializeWebViewForContextualSearch;
186
187 // Update the webViewProxy for the current tab to enable/disable scroll view
188 // observation.
189 - (void)updateWebViewProxy:(id<CRWWebViewProxy>)webViewProxy;
190
191 // Updates the UI to match the current state, setting the text label content
192 // if there is a current search context, and setting the panel state.
193 - (void)updateUI;
194
195 // Updates the UI for a resolved search.
196 - (void)updateForResolvedSearch:
197 (ContextualSearchDelegate::SearchResolution)resolution;
198
199 // State changes.
200 // Set the state of the panel, given |reason|. Handles metrics updates.
201 - (void)setState:(ContextualSearch::PanelState)state
202 reason:(ContextualSearch::StateChangeReason)reason;
203
204 // Dismiss pane for |reason|, invoking |completionHandler|, if any, after
205 // clearing any existing highlighted text in the webview, and finally releasing
206 // the DOM lock.
207 - (void)
208 dismissPaneWithJavascriptCompletionHandler:(ProceduralBlock)completionHandler
209 reason:(ContextualSearch::StateChangeReason)
210 reason;
211
212 // Clean-up the web state (release lock, clear highlight...) in case of a
213 // dismiss.
214 - (void)cleanUpWebStateForDismissWithCompletion:
215 (ProceduralBlock)completionHandler;
216
217 // Convenience method for dismissing the pane with no completion handler.
218 - (void)dismissPane:(ContextualSearch::StateChangeReason)reason;
219
220 // Peek (show at the bottom of the window) the pane for |reason|.
221 - (void)peekPane:(ContextualSearch::StateChangeReason)reason;
222
223 // Preview the pane (covering kPreviewingDisplayRatio of the webview) for
224 // |reason|.
225 - (void)previewPane:(ContextualSearch::StateChangeReason)reason;
226
227 // Cover the pane (covering the entire webview) for |reason|.
228 - (void)coverPane:(ContextualSearch::StateChangeReason)reason;
229
230 // Scroll the webview to show the highlighted text.
231 // Scroll the minimal distance to put |_highlightBoundingRect| at
232 // |kYScrollMargin| from top and bottom edges and |kXScrollMargin| from left and
233 // right edges.
234 // Overflow policy :
235 // - horizontal: center |_highlightBoundingRect|,
236 // - vertical: put |_highlightBoundingRect| at |kYScrollMargin| from top edge.
237 - (void)scrollToShowSelection:(CRWWebViewScrollViewProxy*)scrollView;
238
239 // Creates, enables or disables the dismiss recognizer based on state_.
240 - (void)updateDismissRecognizer;
241
242 @end
243
244 @implementation ContextualSearchController {
245 // Permissions interface for this feature.
246 base::scoped_nsobject<TouchToSearchPermissionsMediator> _permissions;
247
248 // WebState for the tab this object is attached to.
249 web::WebState* _webState;
250
251 // Access to the web view from |_webState|.
252 base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
253
254 // Observer for |_webState|.
255 std::unique_ptr<ContextualSearchWebStateObserver> _webStateObserver;
256
257 // Observer for search tab's web state.
258 std::unique_ptr<ContextualSearchWebStateObserver> _searchTabWebStateObserver;
259
260 // Object that manages find_in_page.js injection into the web view.
261 base::WeakNSObject<JsContextualSearchManager> _contextualSearchJsManager;
262
263 // Gesture reccognizer for contextual search taps.
264 base::scoped_nsobject<UITapGestureRecognizer> _tapRecognizer;
265
266 // Gesture reccognizer for double tap. It is used to prevent |_tapRecognizer|
267 // from firing if there is a double tap on the web view. It is disabled when
268 // the panel is displayed, since any tap will dismiss the panel in that case.
269 base::scoped_nsobject<UITapGestureRecognizer> _doubleTapRecognizer;
270
271 // Gesture recognizer for long-tap copy.
272 base::scoped_nsobject<UILongPressGestureRecognizer> _copyGestureRecognizer;
273
274 // Gesture recognizer to detect taps outside of the CS interface that would
275 // cause it to dismiss.
276 base::scoped_nsobject<WindowGestureObserver> _dismissRecognizer;
277
278 // Context information retrieved from a search tap.
279 std::shared_ptr<ContextualSearchContext> _searchContext;
280
281 // Resolved search information generated from the context or text selection.
282 ContextualSearchDelegate::SearchResolution _resolvedSearch;
283
284 // Delegate for fetching search information.
285 std::unique_ptr<ContextualSearchDelegate> _delegate;
286
287 // The panel view controlled by this object; it is created externally and
288 // owned by its superview. There is no guarantee about its lifetime.
289 base::WeakNSObject<ContextualSearchPanelView> _panelView;
290
291 // The view containing the highlighting of the search terms.
292 base::WeakNSObject<ContextualSearchHighlighterView> _contextualHighlightView;
293
294 // Content view displayed in the peeking section of the panel.
295 base::scoped_nsobject<ContextualSearchHeaderView> _headerView;
296
297 // Vertical constraints for layout of the search tab.
298 base::scoped_nsobject<NSArray> _searchTabVerticalConstraints;
299
300 // Container view for the opt-out promo and the search tab view.
301 base::scoped_nsobject<ContextualSearchResultsView> _searchResultsView;
302
303 // View for the opt-out promo.
304 base::scoped_nsobject<ContextualSearchPromoView> _promoView;
305
306 // The tab that should be used as the opener for the search tab.
307 Tab* _opener;
308
309 // YES if a cancel event was received since last tap, meaning the current tap
310 // must not result in a search.
311 BOOL _currentTapCancelled;
312
313 // The current selection text.
314 std::string _selectedText;
315
316 // Boolean to track if the current WebState is enabled (has
317 // gesture recognizers and DOM lock set up).
318 BOOL _webStateEnabled;
319
320 // Boolean to distinguish selection-clearing taps on the webview from
321 // those on other UI elements.
322 BOOL _webViewTappedWithSelection;
323
324 // Metrics tracking variables and flags.
325 // Time the tap handler fires. The delay of doubleTap is not counted.
326 base::Time _tapTime;
327 // Has the user entered the previewing/covering state yet for the
328 // current search?
329 BOOL _enteredPreviewing;
330 BOOL _enteredCovering;
331 // Has the search results content been visible for the current search?
332 BOOL _resultsVisible;
333 // Has the user exited the peeking/previewing/covering state yet for the
334 // current search?
335 BOOL _exitedPeeking;
336 BOOL _exitedPreviewing;
337 BOOL _exitedCovering;
338 // Was the first run flow invoked during this search?
339 BOOL _searchInvolvedFirstRun;
340 // Did the first run panel become visible during this search?
341 BOOL _firstRunPanelBecameVisible;
342 // Was the search triggered by a long-press selection? Unlike other metrics-
343 // related flags, this is not reset when a search ends; instead it is set
344 // when a new search is started.
345 BOOL _searchTriggeredBySelection;
346 // Has the current search used SERP navigation (tapped on a link on the
347 // search results page)?
348 BOOL _usedSERPNavigation;
349 // Boolean to track if the script has been injected in the current page. This
350 // is a faster check than asking the JS controller.
351 BOOL _isScriptInjected;
352
353 // Boolean to track if the UIMenuControllerWillShowMenuNotification is
354 // observed (to prevent double observation).
355 BOOL _observingActionMenu;
356 // Boolean to track if the current search is triggered by selection, and
357 // action menu should be disabled.
358 BOOL _preventActionMenu;
359 // Boolean to track if a new text selection has been made (as opposed to an
360 // existing one being changed) which will trigger the appearance of the
361 // panel.
362 BOOL _newSelectionDisplaying;
363
364 // Boolean to temporarly disable preloading of search tab.
365 BOOL _preventPreload;
366
367 // Boolean to track if the search term has been resolved.
368 BOOL _searchTermResolved;
369
370 // True when closed has been called and contextual search controller
371 // has been destroyed.
372 BOOL _closed;
373
374 // When view is resized, JavaScript and UIView sizes are not updated at the
375 // same time. Computing a scroll delta to make selection visible in these
376 // conditions will likely scroll to a random position.
377 BOOL _preventScrollToShowSelection;
378
379 // The time of the last dismiss.
380 base::scoped_nsobject<NSDate> _lastDismiss;
381 }
382
383 @synthesize enabled = _enabled;
384 @synthesize controllerDelegate = _controllerDelegate;
385 @synthesize webState = _webState;
386
387 - (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
388 delegate:(id<ContextualSearchControllerDelegate>)
389 delegate {
390 if ((self = [super init])) {
391 _permissions.reset([[TouchToSearchPermissionsMediator alloc]
392 initWithBrowserState:browserState]);
393 [_permissions setAudience:self];
394
395 self.controllerDelegate = delegate;
396
397 // Set up the web state observer. This lasts as long as this object does,
398 // but it will observe and un-observe the web tabs as it changes over time.
399 _webStateObserver.reset(new ContextualSearchWebStateObserver(self));
400
401 _copyGestureRecognizer.reset([[UILongPressGestureRecognizer alloc]
402 initWithTarget:self
403 action:@selector(handleLongPressFrom:)]);
404
405 base::WeakNSObject<ContextualSearchController> weakself(self);
406 auto callback = base::BindBlock(
407 ^(ContextualSearchDelegate::SearchResolution resolution) {
408 [weakself updateForResolvedSearch:resolution];
409 });
410
411 _delegate.reset(new ContextualSearchDelegate(browserState, callback));
412 }
413 return self;
414 }
415
416 - (TouchToSearchPermissionsMediator*)permissions {
417 return _permissions;
418 }
419
420 - (void)setPermissions:(TouchToSearchPermissionsMediator*)permissions {
421 _permissions.reset(permissions);
422 }
423
424 - (ContextualSearchPanelView*)panel {
425 return _panelView;
426 }
427
428 - (void)setPanel:(ContextualSearchPanelView*)panel {
429 DCHECK(!_panelView);
430 DCHECK(panel);
431
432 // Save the new panel, set up observation and delegation relationships.
433 _panelView.reset(panel);
434 [_panelView addMotionObserver:self];
435 [_dismissRecognizer setViewToExclude:_panelView];
436
437 // Create new subviews.
438 NSMutableArray* panelContents = [NSMutableArray arrayWithCapacity:3];
439
440 _headerView.reset([[ContextualSearchHeaderView alloc]
441 initWithHeight:[_panelView configuration].peekingHeight]);
442 [_headerView addGestureRecognizer:_copyGestureRecognizer];
443 [_headerView setTapHandler:self];
444
445 [panelContents addObject:_headerView];
446
447 if (self.permissions.preferenceState == TouchToSearch::UNDECIDED) {
448 _promoView.reset([[ContextualSearchPromoView alloc] initWithFrame:CGRectZero
449 delegate:self]);
450 [panelContents addObject:_promoView];
451 }
452
453 _searchResultsView.reset(
454 [[ContextualSearchResultsView alloc] initWithFrame:CGRectZero]);
455 [_searchResultsView setPromoter:self];
456 [_searchResultsView setPreloadChecker:self];
457 [panelContents addObject:_searchResultsView];
458
459 [_panelView addContentViews:panelContents];
460 }
461
462 - (void)enableContextualSearch:(BOOL)enabled {
463 // Asynchronously enables contextual search, so that some preferences
464 // (UIAccessibilityIsVoiceOverRunning(), for example) have time to synchronize
465 // with their own notifications.
466 base::WeakNSObject<ContextualSearchController> weakSelf(self);
467 dispatch_async(dispatch_get_main_queue(), ^{
468 [weakSelf doEnableContextualSearch:enabled];
469 });
470 }
471
472 - (void)doEnableContextualSearch:(BOOL)enabled {
473 enabled = enabled && [self.permissions canEnable];
474
475 BOOL changing = _enabled != enabled;
476 if (changing) {
477 if (!enabled) {
478 [self dismissPane:ContextualSearch::RESET];
479 }
480 _enabled = enabled;
481 [self enableCurrentWebState];
482 }
483 }
484
485 - (void)updateWebViewProxy:(id<CRWWebViewProxy>)webViewProxy {
486 if (_webViewProxy) {
487 [[_webViewProxy scrollViewProxy] removeObserver:self];
488 }
489 _webViewProxy.reset([webViewProxy retain]);
490 if (_webViewProxy) {
491 [[_webViewProxy scrollViewProxy] addObserver:self];
492 }
493 }
494
495 - (void)setTab:(Tab*)tab {
496 [self setWebState:tab.webState];
497 [_searchResultsView setOpener:tab];
498 }
499
500 - (void)setWebState:(web::WebState*)webState {
501 [self disconnectWebState];
502 if (webState) {
503 _contextualSearchJsManager.reset(static_cast<JsContextualSearchManager*>(
504 [webState->GetJSInjectionReceiver()
505 instanceOfClass:[JsContextualSearchManager class]]));
506 _webState = webState;
507 _webStateObserver->ObserveWebState(webState);
508 [self updateWebViewProxy:webState->GetWebViewProxy()];
509 [self enableCurrentWebState];
510 } else {
511 _webState = nullptr;
512 }
513 }
514
515 - (void)enableCurrentWebState {
516 if (![self webState])
517 return;
518 if (_enabled && [self webState]->ContentIsHTML()) {
519 if (!_webStateEnabled) {
520 DOMAlteringLock::CreateForWebState([self webState]);
521
522 base::WeakNSObject<ContextualSearchController> weakSelf(self);
523 auto callback =
524 base::BindBlock(^bool(const base::DictionaryValue& JSON,
525 const GURL& originURL, bool userIsInteracting) {
526 base::scoped_nsobject<ContextualSearchController> strongSelf(
527 [weakSelf retain]);
528 // |originURL| and |isInteracting| aren't used.
529 return [strongSelf handleScriptCommand:JSON];
530 });
531 [self webState]->AddScriptCommandCallback(callback, kCommandPrefix);
532
533 // |_doubleTapRecognizer| should be added to the web view before
534 // |_tapRecognizer| so |_tapRecognizer| can require it to fail.
535 _doubleTapRecognizer.reset([[UITapGestureRecognizer alloc]
536 initWithTarget:self
537 action:@selector(ignoreTap:)]);
538 [_doubleTapRecognizer setDelegate:self];
539 [_doubleTapRecognizer setNumberOfTapsRequired:2];
540 [_webViewProxy addGestureRecognizer:_doubleTapRecognizer];
541
542 _tapRecognizer.reset([[UITapGestureRecognizer alloc]
543 initWithTarget:self
544 action:@selector(handleTapFrom:)]);
545 [_tapRecognizer setDelegate:self];
546 [_webViewProxy addGestureRecognizer:_tapRecognizer];
547
548 // Make sure that |_tapRecogngizer| doesn't fire if the web view's other
549 // non-single-finger non-single-tap recognizers fire.
550 for (UIGestureRecognizer* recognizer in
551 [[_tapRecognizer view] gestureRecognizers]) {
552 if ([recognizer isKindOfClass:[UITapGestureRecognizer class]] &&
553 ([static_cast<UITapGestureRecognizer*>(recognizer)
554 numberOfTapsRequired] > 1 ||
555 [static_cast<UITapGestureRecognizer*>(recognizer)
556 numberOfTouchesRequired] > 1)) {
557 [_tapRecognizer requireGestureRecognizerToFail:recognizer];
558 }
559 }
560 _webStateEnabled = YES;
561 }
562
563 [self initializeWebViewForContextualSearch];
564 } else {
565 [self disableCurrentWebState];
566 }
567 }
568
569 - (void)disableCurrentWebState {
570 if (_webStateEnabled) {
571 if ([self webState]->ContentIsHTML()) {
572 [self highlightRects:nil];
573 [_contextualHighlightView removeFromSuperview];
574 [_contextualSearchJsManager clearHighlight];
575 [_contextualSearchJsManager disableListeners];
576 }
577 _webState->RemoveScriptCommandCallback(kCommandPrefix);
578 DOMAlteringLock::FromWebState(_webState)->Release(self);
579 [_webViewProxy removeGestureRecognizer:_tapRecognizer];
580 [_webViewProxy removeGestureRecognizer:_doubleTapRecognizer];
581 _webStateEnabled = NO;
582 }
583 }
584
585 - (void)disconnectWebState {
586 if (_webState) {
587 _contextualSearchJsManager.reset();
588 _webStateObserver->ObserveWebState(nullptr);
589 [self updateWebViewProxy:nil];
590 [self disableCurrentWebState];
591 }
592 }
593
594 - (void)updateDismissRecognizer {
595 if (!_panelView)
596 return;
597 if (!_dismissRecognizer) {
598 _dismissRecognizer.reset([[WindowGestureObserver alloc]
599 initWithTarget:self
600 action:@selector(handleWindowGesture:)]);
601 [_dismissRecognizer setViewToExclude:_panelView];
602 [[_panelView window] addGestureRecognizer:_dismissRecognizer];
603 }
604
605 [_dismissRecognizer
606 setEnabled:[_panelView state] >= ContextualSearch::PEEKING];
607 }
608
609 - (void)showLearnMore {
610 [self dismissPane:ContextualSearch::UNKNOWN];
611 GURL learnMoreUrl = google_util::AppendGoogleLocaleParam(
612 GURL(l10n_util::GetStringUTF8(IDS_IOS_CONTEXTUAL_SEARCH_LEARN_MORE_URL)),
613 GetApplicationContext()->GetApplicationLocale());
614 [_controllerDelegate createTabFromContextualSearchController:learnMoreUrl];
615 }
616
617 - (void)dealloc {
618 [self close];
619 [super dealloc];
620 }
621
622 - (void)handleWindowGesture:(UIGestureRecognizer*)recognizer {
623 DCHECK(recognizer == _dismissRecognizer.get());
624 [self dismissPane:ContextualSearch::BASE_PAGE_TAP];
625 }
626
627 - (BOOL)canExtractTapContext {
628 web::URLVerificationTrustLevel trustLevel = web::kNone;
629 GURL pageURL = [self webState]->GetCurrentURL(&trustLevel);
630 return [self.permissions canExtractTapContextForURL:pageURL];
631 }
632
633 - (void)initializeWebViewForContextualSearch {
634 DCHECK(_webStateEnabled);
635 [_contextualSearchJsManager inject];
636 _isScriptInjected = YES;
637 [_contextualSearchJsManager
638 enableEventListenersWithMutationDelay:
639 kDOMModificationDelayForJavaScriptMilliseconds
640 bodyTouchDelay:
641 kBodyTouchDelayForJavaScriptMilliseconds];
642 }
643
644 - (void)handleSelectionChanged:(const std::string&)selection
645 selectionUpdated:(BOOL)updated
646 selectionValid:(BOOL)selectionValid {
647 if (!selectionValid) {
648 [self dismissPane:ContextualSearch::INVALID_SELECTION];
649 return;
650 }
651 std::string selectedText = CleanStringForDisplay(selection, true);
652
653 if (selectedText == _selectedText)
654 return;
655 _newSelectionDisplaying = !updated && !selectedText.empty();
656 _selectedText = selectedText;
657 _searchContext.reset();
658 [self highlightRects:nil];
659 [_contextualSearchJsManager clearHighlight];
660 _delegate->CancelSearchTermRequest();
661
662 if (selectedText.length() == 0) {
663 if (_webViewTappedWithSelection) {
664 [self dismissPane:ContextualSearch::BASE_PAGE_TAP];
665 }
666 } else {
667 // TODO(crbug.com/546220): Detect and use actual page encoding.
668 std::string encoding = "UTF-8";
669
670 _searchContext.reset(
671 new ContextualSearchContext(selectedText, true, GURL(), encoding));
672 _searchTriggeredBySelection = YES;
673
674 _preventPreload = YES;
675 _delegate->PostSearchTermRequest(_searchContext);
676
677 ContextualSearch::RecordSelectionIsValid(true);
678 _preventActionMenu = YES;
679 if (!_observingActionMenu) {
680 _observingActionMenu = YES;
681 [[NSNotificationCenter defaultCenter]
682 addObserver:self
683 selector:@selector(willShowMenuNotification)
684 name:UIMenuControllerWillShowMenuNotification
685 object:nil];
686 }
687 [self peekPane:ContextualSearch::TEXT_SELECT_LONG_PRESS];
688 [_headerView
689 setSearchTerm:base::SysUTF8ToNSString(selectedText)
690 animated:[_panelView state] != ContextualSearch::DISMISSED];
691 }
692 _webViewTappedWithSelection = NO;
693 }
694
695 - (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand {
696 std::string command;
697 if (!JSONCommand.GetString("command", &command))
698 return NO;
699 if (command == "contextualSearch.selectionChanged") {
700 std::string selectedText;
701 if (!JSONCommand.GetString("text", &selectedText))
702 return NO;
703 bool selectionUpdated;
704 if (!JSONCommand.GetBoolean("updated", &selectionUpdated))
705 selectionUpdated = false;
706 bool selectionValid;
707 if (!JSONCommand.GetBoolean("valid", &selectionValid))
708 selectionValid = true;
709 base::WeakNSObject<ContextualSearchController> weakSelf(self);
710 ProceduralBlockWithBool lockAction = ^(BOOL lockAcquired) {
711 if (lockAcquired) {
712 [weakSelf handleSelectionChanged:selectedText
713 selectionUpdated:selectionUpdated
714 selectionValid:selectionValid];
715 }
716 };
717 DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction);
718 return YES;
719 }
720 if (command == "contextualSearch.mutationEvent") {
721 if ([_panelView state] <= ContextualSearch::PEEKING &&
722 !_searchTermResolved) {
723 [self dismissPane:ContextualSearch::UNKNOWN];
724 }
725 return YES;
726 }
727 return NO;
728 }
729
730 - (void)ignoreTap:(UIGestureRecognizer*)recognizer {
731 // This method is intentionally empty. It is intended to ignore the tap.
732 }
733
734 - (void)handleTapFrom:(UIGestureRecognizer*)recognizer {
735 DCHECK(recognizer == _tapRecognizer.get());
736 // Taps will be triggered by long-presses to make a selection in the webview,
737 // as well as 'regular' taps. Long-presses that create a selection will set
738 // |_newSelectionDisplaying| as well as populating _selectedText (this happens
739 // in -handleScriptCommand:).
740
741 // If we just dismissed, do not consider this tap.
742 NSTimeInterval dismissTimeout = [_lastDismiss timeIntervalSinceNow] +
743 kPreventTriggerAfterDismissDelaySeconds;
744
745 // If the panel is already displayed, just dismiss it and return, unless the
746 // tap was from displaying a new selection.
747 if (([_panelView state] != ContextualSearch::DISMISSED &&
748 !_newSelectionDisplaying) ||
749 dismissTimeout > 0) {
750 [self dismissPane:ContextualSearch::BASE_PAGE_TAP];
751 return;
752 }
753 // Otherwise handle the tap.
754 [_tapRecognizer setEnabled:NO];
755 _currentTapCancelled = NO;
756 _newSelectionDisplaying = NO;
757 ProceduralBlockWithBool lockAction = ^(BOOL lockAcquired) {
758 if (!lockAcquired || !_isScriptInjected || _currentTapCancelled ||
759 [recognizer state] != UIGestureRecognizerStateEnded ||
760 !_selectedText.empty()) {
761 [_tapRecognizer setEnabled:YES];
762 if (!_selectedText.empty())
763 _webViewTappedWithSelection = YES;
764 return;
765 }
766
767 CGPoint tapPoint = [recognizer locationInView:recognizer.view];
768 // tapPoint is the coordinate of the tap in the webView. If the view is
769 // currently offset because a header is displayed, offset the tapPoint.
770 tapPoint.y -= [_controllerDelegate currentHeaderHeight];
771
772 // Handle tap asynchronously to monitor DOM modifications. See comment
773 // of |kDOMModificationDelaySeconds| for details.
774 dispatch_time_t dispatch = dispatch_time(
775 DISPATCH_TIME_NOW,
776 static_cast<int64_t>(kDOMModificationDelaySeconds * NSEC_PER_SEC));
777 base::WeakNSObject<ContextualSearchController> weakSelf(self);
778 dispatch_after(dispatch, dispatch_get_main_queue(), ^{
779 [weakSelf handleTapAtPoint:tapPoint];
780 });
781 };
782 DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction);
783 }
784
785 - (void)handleLongPressFrom:(UIGestureRecognizer*)recognizer {
786 DCHECK(recognizer == _copyGestureRecognizer.get());
787 if (recognizer.state != UIGestureRecognizerStateEnded)
788 return;
789
790 // Put the resolved search term (or the current selected text) into the
791 // pasteboard.
792 std::string text;
793 if (!_resolvedSearch.display_text.empty()) {
794 text = _resolvedSearch.display_text;
795 }
796
797 if (!text.empty()) {
798 UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
799 pasteboard.string = base::SysUTF8ToNSString(_resolvedSearch.display_text);
800 // Let the user know.
801 NSString* messageText = l10n_util::GetNSString(IDS_IOS_SEARCH_COPIED);
802 MDCSnackbarMessage* message =
803 [MDCSnackbarMessage messageWithText:messageText];
804 message.duration = 1.0;
805 message.category = @"search term copied";
806 [MDCSnackbarManager showMessage:message];
807 }
808 }
809
810 - (void)handleTapAtPoint:(CGPoint)point {
811 _tapTime = base::Time::Now();
812 if (_currentTapCancelled) {
813 [_tapRecognizer setEnabled:YES];
814 return;
815 }
816
817 _searchTriggeredBySelection = NO;
818
819 // TODO(crbug.com/546220): Detect and use actual page encoding.
820 std::string encoding = "UTF-8";
821
822 CGPoint relativeTapPoint = point;
823 CGSize contentSize = [_webViewProxy scrollViewProxy].contentSize;
824 relativeTapPoint.x += [_webViewProxy scrollViewProxy].contentOffset.x;
825 relativeTapPoint.y += [_webViewProxy scrollViewProxy].contentOffset.y;
826
827 relativeTapPoint.x /= contentSize.width;
828 relativeTapPoint.y /= contentSize.height;
829
830 base::WeakNSProtocol<id<CRWWebViewProxy>> weakWebViewProxy(
831 _webViewProxy.get());
832 void (^handler)(NSString*) = ^(NSString* result) {
833 [_tapRecognizer setEnabled:YES];
834 // If there has been an error in the javascript, return can be nil.
835 if (!result || _currentTapCancelled)
836 return;
837
838 // Parse JSON.
839 const std::string json = base::SysNSStringToUTF8(result);
840 std::unique_ptr<base::Value> parsedResult(
841 base::JSONReader::Read(json, false));
842 if (!parsedResult.get() ||
843 !parsedResult->IsType(base::Value::Type::DICTIONARY)) {
844 return;
845 }
846
847 base::DictionaryValue* resultDict =
848 static_cast<base::DictionaryValue*>(parsedResult.get());
849 const base::DictionaryValue* context = nullptr;
850 BOOL contextError = NO;
851 if (!resultDict->GetDictionary("context", &context)) {
852 // No context returned -- the tap wasn't on a word.
853 DVLOG(1) << "Contextual search results did not include a context.";
854 contextError = YES;
855 } else {
856 std::string error;
857 context->GetString("error", &error);
858 if (!error.empty()) {
859 // Something went wrong!
860 DVLOG(0) << "Contextual search error: " << error;
861 contextError = YES;
862 }
863 }
864
865 if (contextError) {
866 _searchContext.reset();
867 [self updateUI];
868 // The JavaScript will have taken care of clearing the highlighting.
869 return;
870 }
871
872 // Marshall the retrieved context.
873 std::string url, selectedText;
874 BOOL marshallingOK = YES;
875 GURL sentUrl;
876 if ([self.permissions canSendPageURLs]) {
877 marshallingOK = marshallingOK && context->GetString("url", &url);
878 sentUrl = GURL(url);
879 }
880 marshallingOK =
881 marshallingOK && context->GetString("selectedText", &selectedText);
882
883 if (!marshallingOK) {
884 _searchContext.reset();
885 [self updateUI];
886 // The JavaScript will have taken care of clearing the highlighting.
887 return;
888 }
889 _searchContext.reset(
890 new ContextualSearchContext(selectedText, true, sentUrl, encoding));
891
892 if ([self canExtractTapContext]) {
893 marshallingOK =
894 marshallingOK &&
895 context->GetString("surroundingText",
896 &_searchContext->surrounding_text) &&
897 context->GetInteger("offsetStart", &_searchContext->start_offset) &&
898 context->GetInteger("offsetEnd", &_searchContext->end_offset);
899 }
900
901 if (!marshallingOK) {
902 _searchContext.reset();
903 [self updateUI];
904 // The JavaScript will have taken care of clearing the highlighting.
905 return;
906 }
907
908 DVLOG(1) << "Contextual search results:\n"
909 << " URL: " << _searchContext->page_url.spec() << "\n"
910 << " selectedText: " << _searchContext->selected_text << "\n"
911 << " offsets: " << _searchContext->start_offset << "-"
912 << _searchContext->end_offset << "\n"
913 << " surroundingText: " << _searchContext->surrounding_text;
914
915 std::string rects;
916 if (!context->GetString("rects", &rects)) {
917 _searchContext.reset();
918 [self updateUI];
919 return;
920 }
921 NSArray* rectsArray = StringValueToRectArray(rects);
922 if (!rectsArray) {
923 _searchContext.reset();
924 [self updateUI];
925 return;
926 }
927 [self highlightRects:rectsArray];
928
929 [self scrollToShowSelection:[weakWebViewProxy scrollViewProxy]];
930
931 // Update the content view and the state of the UI.
932 [self updateUI];
933 _preventPreload = NO;
934
935 _delegate->PostSearchTermRequest(_searchContext);
936 _searchTriggeredBySelection = NO;
937 };
938 [_contextualSearchJsManager fetchContextFromSelectionAtPoint:relativeTapPoint
939 completionHandler:handler];
940 }
941
942 - (void)handleHighlightJSResult:(id)result withError:(NSError*)error {
943 if (error) {
944 [self highlightRects:nil];
945 [_contextualSearchJsManager clearHighlight];
946 return;
947 }
948 std::string JSON(
949 base::SysNSStringToUTF8(base::mac::ObjCCastStrict<NSString>(result)));
950 // |json| is a JSON dicionary containing at list 2 entries:
951 // - 'rects': containing a list of rect dictionaries representing the zone of
952 // the page to highlight as a string in the format
953 // top1 bottom1 left1 right1,top2 bottom2 left2 right2,...,
954 // - 'size': containing a dictionary containing the size of the document as
955 // seen in JavaScript.
956 // As the 'rects' coordinates are based on a document which size is contained
957 // in 'size', if the web content view does not have the same size, they should
958 // not be considered.
959
960 std::unique_ptr<base::Value> parsedResult(
961 base::JSONReader::Read(JSON, false));
962 if (!parsedResult.get() ||
963 !parsedResult->IsType(base::Value::Type::DICTIONARY)) {
964 return;
965 }
966 base::DictionaryValue* resultDict =
967 static_cast<base::DictionaryValue*>(parsedResult.get());
968
969 CGSize contentSize = [_webViewProxy scrollViewProxy].contentSize;
970 const base::DictionaryValue* contentSizeDict;
971 if (resultDict->GetDictionary("size", &contentSizeDict)) {
972 double width, height;
973 if (!contentSizeDict->GetDouble("height", &height) ||
974 !contentSizeDict->GetDouble("width", &width)) {
975 // Value is not correctly formatted. Early return.
976 return;
977 }
978 width *= [_webViewProxy scrollViewProxy].zoomScale;
979 height *= [_webViewProxy scrollViewProxy].zoomScale;
980 if (fabsl(contentSize.width - width) > 2 ||
981 fabsl(contentSize.height - height) > 2) {
982 // The coords in of the UIView and in JavaScript are not synced. A scroll
983 // now would be almost random.
984 _preventScrollToShowSelection = YES;
985 dispatch_after(
986 dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
987 dispatch_get_main_queue(), ^{
988 [self updateHighlight];
989 });
990 return;
991 }
992 _preventScrollToShowSelection = NO;
993 }
994
995 std::string rectsList;
996 if (resultDict->GetString("rects", &rectsList)) {
997 NSArray* rects = StringValueToRectArray(rectsList);
998 if (rects) {
999 [self highlightRects:rects];
1000 [self scrollToShowSelection:[_webViewProxy scrollViewProxy]];
1001 }
1002 }
1003 }
1004
1005 - (void)updateForResolvedSearch:
1006 (ContextualSearchDelegate::SearchResolution)resolution {
1007 _resolvedSearch = resolution;
1008
1009 DVLOG(1) << "is invalid: " << _resolvedSearch.is_invalid << "\n"
1010 << "response code: " << _resolvedSearch.response_code << "\n"
1011 << "search term: " << _resolvedSearch.search_term << "\n"
1012 << "search term: " << _resolvedSearch.alternate_term << "\n"
1013 << "display text: " << _resolvedSearch.display_text << "\n"
1014 << "stop preload: " << _resolvedSearch.prevent_preload;
1015
1016 if (_resolvedSearch.is_invalid) {
1017 [self dismissPane:ContextualSearch::UNKNOWN];
1018 } else {
1019 _searchTermResolved = YES;
1020 [_headerView
1021 setSearchTerm:base::SysUTF8ToNSString(_resolvedSearch.display_text)
1022 animated:[_panelView state] != ContextualSearch::DISMISSED];
1023 if (_resolvedSearch.start_offset != -1 &&
1024 _resolvedSearch.end_offset != -1) {
1025 base::WeakNSObject<ContextualSearchController> weakSelf(self);
1026 [_contextualSearchJsManager
1027 expandHighlightToStartOffset:_resolvedSearch.start_offset
1028 endOffset:_resolvedSearch.end_offset
1029 completionHandler:^(id result, NSError* error) {
1030 [weakSelf handleHighlightJSResult:result
1031 withError:error];
1032 }];
1033 }
1034 GURL url = _delegate->GetURLForResolvedSearch(_resolvedSearch, true);
1035 [_searchResultsView createTabForSearch:url
1036 preloadEnabled:!_resolvedSearch.prevent_preload];
1037 // Record the tap-to-search interval.
1038 ContextualSearch::RecordTimeToSearch(base::Time::Now() - _tapTime);
1039 }
1040 }
1041
1042 - (void)updateUI {
1043 if (_searchContext) {
1044 ContextualSearch::RecordSelectionIsValid(true);
1045 [self peekPane:ContextualSearch::TEXT_SELECT_TAP];
1046 _searchInvolvedFirstRun =
1047 self.permissions.preferenceState == TouchToSearch::UNDECIDED;
1048
1049 if (_searchContext->surrounding_text.empty()) {
1050 [_headerView
1051 setSearchTerm:base::SysUTF8ToNSString(_searchContext->selected_text)
1052 animated:[_panelView state] != ContextualSearch::DISMISSED];
1053 } else {
1054 NSString* surroundingText =
1055 base::SysUTF16ToNSString(_searchContext->surrounding_text);
1056 NSInteger startOffset =
1057 CLAMP(0, _searchContext->start_offset,
1058 static_cast<NSInteger>([surroundingText length]));
1059 NSString* displayedText =
1060 [surroundingText substringFromIndex:startOffset];
1061 NSInteger adjusted_offset =
1062 _searchContext->end_offset - _searchContext->start_offset;
1063 NSInteger followingOffset = CLAMP(
1064 0, adjusted_offset, static_cast<NSInteger>([surroundingText length]));
1065 NSRange followingTextRange =
1066 NSMakeRange(followingOffset, displayedText.length - followingOffset);
1067 [_headerView setText:displayedText
1068 followingTextRange:followingTextRange
1069 animated:[_panelView state] != ContextualSearch::DISMISSED];
1070 }
1071 } else {
1072 ContextualSearch::RecordSelectionIsValid(false);
1073 [self dismissPane:ContextualSearch::INVALID_SELECTION];
1074 }
1075 }
1076 - (void)scrollToShowSelection:(CRWWebViewScrollViewProxy*)scrollView {
1077 if (!scrollView || _preventScrollToShowSelection)
1078 return;
1079 if (!_contextualHighlightView.get()) {
1080 return;
1081 }
1082 CGRect highlightBoundingRect = [_contextualHighlightView boundingRect];
1083 if (CGRectIsNull(highlightBoundingRect)) {
1084 return;
1085 }
1086
1087 // Do the maths without the insets.
1088 CGPoint scrollPoint = [scrollView contentOffset];
1089 scrollPoint.y += scrollView.contentInset.top;
1090 scrollPoint.x += scrollView.contentInset.left;
1091
1092 // Coordinates of the bounding box to show.
1093 CGFloat top = CGRectGetMinY(highlightBoundingRect);
1094 CGFloat bottom = CGRectGetMaxY(highlightBoundingRect);
1095 CGFloat left = CGRectGetMinX(highlightBoundingRect);
1096 CGFloat right = CGRectGetMaxX(highlightBoundingRect);
1097
1098 CGSize displaySize = [_contextualHighlightView frame].size;
1099
1100 CGFloat panelHeight = CGRectGetHeight(
1101 CGRectIntersection([_panelView frame], [_panelView superview].bounds));
1102
1103 displaySize.height -= scrollView.contentInset.top +
1104 scrollView.contentInset.bottom + panelHeight;
1105 displaySize.width -=
1106 scrollView.contentInset.left + scrollView.contentInset.right;
1107
1108 // Coordinates of the displayed frame in the same coordinates system.
1109 CGFloat frameTop = scrollPoint.y;
1110 CGFloat frameBottom = frameTop + displaySize.height;
1111 CGFloat frameLeft = scrollPoint.x;
1112 CGFloat frameRight = frameLeft + displaySize.width;
1113
1114 CGSize contentSize = scrollView.contentSize;
1115 CGFloat maxOffsetY = MAX(contentSize.height - displaySize.height, 0);
1116 CGFloat maxOffsetX = MAX(contentSize.width - displaySize.width, 0);
1117
1118 if (highlightBoundingRect.size.width + 2 * kXScrollMargin >
1119 displaySize.width) {
1120 // Selection does not fit in the screen. Center horizontal scroll.
1121 if (contentSize.width > displaySize.width) {
1122 scrollPoint.x = (left + right - displaySize.width) / 2;
1123 }
1124 } else {
1125 // Make sure right is visible.
1126 if (right + kXScrollMargin > frameRight) {
1127 scrollPoint.x = right + kXScrollMargin - displaySize.width;
1128 }
1129
1130 // Make sure left is visible.
1131 if (left - kXScrollMargin < frameLeft) {
1132 scrollPoint.x = left - kXScrollMargin;
1133 }
1134 }
1135
1136 // Make sure bottom is visible.
1137 if (bottom + kYScrollMargin > frameBottom) {
1138 scrollPoint.y = bottom + kYScrollMargin - displaySize.height;
1139 }
1140
1141 // Make sure top is visible.
1142 if (top - kYScrollMargin - [_controllerDelegate currentHeaderHeight] <
1143 frameTop) {
1144 scrollPoint.y =
1145 top - kYScrollMargin - [_controllerDelegate currentHeaderHeight];
1146 }
1147
1148 if (scrollPoint.x < 0)
1149 scrollPoint.x = 0;
1150 if (scrollPoint.x > maxOffsetX) {
1151 scrollPoint.x = maxOffsetX;
1152 }
1153 if (scrollPoint.y < 0)
1154 scrollPoint.y = 0;
1155 if (scrollPoint.y > maxOffsetY)
1156 scrollPoint.y = maxOffsetY;
1157
1158 scrollPoint.y -= scrollView.contentInset.top;
1159 scrollPoint.x -= scrollView.contentInset.left;
1160 [scrollView setContentOffset:scrollPoint animated:YES];
1161 }
1162
1163 - (void)highlightRects:(NSArray*)rects {
1164 if (![self webState]) {
1165 return;
1166 }
1167 if (!_contextualHighlightView.get() && [rects count]) {
1168 CGRect frame = [[self webState]->GetWebViewProxy() frame];
1169 ContextualSearchHighlighterView* highlightView =
1170 [[[ContextualSearchHighlighterView alloc] initWithFrame:frame
1171 delegate:self]
1172 autorelease];
1173 _contextualHighlightView.reset(highlightView);
1174 [[self webState]->GetWebViewProxy() addSubview:highlightView];
1175 }
1176 CGPoint scroll = [[_webViewProxy scrollViewProxy] contentOffset];
1177 [_contextualHighlightView
1178 highlightRects:rects
1179 withOffset:[_controllerDelegate currentHeaderHeight]
1180 zoom:[[_webViewProxy scrollViewProxy] zoomScale]
1181 scroll:scroll];
1182 }
1183
1184 - (void)willShowMenuNotification {
1185 if (!_preventActionMenu)
1186 return;
1187 BOOL dismiss = NO;
1188 if ([_panelView state] > ContextualSearch::PEEKING) {
1189 dismiss = YES;
1190 }
1191 if ([_panelView state] == ContextualSearch::PEEKING) {
1192 CGPoint headerTop = [_headerView convertPoint:CGPointZero toView:nil];
1193 CGRect menuRect = [[UIMenuController sharedMenuController] menuFrame];
1194 if (headerTop.y < CGRectGetMaxY(menuRect)) {
1195 dismiss = YES;
1196 }
1197 }
1198 if (dismiss) {
1199 dispatch_async(dispatch_get_main_queue(), ^{
1200 [[UIMenuController sharedMenuController] setMenuVisible:NO];
1201 });
1202 }
1203 }
1204
1205 - (void)close {
1206 if (_closed)
1207 return;
1208
1209 _closed = YES;
1210 [self disableCurrentWebState];
1211 [self setWebState:nil];
1212 [_headerView removeGestureRecognizer:_copyGestureRecognizer];
1213 [[_panelView window] removeGestureRecognizer:_dismissRecognizer];
1214 _delegate.reset();
1215 [_searchResultsView setActive:NO];
1216 _searchResultsView.reset();
1217 }
1218
1219 #pragma mark - Promo view management
1220
1221 - (void)userOptedInFromPromo:(BOOL)optIn {
1222 if (optIn) {
1223 self.permissions.preferenceState = TouchToSearch::ENABLED;
1224 [_promoView closeAnimated:YES];
1225 [_promoView setDisabled:YES];
1226 } else {
1227 [self dismissPane:ContextualSearch::OPTOUT];
1228 self.permissions.preferenceState = TouchToSearch::DISABLED;
1229 }
1230 ContextualSearch::RecordFirstRunFlowOutcome(self.permissions.preferenceState);
1231 }
1232
1233 #pragma mark - ContextualSearchPreloadChecker
1234
1235 - (BOOL)canPreloadSearchResults {
1236 if (_preventPreload) {
1237 return NO;
1238 }
1239 return [self.permissions canPreloadSearchResults];
1240 }
1241
1242 #pragma mark - ContextualSearchTabPromoter
1243
1244 - (void)promoteTabHeaderPressed:(BOOL)headerPressed {
1245 // Move the panel so it's covering before the transition.
1246 if ([_panelView state] != ContextualSearch::COVERING) {
1247 [self coverPane:ContextualSearch::SERP_NAVIGATION];
1248 }
1249 // TODO(crbug.com/455334): Make this transition look nicer.
1250 [_searchResultsView scrollToTopAnimated:YES];
1251
1252 [self cleanUpWebStateForDismissWithCompletion:nil];
1253
1254 // Tell the BVC to handle the promotion, which will cause a new panel view
1255 // to be created.
1256 [_controllerDelegate promotePanelToTabProvidedBy:_searchResultsView
1257 focusInput:NO];
1258 }
1259
1260 #pragma mark - ContextualSearchPanelMotionObserver
1261
1262 - (void)panel:(ContextualSearchPanelView*)panel
1263 didStopMovingWithMotion:(ContextualSearch::PanelMotion)motion {
1264 if (motion.state == ContextualSearch::DISMISSED) {
1265 [self dismissPane:ContextualSearch::SWIPE];
1266 } else if (motion.state == ContextualSearch::PEEKING) {
1267 // newOrigin is above peeking height but below preview height.
1268 if ([_panelView state] >= ContextualSearch::PREVIEWING) {
1269 // Dragged down from previewing or covering
1270 [self peekPane:ContextualSearch::SWIPE];
1271 } else {
1272 // Dragged up or stayed the same.
1273 [self previewPane:ContextualSearch::SWIPE];
1274 }
1275 } else {
1276 if ([_panelView state] == ContextualSearch::COVERING) {
1277 if (motion.state != ContextualSearch::COVERING) {
1278 // Dragged down from covering.
1279 [self previewPane:ContextualSearch::SWIPE];
1280 }
1281 } else {
1282 // Dragged up.
1283 [self coverPane:ContextualSearch::SWIPE];
1284 }
1285 }
1286 [self updateHighlight];
1287 }
1288
1289 - (void)panelWillPromote:(ContextualSearchPanelView*)panel {
1290 DCHECK(panel == _panelView);
1291 [panel removeMotionObserver:self];
1292 _panelView.reset();
1293 [self setState:ContextualSearch::DISMISSED
1294 reason:ContextualSearch::TAB_PROMOTION];
1295 }
1296
1297 #pragma mark - ContextualSearchPanelTapHandler
1298
1299 - (void)panelWasTapped:(UIGestureRecognizer*)gesture {
1300 // Tapping when peeking switches to previewing.
1301 // Tapping otherwise turns the panel into a tab.
1302 if ([_panelView state] == ContextualSearch::PEEKING) {
1303 [self previewPane:ContextualSearch::SEARCH_BAR_TAP];
1304 } else {
1305 [self promoteTabHeaderPressed:YES];
1306 }
1307 }
1308
1309 - (void)closePanel {
1310 [self dismissPane:ContextualSearch::SEARCH_BAR_TAP];
1311 }
1312
1313 #pragma mark - State change methods
1314
1315 - (void)setState:(ContextualSearch::PanelState)state
1316 reason:(ContextualSearch::StateChangeReason)reason {
1317 ContextualSearch::PanelState fromState = [_panelView state];
1318
1319 // If we're moving to PEEKING as a result of text selection, that's starting
1320 // a new search.
1321 BOOL startingSearch = state == ContextualSearch::PEEKING &&
1322 (reason == ContextualSearch::TEXT_SELECT_TAP ||
1323 reason == ContextualSearch::TEXT_SELECT_LONG_PRESS);
1324 // If we're showing anything, then there's an ongoing search.
1325 BOOL ongoingSearch = fromState > ContextualSearch::DISMISSED;
1326 // If there's an ongoing search and we're dismissing or starting a search,
1327 // then we're ending a search.
1328 BOOL endingSearch =
1329 ongoingSearch && (state == ContextualSearch::DISMISSED || startingSearch);
1330 // If we're starting a search while there's one already there, it's chained.
1331 BOOL chained = startingSearch && endingSearch;
1332
1333 BOOL sameState = fromState == state;
1334 BOOL firstExitFromPeeking = fromState == ContextualSearch::PEEKING &&
1335 !_exitedPeeking && (!sameState || startingSearch);
1336 BOOL firstExitFromPreviewing = fromState == ContextualSearch::PREVIEWING &&
1337 !_exitedPreviewing && !sameState;
1338 BOOL firstExitFromCovering =
1339 fromState == ContextualSearch::COVERING && !_exitedCovering && !sameState;
1340
1341 _resultsVisible = _resultsVisible || [_searchResultsView contentVisible];
1342
1343 if (endingSearch) {
1344 if (_searchInvolvedFirstRun) {
1345 // If the first run panel might have been shown, did the user see it?
1346 ContextualSearch::RecordFirstRunPanelSeen(_firstRunPanelBecameVisible);
1347 }
1348 // Record search timing.
1349 [_searchResultsView recordFinishedSearchChained:chained];
1350 // Record if the user saw the search results.
1351 if (_searchTriggeredBySelection) {
1352 ContextualSearch::RecordSelectionResultsSeen(_resultsVisible);
1353 } else {
1354 ContextualSearch::RecordTapResultsSeen(_resultsVisible);
1355 }
1356 }
1357
1358 // Log state change. We only log the first transition to a state within a
1359 // contextual search. Note that when a user clicks on a link on the search
1360 // content view, this will trigger a transition to COVERING (SERP_NAVIGATION)
1361 // followed by a transition to DISMISSED (TAB_PROMOTION). For the purpose of
1362 // logging, the reason for the second transition is reinterpreted to
1363 // SERP_NAVIGATION, in order to distinguish it from a tab promotion caused
1364 // when tapping on the header when the panel is maximized.
1365 ContextualSearch::StateChangeReason loggedReason =
1366 _usedSERPNavigation ? ContextualSearch::SERP_NAVIGATION : reason;
1367 if (startingSearch || endingSearch ||
1368 (!sameState && !_enteredPreviewing &&
1369 state == ContextualSearch::PREVIEWING) ||
1370 (!sameState && !_enteredCovering &&
1371 state == ContextualSearch::COVERING)) {
1372 ContextualSearch::RecordFirstStateEntry(fromState, state, loggedReason);
1373 }
1374 if ((startingSearch && !chained) || firstExitFromPeeking ||
1375 firstExitFromPreviewing || firstExitFromCovering) {
1376 ContextualSearch::RecordFirstStateExit(fromState, state, loggedReason);
1377 }
1378
1379 if (firstExitFromPeeking) {
1380 _exitedPeeking = YES;
1381 } else if (firstExitFromPreviewing) {
1382 _exitedPreviewing = YES;
1383 } else if (firstExitFromCovering) {
1384 _exitedCovering = YES;
1385 }
1386
1387 [_panelView setState:state];
1388 // If the panel is now visible, enable the window-tap detector.
1389
1390 [self updateDismissRecognizer];
1391
1392 if (state == ContextualSearch::PREVIEWING) {
1393 _enteredPreviewing = YES;
1394 } else if (state == ContextualSearch::COVERING) {
1395 _enteredCovering = YES;
1396 }
1397
1398 if (reason == ContextualSearch::SERP_NAVIGATION) {
1399 _usedSERPNavigation = YES;
1400 }
1401
1402 if (endingSearch) {
1403 _enteredPreviewing = NO;
1404 _enteredCovering = NO;
1405 _resultsVisible = NO;
1406 _exitedPeeking = NO;
1407 _exitedPreviewing = NO;
1408 _exitedCovering = NO;
1409 _searchInvolvedFirstRun = NO;
1410 _firstRunPanelBecameVisible = NO;
1411 _searchTermResolved = NO;
1412 _usedSERPNavigation = NO;
1413 }
1414 }
1415
1416 - (void)
1417 dismissPaneWithJavascriptCompletionHandler:(ProceduralBlock)completionHandler
1418 reason:(ContextualSearch::StateChangeReason)
1419 reason {
1420 [self cleanUpWebStateForDismissWithCompletion:completionHandler];
1421 [self setState:ContextualSearch::DISMISSED reason:reason];
1422 }
1423
1424 - (void)cleanUpWebStateForDismissWithCompletion:
1425 (ProceduralBlock)completionHandler {
1426 _lastDismiss.reset([[NSDate date] retain]);
1427 _currentTapCancelled = YES;
1428 ContextualSearch::PanelState originalState = [_panelView state];
1429 if (originalState == ContextualSearch::DISMISSED) {
1430 DCHECK(![_searchResultsView active]);
1431 if ([self webState]) {
1432 DOMAlteringLock* lock = DOMAlteringLock::FromWebState([self webState]);
1433 if (lock) {
1434 lock->Release(self);
1435 }
1436 }
1437 if (completionHandler)
1438 completionHandler();
1439 return;
1440 }
1441
1442 [_doubleTapRecognizer setEnabled:YES];
1443 _searchContext.reset();
1444 [_searchResultsView setActive:NO];
1445 _delegate->CancelSearchTermRequest();
1446 _selectedText = "";
1447
1448 ContextualSearchDelegate::SearchResolution blank;
1449 _resolvedSearch = blank;
1450 if (completionHandler) {
1451 base::WeakNSObject<ContextualSearchController> weakSelf(self);
1452 ProceduralBlock javaScriptCompletion = ^{
1453 if ([self webState]) {
1454 DOMAlteringLock::FromWebState([self webState])->Release(self);
1455 completionHandler();
1456 }
1457 };
1458 [self highlightRects:nil];
1459 [_contextualSearchJsManager clearHighlight];
1460 javaScriptCompletion();
1461 } else {
1462 [self highlightRects:nil];
1463 [_contextualSearchJsManager clearHighlight];
1464 DOMAlteringLock::FromWebState([self webState])->Release(self);
1465 }
1466
1467 _preventActionMenu = NO;
1468
1469 // If the tapped word was at the bottom of the webview, and it was scrolled
1470 // up to be displayed over the pane, scroll it back down now.
1471 // (Ideally this "overscrolling" should just happen as the pane moves).
1472 // TODO(crbug.com/546227): Handle this with a constraint.
1473 CGPoint contentOffset = [[_webViewProxy scrollViewProxy] contentOffset];
1474 CGSize contentSize = [[_webViewProxy scrollViewProxy] contentSize];
1475 CGSize viewSize = [[_webViewProxy scrollViewProxy] frame].size;
1476 CGFloat maxOffset = contentSize.height - viewSize.height;
1477 if (contentOffset.y > maxOffset) {
1478 contentOffset.y = maxOffset;
1479 [[_webViewProxy scrollViewProxy] setContentOffset:contentOffset
1480 animated:YES];
1481 }
1482 }
1483
1484 - (void)dismissPane:(ContextualSearch::StateChangeReason)reason {
1485 [self dismissPaneWithJavascriptCompletionHandler:nil reason:reason];
1486 }
1487
1488 - (void)peekPane:(ContextualSearch::StateChangeReason)reason {
1489 [self setState:ContextualSearch::PEEKING reason:reason];
1490 [_doubleTapRecognizer setEnabled:NO];
1491 [self scrollToShowSelection:[_webViewProxy scrollViewProxy]];
1492 }
1493
1494 - (void)previewPane:(ContextualSearch::StateChangeReason)reason {
1495 if (_searchInvolvedFirstRun) {
1496 _firstRunPanelBecameVisible = YES;
1497 }
1498 [self setState:ContextualSearch::PREVIEWING reason:reason];
1499 [_doubleTapRecognizer setEnabled:NO];
1500 [self scrollToShowSelection:[_webViewProxy scrollViewProxy]];
1501 _delegate->StartPendingSearchTermRequest();
1502 }
1503
1504 - (void)coverPane:(ContextualSearch::StateChangeReason)reason {
1505 [self setState:ContextualSearch::COVERING reason:reason];
1506 }
1507
1508 - (void)movePanelOffscreen {
1509 [self dismissPane:ContextualSearch::RESET];
1510 }
1511
1512 #pragma mark - ContextualSearchPromoViewDelegate methods
1513
1514 - (void)promoViewAcceptTapped {
1515 [self userOptedInFromPromo:YES];
1516 }
1517
1518 - (void)promoViewDeclineTapped {
1519 [self userOptedInFromPromo:NO];
1520 }
1521
1522 - (void)promoViewSettingsTapped {
1523 base::scoped_nsobject<GenericChromeCommand> command(
1524 [[GenericChromeCommand alloc]
1525 initWithTag:IDC_SHOW_CONTEXTUAL_SEARCH_SETTINGS]);
1526 UIWindow* main_window = [[UIApplication sharedApplication] keyWindow];
1527 [main_window chromeExecuteCommand:command];
1528 }
1529
1530 #pragma mark - ContextualSearchWebStateObserver methods
1531
1532 - (void)webState:(web::WebState*)webState
1533 pageLoadedWithStatus:(web::PageLoadCompletionStatus)loadStatus {
1534 if (loadStatus != web::PageLoadCompletionStatus::SUCCESS)
1535 return;
1536
1537 [self movePanelOffscreen];
1538 _isScriptInjected = NO;
1539 [self enableCurrentWebState];
1540 }
1541
1542 - (void)webStateDestroyed:(web::WebState*)webState {
1543 [self updateWebViewProxy:nil];
1544 }
1545
1546 #pragma mark - UIGestureRecognizerDelegate Methods
1547
1548 // Ensures that |_tapRecognizer| and |_doubleTapRecognizer| cooperate with all
1549 // other gesture recognizers.
1550 - (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
1551 shouldRecognizeSimultaneouslyWithGestureRecognizer:
1552 (UIGestureRecognizer*)otherGestureRecognizer {
1553 return gestureRecognizer == _tapRecognizer.get() ||
1554 gestureRecognizer == _doubleTapRecognizer.get();
1555 }
1556
1557 #pragma mark - CRWWebViewScrollViewObserver methods
1558
1559 - (void)webViewScrollViewWillBeginDragging:
1560 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
1561 [self dismissPane:ContextualSearch::BASE_PAGE_SCROLL];
1562 [_tapRecognizer setEnabled:NO];
1563 }
1564
1565 - (void)webViewScrollViewDidEndDragging:
1566 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy
1567 willDecelerate:(BOOL)decelerate {
1568 if (!decelerate)
1569 [_tapRecognizer setEnabled:YES];
1570 }
1571
1572 - (void)webViewScrollViewDidEndDecelerating:
1573 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
1574 [_tapRecognizer setEnabled:YES];
1575 }
1576
1577 - (void)webViewScrollViewDidScroll:
1578 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
1579 _currentTapCancelled = YES;
1580 [_contextualHighlightView
1581 setScroll:[webViewScrollViewProxy contentOffset]
1582 zoom:[webViewScrollViewProxy zoomScale]
1583 offset:[_controllerDelegate currentHeaderHeight]];
1584 }
1585
1586 - (void)webViewScrollViewDidZoom:
1587 (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
1588 _currentTapCancelled = YES;
1589 [_contextualHighlightView
1590 setScroll:[webViewScrollViewProxy contentOffset]
1591 zoom:[webViewScrollViewProxy zoomScale]
1592 offset:[_controllerDelegate currentHeaderHeight]];
1593 [self scrollToShowSelection:webViewScrollViewProxy];
1594 }
1595
1596 #pragma mark - DOMAltering methods
1597
1598 - (BOOL)canReleaseDOMLock {
1599 return YES;
1600 }
1601
1602 - (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler {
1603 [self dismissPaneWithJavascriptCompletionHandler:completionHandler
1604 reason:ContextualSearch::RESET];
1605 }
1606
1607 #pragma mark - TouchToSearchPermissionsChangeAudience methods
1608
1609 - (void)touchToSearchDidChangePreferenceState:
1610 (TouchToSearch::TouchToSearchPreferenceState)preferenceState {
1611 if (preferenceState != TouchToSearch::UNDECIDED) {
1612 ContextualSearch::RecordPreferenceChanged(preferenceState ==
1613 TouchToSearch::ENABLED);
1614 }
1615 }
1616
1617 - (void)touchToSearchPermissionsUpdated {
1618 // This method is already invoked asynchronously, so it's safe to
1619 // synchronously attempt to enable the feature.
1620 [self enableContextualSearch:YES];
1621 }
1622
1623 #pragma mark - ContextualSearchHighlighterDelegate methods
1624
1625 - (void)updateHighlight {
1626 base::WeakNSObject<ContextualSearchController> weakSelf(self);
1627 [_contextualSearchJsManager
1628 highlightRectsWithCompletionHandler:^void(id result, NSError* error) {
1629 [weakSelf handleHighlightJSResult:result withError:error];
1630 }];
1631 }
1632
1633 @end
OLDNEW

Powered by Google App Engine
This is Rietveld 408576698