OLD | NEW |
(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 |
OLD | NEW |