| Index: ios/chrome/browser/ui/contextual_search/contextual_search_controller.mm
|
| diff --git a/ios/chrome/browser/ui/contextual_search/contextual_search_controller.mm b/ios/chrome/browser/ui/contextual_search/contextual_search_controller.mm
|
| new file mode 100644
|
| index 0000000000000000000000000000000000000000..39bc9d8a9e315e4b90e22b306abb5b00f051c6df
|
| --- /dev/null
|
| +++ b/ios/chrome/browser/ui/contextual_search/contextual_search_controller.mm
|
| @@ -0,0 +1,1633 @@
|
| +// Copyright 2014 The Chromium Authors. All rights reserved.
|
| +// Use of this source code is governed by a BSD-style license that can be
|
| +// found in the LICENSE file.
|
| +
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_controller.h"
|
| +
|
| +#include <memory>
|
| +#include <utility>
|
| +
|
| +#include "base/ios/ios_util.h"
|
| +#import "base/ios/weak_nsobject.h"
|
| +#include "base/json/json_reader.h"
|
| +#include "base/logging.h"
|
| +#import "base/mac/bind_objc_block.h"
|
| +#include "base/mac/foundation_util.h"
|
| +#include "base/mac/scoped_block.h"
|
| +#include "base/mac/scoped_nsobject.h"
|
| +#include "base/strings/sys_string_conversions.h"
|
| +#include "base/strings/utf_string_conversions.h"
|
| +#include "base/time/time.h"
|
| +#include "base/values.h"
|
| +#include "components/google/core/browser/google_util.h"
|
| +#include "components/search_engines/template_url_service.h"
|
| +#include "ios/chrome/browser/application_context.h"
|
| +#import "ios/chrome/browser/procedural_block_types.h"
|
| +#import "ios/chrome/browser/tabs/tab.h"
|
| +#import "ios/chrome/browser/ui/commands/UIKit+ChromeExecuteCommand.h"
|
| +#import "ios/chrome/browser/ui/commands/generic_chrome_command.h"
|
| +#include "ios/chrome/browser/ui/commands/ios_command_ids.h"
|
| +#include "ios/chrome/browser/ui/contextual_search/contextual_search_context.h"
|
| +#include "ios/chrome/browser/ui/contextual_search/contextual_search_delegate.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_header_view.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_highlighter_view.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_metrics.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_protocols.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_promo_view.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/contextual_search_results_view.h"
|
| +#include "ios/chrome/browser/ui/contextual_search/contextual_search_web_state_observer.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/js_contextual_search_manager.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/touch_to_search_permissions_mediator.h"
|
| +#import "ios/chrome/browser/ui/contextual_search/window_gesture_observer.h"
|
| +#import "ios/chrome/browser/ui/show_privacy_settings_util.h"
|
| +#include "ios/chrome/browser/ui/ui_util.h"
|
| +#import "ios/chrome/browser/ui/uikit_ui_util.h"
|
| +#import "ios/chrome/browser/web/dom_altering_lock.h"
|
| +#include "ios/chrome/common/string_util.h"
|
| +#include "ios/chrome/grit/ios_strings.h"
|
| +#import "ios/third_party/material_components_ios/src/components/Snackbar/src/MaterialSnackbar.h"
|
| +#include "ios/web/public/browser_state.h"
|
| +#include "ios/web/public/load_committed_details.h"
|
| +#include "ios/web/public/referrer.h"
|
| +#import "ios/web/public/web_state/crw_web_view_proxy.h"
|
| +#import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h"
|
| +#import "ios/web/public/web_state/js/crw_js_injection_receiver.h"
|
| +#include "ios/web/public/web_state/web_state.h"
|
| +#include "ios/web/public/web_state/web_state_observer.h"
|
| +#include "ui/base/l10n/l10n_util.h"
|
| +#include "ui/base/l10n/l10n_util_mac.h"
|
| +
|
| +// Returns |value| clamped so that min <= value <= max
|
| +#define CLAMP(min, value, max) MAX(min, MIN(value, max))
|
| +
|
| +namespace {
|
| +// command prefix for injected JavaScript.
|
| +const std::string kCommandPrefix = "contextualSearch";
|
| +
|
| +// Distance from edges of frame when scrolling to show selection.
|
| +const CGFloat kYScrollMargin = 30.0;
|
| +const CGFloat kXScrollMargin = 10.0;
|
| +
|
| +// Delay to check if there is a DOM modification (in second).
|
| +// If delay is too short, JavaScript won't have time to handle the event and the
|
| +// DOM tree will be modified after the highlight.
|
| +// If delay is too long, the user experience will be degraded.
|
| +// Experiments on some websites (e.g. belgianrails.be) show that delay must be
|
| +// over 0.35 second.
|
| +// Timeline is as follow:
|
| +// t: tap happens,
|
| +// t + doubleTapDelay (t1): tap is triggered
|
| +// t1: JavaScript handles tap,
|
| +// t1 + delta1: DOM may be modified,
|
| +// t1 + kDOMModificationDelaySeconds: |handleTapFrom:| starts handling tap,
|
| +// t1 + kDOMModificationDelaySeconds + delta2: JavaScript handleTap is called.
|
| +//
|
| +// The delay between DOM mutation and contextual search tap handling is really
|
| +// kDOMModificationDelaySeconds + delta2 - delta1.
|
| +// To handle this random delta value, the timeout passed to JavaScript is
|
| +// doubled
|
| +// The body touch event timeout must include the double tap delay.
|
| +const CGFloat kDOMModificationDelaySeconds = 0.1;
|
| +const CGFloat kDOMModificationDelayForJavaScriptMilliseconds =
|
| + 2 * 1000 * kDOMModificationDelaySeconds;
|
| +
|
| +// The touch delay disables CS on many sites, so for now it is disabled.
|
| +// The previous value used was:
|
| +// kDOMModificationDelayForJavaScriptMilliseconds + 300
|
| +const CGFloat kBodyTouchDelayForJavaScriptMilliseconds = 0;
|
| +
|
| +// After a dismiss, do not allow retrigger before a delay to prevent triggering
|
| +// on double tap, and to prevent retrigger on the same event.
|
| +const NSTimeInterval kPreventTriggerAfterDismissDelaySeconds = 0.3;
|
| +
|
| +CGRect StringValueToRect(NSString* rectString) {
|
| + double rectTop, rectBottom, rectLeft, rectRight;
|
| + NSArray* items = [rectString componentsSeparatedByString:@" "];
|
| + if ([items count] != 4) {
|
| + return CGRectNull;
|
| + }
|
| + rectTop = [[items objectAtIndex:0] doubleValue];
|
| + rectBottom = [[items objectAtIndex:1] doubleValue];
|
| + rectLeft = [[items objectAtIndex:2] doubleValue];
|
| + rectRight = [[items objectAtIndex:3] doubleValue];
|
| + if (isnan(rectTop) || isinf(rectTop) || isnan(rectBottom) ||
|
| + isinf(rectBottom) || isnan(rectLeft) || isinf(rectLeft) ||
|
| + isnan(rectRight) || isinf(rectRight) || rectRight <= rectLeft ||
|
| + rectBottom <= rectTop) {
|
| + return CGRectNull;
|
| + }
|
| + CGRect rect =
|
| + CGRectMake(rectLeft, rectTop, rectRight - rectLeft, rectBottom - rectTop);
|
| + return rect;
|
| +}
|
| +
|
| +NSArray* StringValueToRectArray(const std::string& list) {
|
| + NSString* nsList = base::SysUTF8ToNSString(list);
|
| + NSMutableArray* rectsArray = [[[NSMutableArray alloc] init] autorelease];
|
| + NSArray* items = [nsList componentsSeparatedByString:@","];
|
| + for (NSString* item : items) {
|
| + CGRect rect = StringValueToRect(item);
|
| + if (CGRectIsNull(rect)) {
|
| + return nil;
|
| + }
|
| + [rectsArray addObject:[NSValue valueWithCGRect:rect]];
|
| + }
|
| + return rectsArray;
|
| +}
|
| +
|
| +} // namespace
|
| +
|
| +@interface ContextualSearchController ()<DOMAltering,
|
| + CRWWebViewScrollViewProxyObserver,
|
| + UIGestureRecognizerDelegate,
|
| + ContextualSearchHighlighterDelegate,
|
| + ContextualSearchPromoViewDelegate,
|
| + ContextualSearchPanelMotionObserver,
|
| + ContextualSearchPanelTapHandler,
|
| + ContextualSearchPreloadChecker,
|
| + ContextualSearchTabPromoter,
|
| + ContextualSearchWebStateDelegate,
|
| + TouchToSearchPermissionsChangeAudience>
|
| +
|
| +// Controller delegate for the controller to call back to.
|
| +@property(nonatomic, readwrite, assign) id<ContextualSearchControllerDelegate>
|
| + controllerDelegate;
|
| +
|
| +// Permissions interface for this feature. Property is readwrite for testing.
|
| +@property(nonatomic, readwrite, retain)
|
| + TouchToSearchPermissionsMediator* permissions;
|
| +
|
| +// Synchronous method executed by -asynchronouslyEnableContextualSearch:
|
| +- (void)doEnableContextualSearch:(BOOL)enabled;
|
| +
|
| +// Handler for injected JavaScript callbacks.
|
| +- (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand;
|
| +
|
| +// Handle the selection change event if the DOM lock is acquired.
|
| +// |selection| is the currently selected text in the webview.
|
| +// if |updated| is true, then the selection changed by the user moving one of
|
| +// the selection handles (not making a new selection).
|
| +// If |selectionValid| is false, the selection contains invalid chars or element
|
| +// and TTS should be dismissed. If selection is invalid, |selection| is empty.
|
| +- (void)handleSelectionChanged:(const std::string&)selection
|
| + selectionUpdated:(BOOL)update
|
| + selectionValid:(BOOL)selectionValid;
|
| +
|
| +// Action for the tap gesture recognizer.
|
| +- (void)handleTapFrom:(UIGestureRecognizer*)gestureRecognizer;
|
| +
|
| +// Handle a tap on a web view at |point|, extracting contextual search
|
| +// information from the tapped word and surrounding text.
|
| +- (void)handleTapAtPoint:(CGPoint)point;
|
| +
|
| +// Initialize the contextual search JavaScript.
|
| +- (void)initializeWebViewForContextualSearch;
|
| +
|
| +// Update the webViewProxy for the current tab to enable/disable scroll view
|
| +// observation.
|
| +- (void)updateWebViewProxy:(id<CRWWebViewProxy>)webViewProxy;
|
| +
|
| +// Updates the UI to match the current state, setting the text label content
|
| +// if there is a current search context, and setting the panel state.
|
| +- (void)updateUI;
|
| +
|
| +// Updates the UI for a resolved search.
|
| +- (void)updateForResolvedSearch:
|
| + (ContextualSearchDelegate::SearchResolution)resolution;
|
| +
|
| +// State changes.
|
| +// Set the state of the panel, given |reason|. Handles metrics updates.
|
| +- (void)setState:(ContextualSearch::PanelState)state
|
| + reason:(ContextualSearch::StateChangeReason)reason;
|
| +
|
| +// Dismiss pane for |reason|, invoking |completionHandler|, if any, after
|
| +// clearing any existing highlighted text in the webview, and finally releasing
|
| +// the DOM lock.
|
| +- (void)
|
| +dismissPaneWithJavascriptCompletionHandler:(ProceduralBlock)completionHandler
|
| + reason:(ContextualSearch::StateChangeReason)
|
| + reason;
|
| +
|
| +// Clean-up the web state (release lock, clear highlight...) in case of a
|
| +// dismiss.
|
| +- (void)cleanUpWebStateForDismissWithCompletion:
|
| + (ProceduralBlock)completionHandler;
|
| +
|
| +// Convenience method for dismissing the pane with no completion handler.
|
| +- (void)dismissPane:(ContextualSearch::StateChangeReason)reason;
|
| +
|
| +// Peek (show at the bottom of the window) the pane for |reason|.
|
| +- (void)peekPane:(ContextualSearch::StateChangeReason)reason;
|
| +
|
| +// Preview the pane (covering kPreviewingDisplayRatio of the webview) for
|
| +// |reason|.
|
| +- (void)previewPane:(ContextualSearch::StateChangeReason)reason;
|
| +
|
| +// Cover the pane (covering the entire webview) for |reason|.
|
| +- (void)coverPane:(ContextualSearch::StateChangeReason)reason;
|
| +
|
| +// Scroll the webview to show the highlighted text.
|
| +// Scroll the minimal distance to put |_highlightBoundingRect| at
|
| +// |kYScrollMargin| from top and bottom edges and |kXScrollMargin| from left and
|
| +// right edges.
|
| +// Overflow policy :
|
| +// - horizontal: center |_highlightBoundingRect|,
|
| +// - vertical: put |_highlightBoundingRect| at |kYScrollMargin| from top edge.
|
| +- (void)scrollToShowSelection:(CRWWebViewScrollViewProxy*)scrollView;
|
| +
|
| +// Creates, enables or disables the dismiss recognizer based on state_.
|
| +- (void)updateDismissRecognizer;
|
| +
|
| +@end
|
| +
|
| +@implementation ContextualSearchController {
|
| + // Permissions interface for this feature.
|
| + base::scoped_nsobject<TouchToSearchPermissionsMediator> _permissions;
|
| +
|
| + // WebState for the tab this object is attached to.
|
| + web::WebState* _webState;
|
| +
|
| + // Access to the web view from |_webState|.
|
| + base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
|
| +
|
| + // Observer for |_webState|.
|
| + std::unique_ptr<ContextualSearchWebStateObserver> _webStateObserver;
|
| +
|
| + // Observer for search tab's web state.
|
| + std::unique_ptr<ContextualSearchWebStateObserver> _searchTabWebStateObserver;
|
| +
|
| + // Object that manages find_in_page.js injection into the web view.
|
| + base::WeakNSObject<JsContextualSearchManager> _contextualSearchJsManager;
|
| +
|
| + // Gesture reccognizer for contextual search taps.
|
| + base::scoped_nsobject<UITapGestureRecognizer> _tapRecognizer;
|
| +
|
| + // Gesture reccognizer for double tap. It is used to prevent |_tapRecognizer|
|
| + // from firing if there is a double tap on the web view. It is disabled when
|
| + // the panel is displayed, since any tap will dismiss the panel in that case.
|
| + base::scoped_nsobject<UITapGestureRecognizer> _doubleTapRecognizer;
|
| +
|
| + // Gesture recognizer for long-tap copy.
|
| + base::scoped_nsobject<UILongPressGestureRecognizer> _copyGestureRecognizer;
|
| +
|
| + // Gesture recognizer to detect taps outside of the CS interface that would
|
| + // cause it to dismiss.
|
| + base::scoped_nsobject<WindowGestureObserver> _dismissRecognizer;
|
| +
|
| + // Context information retrieved from a search tap.
|
| + std::shared_ptr<ContextualSearchContext> _searchContext;
|
| +
|
| + // Resolved search information generated from the context or text selection.
|
| + ContextualSearchDelegate::SearchResolution _resolvedSearch;
|
| +
|
| + // Delegate for fetching search information.
|
| + std::unique_ptr<ContextualSearchDelegate> _delegate;
|
| +
|
| + // The panel view controlled by this object; it is created externally and
|
| + // owned by its superview. There is no guarantee about its lifetime.
|
| + base::WeakNSObject<ContextualSearchPanelView> _panelView;
|
| +
|
| + // The view containing the highlighting of the search terms.
|
| + base::WeakNSObject<ContextualSearchHighlighterView> _contextualHighlightView;
|
| +
|
| + // Content view displayed in the peeking section of the panel.
|
| + base::scoped_nsobject<ContextualSearchHeaderView> _headerView;
|
| +
|
| + // Vertical constraints for layout of the search tab.
|
| + base::scoped_nsobject<NSArray> _searchTabVerticalConstraints;
|
| +
|
| + // Container view for the opt-out promo and the search tab view.
|
| + base::scoped_nsobject<ContextualSearchResultsView> _searchResultsView;
|
| +
|
| + // View for the opt-out promo.
|
| + base::scoped_nsobject<ContextualSearchPromoView> _promoView;
|
| +
|
| + // The tab that should be used as the opener for the search tab.
|
| + Tab* _opener;
|
| +
|
| + // YES if a cancel event was received since last tap, meaning the current tap
|
| + // must not result in a search.
|
| + BOOL _currentTapCancelled;
|
| +
|
| + // The current selection text.
|
| + std::string _selectedText;
|
| +
|
| + // Boolean to track if the current WebState is enabled (has
|
| + // gesture recognizers and DOM lock set up).
|
| + BOOL _webStateEnabled;
|
| +
|
| + // Boolean to distinguish selection-clearing taps on the webview from
|
| + // those on other UI elements.
|
| + BOOL _webViewTappedWithSelection;
|
| +
|
| + // Metrics tracking variables and flags.
|
| + // Time the tap handler fires. The delay of doubleTap is not counted.
|
| + base::Time _tapTime;
|
| + // Has the user entered the previewing/covering state yet for the
|
| + // current search?
|
| + BOOL _enteredPreviewing;
|
| + BOOL _enteredCovering;
|
| + // Has the search results content been visible for the current search?
|
| + BOOL _resultsVisible;
|
| + // Has the user exited the peeking/previewing/covering state yet for the
|
| + // current search?
|
| + BOOL _exitedPeeking;
|
| + BOOL _exitedPreviewing;
|
| + BOOL _exitedCovering;
|
| + // Was the first run flow invoked during this search?
|
| + BOOL _searchInvolvedFirstRun;
|
| + // Did the first run panel become visible during this search?
|
| + BOOL _firstRunPanelBecameVisible;
|
| + // Was the search triggered by a long-press selection? Unlike other metrics-
|
| + // related flags, this is not reset when a search ends; instead it is set
|
| + // when a new search is started.
|
| + BOOL _searchTriggeredBySelection;
|
| + // Has the current search used SERP navigation (tapped on a link on the
|
| + // search results page)?
|
| + BOOL _usedSERPNavigation;
|
| + // Boolean to track if the script has been injected in the current page. This
|
| + // is a faster check than asking the JS controller.
|
| + BOOL _isScriptInjected;
|
| +
|
| + // Boolean to track if the UIMenuControllerWillShowMenuNotification is
|
| + // observed (to prevent double observation).
|
| + BOOL _observingActionMenu;
|
| + // Boolean to track if the current search is triggered by selection, and
|
| + // action menu should be disabled.
|
| + BOOL _preventActionMenu;
|
| + // Boolean to track if a new text selection has been made (as opposed to an
|
| + // existing one being changed) which will trigger the appearance of the
|
| + // panel.
|
| + BOOL _newSelectionDisplaying;
|
| +
|
| + // Boolean to temporarly disable preloading of search tab.
|
| + BOOL _preventPreload;
|
| +
|
| + // Boolean to track if the search term has been resolved.
|
| + BOOL _searchTermResolved;
|
| +
|
| + // True when closed has been called and contextual search controller
|
| + // has been destroyed.
|
| + BOOL _closed;
|
| +
|
| + // When view is resized, JavaScript and UIView sizes are not updated at the
|
| + // same time. Computing a scroll delta to make selection visible in these
|
| + // conditions will likely scroll to a random position.
|
| + BOOL _preventScrollToShowSelection;
|
| +
|
| + // The time of the last dismiss.
|
| + base::scoped_nsobject<NSDate> _lastDismiss;
|
| +}
|
| +
|
| +@synthesize enabled = _enabled;
|
| +@synthesize controllerDelegate = _controllerDelegate;
|
| +@synthesize webState = _webState;
|
| +
|
| +- (instancetype)initWithBrowserState:(ios::ChromeBrowserState*)browserState
|
| + delegate:(id<ContextualSearchControllerDelegate>)
|
| + delegate {
|
| + if ((self = [super init])) {
|
| + _permissions.reset([[TouchToSearchPermissionsMediator alloc]
|
| + initWithBrowserState:browserState]);
|
| + [_permissions setAudience:self];
|
| +
|
| + self.controllerDelegate = delegate;
|
| +
|
| + // Set up the web state observer. This lasts as long as this object does,
|
| + // but it will observe and un-observe the web tabs as it changes over time.
|
| + _webStateObserver.reset(new ContextualSearchWebStateObserver(self));
|
| +
|
| + _copyGestureRecognizer.reset([[UILongPressGestureRecognizer alloc]
|
| + initWithTarget:self
|
| + action:@selector(handleLongPressFrom:)]);
|
| +
|
| + base::WeakNSObject<ContextualSearchController> weakself(self);
|
| + auto callback = base::BindBlock(
|
| + ^(ContextualSearchDelegate::SearchResolution resolution) {
|
| + [weakself updateForResolvedSearch:resolution];
|
| + });
|
| +
|
| + _delegate.reset(new ContextualSearchDelegate(browserState, callback));
|
| + }
|
| + return self;
|
| +}
|
| +
|
| +- (TouchToSearchPermissionsMediator*)permissions {
|
| + return _permissions;
|
| +}
|
| +
|
| +- (void)setPermissions:(TouchToSearchPermissionsMediator*)permissions {
|
| + _permissions.reset(permissions);
|
| +}
|
| +
|
| +- (ContextualSearchPanelView*)panel {
|
| + return _panelView;
|
| +}
|
| +
|
| +- (void)setPanel:(ContextualSearchPanelView*)panel {
|
| + DCHECK(!_panelView);
|
| + DCHECK(panel);
|
| +
|
| + // Save the new panel, set up observation and delegation relationships.
|
| + _panelView.reset(panel);
|
| + [_panelView addMotionObserver:self];
|
| + [_dismissRecognizer setViewToExclude:_panelView];
|
| +
|
| + // Create new subviews.
|
| + NSMutableArray* panelContents = [NSMutableArray arrayWithCapacity:3];
|
| +
|
| + _headerView.reset([[ContextualSearchHeaderView alloc]
|
| + initWithHeight:[_panelView configuration].peekingHeight]);
|
| + [_headerView addGestureRecognizer:_copyGestureRecognizer];
|
| + [_headerView setTapHandler:self];
|
| +
|
| + [panelContents addObject:_headerView];
|
| +
|
| + if (self.permissions.preferenceState == TouchToSearch::UNDECIDED) {
|
| + _promoView.reset([[ContextualSearchPromoView alloc] initWithFrame:CGRectZero
|
| + delegate:self]);
|
| + [panelContents addObject:_promoView];
|
| + }
|
| +
|
| + _searchResultsView.reset(
|
| + [[ContextualSearchResultsView alloc] initWithFrame:CGRectZero]);
|
| + [_searchResultsView setPromoter:self];
|
| + [_searchResultsView setPreloadChecker:self];
|
| + [panelContents addObject:_searchResultsView];
|
| +
|
| + [_panelView addContentViews:panelContents];
|
| +}
|
| +
|
| +- (void)enableContextualSearch:(BOOL)enabled {
|
| + // Asynchronously enables contextual search, so that some preferences
|
| + // (UIAccessibilityIsVoiceOverRunning(), for example) have time to synchronize
|
| + // with their own notifications.
|
| + base::WeakNSObject<ContextualSearchController> weakSelf(self);
|
| + dispatch_async(dispatch_get_main_queue(), ^{
|
| + [weakSelf doEnableContextualSearch:enabled];
|
| + });
|
| +}
|
| +
|
| +- (void)doEnableContextualSearch:(BOOL)enabled {
|
| + enabled = enabled && [self.permissions canEnable];
|
| +
|
| + BOOL changing = _enabled != enabled;
|
| + if (changing) {
|
| + if (!enabled) {
|
| + [self dismissPane:ContextualSearch::RESET];
|
| + }
|
| + _enabled = enabled;
|
| + [self enableCurrentWebState];
|
| + }
|
| +}
|
| +
|
| +- (void)updateWebViewProxy:(id<CRWWebViewProxy>)webViewProxy {
|
| + if (_webViewProxy) {
|
| + [[_webViewProxy scrollViewProxy] removeObserver:self];
|
| + }
|
| + _webViewProxy.reset([webViewProxy retain]);
|
| + if (_webViewProxy) {
|
| + [[_webViewProxy scrollViewProxy] addObserver:self];
|
| + }
|
| +}
|
| +
|
| +- (void)setTab:(Tab*)tab {
|
| + [self setWebState:tab.webState];
|
| + [_searchResultsView setOpener:tab];
|
| +}
|
| +
|
| +- (void)setWebState:(web::WebState*)webState {
|
| + [self disconnectWebState];
|
| + if (webState) {
|
| + _contextualSearchJsManager.reset(static_cast<JsContextualSearchManager*>(
|
| + [webState->GetJSInjectionReceiver()
|
| + instanceOfClass:[JsContextualSearchManager class]]));
|
| + _webState = webState;
|
| + _webStateObserver->ObserveWebState(webState);
|
| + [self updateWebViewProxy:webState->GetWebViewProxy()];
|
| + [self enableCurrentWebState];
|
| + } else {
|
| + _webState = nullptr;
|
| + }
|
| +}
|
| +
|
| +- (void)enableCurrentWebState {
|
| + if (![self webState])
|
| + return;
|
| + if (_enabled && [self webState]->ContentIsHTML()) {
|
| + if (!_webStateEnabled) {
|
| + DOMAlteringLock::CreateForWebState([self webState]);
|
| +
|
| + base::WeakNSObject<ContextualSearchController> weakSelf(self);
|
| + auto callback =
|
| + base::BindBlock(^bool(const base::DictionaryValue& JSON,
|
| + const GURL& originURL, bool userIsInteracting) {
|
| + base::scoped_nsobject<ContextualSearchController> strongSelf(
|
| + [weakSelf retain]);
|
| + // |originURL| and |isInteracting| aren't used.
|
| + return [strongSelf handleScriptCommand:JSON];
|
| + });
|
| + [self webState]->AddScriptCommandCallback(callback, kCommandPrefix);
|
| +
|
| + // |_doubleTapRecognizer| should be added to the web view before
|
| + // |_tapRecognizer| so |_tapRecognizer| can require it to fail.
|
| + _doubleTapRecognizer.reset([[UITapGestureRecognizer alloc]
|
| + initWithTarget:self
|
| + action:@selector(ignoreTap:)]);
|
| + [_doubleTapRecognizer setDelegate:self];
|
| + [_doubleTapRecognizer setNumberOfTapsRequired:2];
|
| + [_webViewProxy addGestureRecognizer:_doubleTapRecognizer];
|
| +
|
| + _tapRecognizer.reset([[UITapGestureRecognizer alloc]
|
| + initWithTarget:self
|
| + action:@selector(handleTapFrom:)]);
|
| + [_tapRecognizer setDelegate:self];
|
| + [_webViewProxy addGestureRecognizer:_tapRecognizer];
|
| +
|
| + // Make sure that |_tapRecogngizer| doesn't fire if the web view's other
|
| + // non-single-finger non-single-tap recognizers fire.
|
| + for (UIGestureRecognizer* recognizer in
|
| + [[_tapRecognizer view] gestureRecognizers]) {
|
| + if ([recognizer isKindOfClass:[UITapGestureRecognizer class]] &&
|
| + ([static_cast<UITapGestureRecognizer*>(recognizer)
|
| + numberOfTapsRequired] > 1 ||
|
| + [static_cast<UITapGestureRecognizer*>(recognizer)
|
| + numberOfTouchesRequired] > 1)) {
|
| + [_tapRecognizer requireGestureRecognizerToFail:recognizer];
|
| + }
|
| + }
|
| + _webStateEnabled = YES;
|
| + }
|
| +
|
| + [self initializeWebViewForContextualSearch];
|
| + } else {
|
| + [self disableCurrentWebState];
|
| + }
|
| +}
|
| +
|
| +- (void)disableCurrentWebState {
|
| + if (_webStateEnabled) {
|
| + if ([self webState]->ContentIsHTML()) {
|
| + [self highlightRects:nil];
|
| + [_contextualHighlightView removeFromSuperview];
|
| + [_contextualSearchJsManager clearHighlight];
|
| + [_contextualSearchJsManager disableListeners];
|
| + }
|
| + _webState->RemoveScriptCommandCallback(kCommandPrefix);
|
| + DOMAlteringLock::FromWebState(_webState)->Release(self);
|
| + [_webViewProxy removeGestureRecognizer:_tapRecognizer];
|
| + [_webViewProxy removeGestureRecognizer:_doubleTapRecognizer];
|
| + _webStateEnabled = NO;
|
| + }
|
| +}
|
| +
|
| +- (void)disconnectWebState {
|
| + if (_webState) {
|
| + _contextualSearchJsManager.reset();
|
| + _webStateObserver->ObserveWebState(nullptr);
|
| + [self updateWebViewProxy:nil];
|
| + [self disableCurrentWebState];
|
| + }
|
| +}
|
| +
|
| +- (void)updateDismissRecognizer {
|
| + if (!_panelView)
|
| + return;
|
| + if (!_dismissRecognizer) {
|
| + _dismissRecognizer.reset([[WindowGestureObserver alloc]
|
| + initWithTarget:self
|
| + action:@selector(handleWindowGesture:)]);
|
| + [_dismissRecognizer setViewToExclude:_panelView];
|
| + [[_panelView window] addGestureRecognizer:_dismissRecognizer];
|
| + }
|
| +
|
| + [_dismissRecognizer
|
| + setEnabled:[_panelView state] >= ContextualSearch::PEEKING];
|
| +}
|
| +
|
| +- (void)showLearnMore {
|
| + [self dismissPane:ContextualSearch::UNKNOWN];
|
| + GURL learnMoreUrl = google_util::AppendGoogleLocaleParam(
|
| + GURL(l10n_util::GetStringUTF8(IDS_IOS_CONTEXTUAL_SEARCH_LEARN_MORE_URL)),
|
| + GetApplicationContext()->GetApplicationLocale());
|
| + [_controllerDelegate createTabFromContextualSearchController:learnMoreUrl];
|
| +}
|
| +
|
| +- (void)dealloc {
|
| + [self close];
|
| + [super dealloc];
|
| +}
|
| +
|
| +- (void)handleWindowGesture:(UIGestureRecognizer*)recognizer {
|
| + DCHECK(recognizer == _dismissRecognizer.get());
|
| + [self dismissPane:ContextualSearch::BASE_PAGE_TAP];
|
| +}
|
| +
|
| +- (BOOL)canExtractTapContext {
|
| + web::URLVerificationTrustLevel trustLevel = web::kNone;
|
| + GURL pageURL = [self webState]->GetCurrentURL(&trustLevel);
|
| + return [self.permissions canExtractTapContextForURL:pageURL];
|
| +}
|
| +
|
| +- (void)initializeWebViewForContextualSearch {
|
| + DCHECK(_webStateEnabled);
|
| + [_contextualSearchJsManager inject];
|
| + _isScriptInjected = YES;
|
| + [_contextualSearchJsManager
|
| + enableEventListenersWithMutationDelay:
|
| + kDOMModificationDelayForJavaScriptMilliseconds
|
| + bodyTouchDelay:
|
| + kBodyTouchDelayForJavaScriptMilliseconds];
|
| +}
|
| +
|
| +- (void)handleSelectionChanged:(const std::string&)selection
|
| + selectionUpdated:(BOOL)updated
|
| + selectionValid:(BOOL)selectionValid {
|
| + if (!selectionValid) {
|
| + [self dismissPane:ContextualSearch::INVALID_SELECTION];
|
| + return;
|
| + }
|
| + std::string selectedText = CleanStringForDisplay(selection, true);
|
| +
|
| + if (selectedText == _selectedText)
|
| + return;
|
| + _newSelectionDisplaying = !updated && !selectedText.empty();
|
| + _selectedText = selectedText;
|
| + _searchContext.reset();
|
| + [self highlightRects:nil];
|
| + [_contextualSearchJsManager clearHighlight];
|
| + _delegate->CancelSearchTermRequest();
|
| +
|
| + if (selectedText.length() == 0) {
|
| + if (_webViewTappedWithSelection) {
|
| + [self dismissPane:ContextualSearch::BASE_PAGE_TAP];
|
| + }
|
| + } else {
|
| + // TODO(crbug.com/546220): Detect and use actual page encoding.
|
| + std::string encoding = "UTF-8";
|
| +
|
| + _searchContext.reset(
|
| + new ContextualSearchContext(selectedText, true, GURL(), encoding));
|
| + _searchTriggeredBySelection = YES;
|
| +
|
| + _preventPreload = YES;
|
| + _delegate->PostSearchTermRequest(_searchContext);
|
| +
|
| + ContextualSearch::RecordSelectionIsValid(true);
|
| + _preventActionMenu = YES;
|
| + if (!_observingActionMenu) {
|
| + _observingActionMenu = YES;
|
| + [[NSNotificationCenter defaultCenter]
|
| + addObserver:self
|
| + selector:@selector(willShowMenuNotification)
|
| + name:UIMenuControllerWillShowMenuNotification
|
| + object:nil];
|
| + }
|
| + [self peekPane:ContextualSearch::TEXT_SELECT_LONG_PRESS];
|
| + [_headerView
|
| + setSearchTerm:base::SysUTF8ToNSString(selectedText)
|
| + animated:[_panelView state] != ContextualSearch::DISMISSED];
|
| + }
|
| + _webViewTappedWithSelection = NO;
|
| +}
|
| +
|
| +- (BOOL)handleScriptCommand:(const base::DictionaryValue&)JSONCommand {
|
| + std::string command;
|
| + if (!JSONCommand.GetString("command", &command))
|
| + return NO;
|
| + if (command == "contextualSearch.selectionChanged") {
|
| + std::string selectedText;
|
| + if (!JSONCommand.GetString("text", &selectedText))
|
| + return NO;
|
| + bool selectionUpdated;
|
| + if (!JSONCommand.GetBoolean("updated", &selectionUpdated))
|
| + selectionUpdated = false;
|
| + bool selectionValid;
|
| + if (!JSONCommand.GetBoolean("valid", &selectionValid))
|
| + selectionValid = true;
|
| + base::WeakNSObject<ContextualSearchController> weakSelf(self);
|
| + ProceduralBlockWithBool lockAction = ^(BOOL lockAcquired) {
|
| + if (lockAcquired) {
|
| + [weakSelf handleSelectionChanged:selectedText
|
| + selectionUpdated:selectionUpdated
|
| + selectionValid:selectionValid];
|
| + }
|
| + };
|
| + DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction);
|
| + return YES;
|
| + }
|
| + if (command == "contextualSearch.mutationEvent") {
|
| + if ([_panelView state] <= ContextualSearch::PEEKING &&
|
| + !_searchTermResolved) {
|
| + [self dismissPane:ContextualSearch::UNKNOWN];
|
| + }
|
| + return YES;
|
| + }
|
| + return NO;
|
| +}
|
| +
|
| +- (void)ignoreTap:(UIGestureRecognizer*)recognizer {
|
| + // This method is intentionally empty. It is intended to ignore the tap.
|
| +}
|
| +
|
| +- (void)handleTapFrom:(UIGestureRecognizer*)recognizer {
|
| + DCHECK(recognizer == _tapRecognizer.get());
|
| + // Taps will be triggered by long-presses to make a selection in the webview,
|
| + // as well as 'regular' taps. Long-presses that create a selection will set
|
| + // |_newSelectionDisplaying| as well as populating _selectedText (this happens
|
| + // in -handleScriptCommand:).
|
| +
|
| + // If we just dismissed, do not consider this tap.
|
| + NSTimeInterval dismissTimeout = [_lastDismiss timeIntervalSinceNow] +
|
| + kPreventTriggerAfterDismissDelaySeconds;
|
| +
|
| + // If the panel is already displayed, just dismiss it and return, unless the
|
| + // tap was from displaying a new selection.
|
| + if (([_panelView state] != ContextualSearch::DISMISSED &&
|
| + !_newSelectionDisplaying) ||
|
| + dismissTimeout > 0) {
|
| + [self dismissPane:ContextualSearch::BASE_PAGE_TAP];
|
| + return;
|
| + }
|
| + // Otherwise handle the tap.
|
| + [_tapRecognizer setEnabled:NO];
|
| + _currentTapCancelled = NO;
|
| + _newSelectionDisplaying = NO;
|
| + ProceduralBlockWithBool lockAction = ^(BOOL lockAcquired) {
|
| + if (!lockAcquired || !_isScriptInjected || _currentTapCancelled ||
|
| + [recognizer state] != UIGestureRecognizerStateEnded ||
|
| + !_selectedText.empty()) {
|
| + [_tapRecognizer setEnabled:YES];
|
| + if (!_selectedText.empty())
|
| + _webViewTappedWithSelection = YES;
|
| + return;
|
| + }
|
| +
|
| + CGPoint tapPoint = [recognizer locationInView:recognizer.view];
|
| + // tapPoint is the coordinate of the tap in the webView. If the view is
|
| + // currently offset because a header is displayed, offset the tapPoint.
|
| + tapPoint.y -= [_controllerDelegate currentHeaderHeight];
|
| +
|
| + // Handle tap asynchronously to monitor DOM modifications. See comment
|
| + // of |kDOMModificationDelaySeconds| for details.
|
| + dispatch_time_t dispatch = dispatch_time(
|
| + DISPATCH_TIME_NOW,
|
| + static_cast<int64_t>(kDOMModificationDelaySeconds * NSEC_PER_SEC));
|
| + base::WeakNSObject<ContextualSearchController> weakSelf(self);
|
| + dispatch_after(dispatch, dispatch_get_main_queue(), ^{
|
| + [weakSelf handleTapAtPoint:tapPoint];
|
| + });
|
| + };
|
| + DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction);
|
| +}
|
| +
|
| +- (void)handleLongPressFrom:(UIGestureRecognizer*)recognizer {
|
| + DCHECK(recognizer == _copyGestureRecognizer.get());
|
| + if (recognizer.state != UIGestureRecognizerStateEnded)
|
| + return;
|
| +
|
| + // Put the resolved search term (or the current selected text) into the
|
| + // pasteboard.
|
| + std::string text;
|
| + if (!_resolvedSearch.display_text.empty()) {
|
| + text = _resolvedSearch.display_text;
|
| + }
|
| +
|
| + if (!text.empty()) {
|
| + UIPasteboard* pasteboard = [UIPasteboard generalPasteboard];
|
| + pasteboard.string = base::SysUTF8ToNSString(_resolvedSearch.display_text);
|
| + // Let the user know.
|
| + NSString* messageText = l10n_util::GetNSString(IDS_IOS_SEARCH_COPIED);
|
| + MDCSnackbarMessage* message =
|
| + [MDCSnackbarMessage messageWithText:messageText];
|
| + message.duration = 1.0;
|
| + message.category = @"search term copied";
|
| + [MDCSnackbarManager showMessage:message];
|
| + }
|
| +}
|
| +
|
| +- (void)handleTapAtPoint:(CGPoint)point {
|
| + _tapTime = base::Time::Now();
|
| + if (_currentTapCancelled) {
|
| + [_tapRecognizer setEnabled:YES];
|
| + return;
|
| + }
|
| +
|
| + _searchTriggeredBySelection = NO;
|
| +
|
| + // TODO(crbug.com/546220): Detect and use actual page encoding.
|
| + std::string encoding = "UTF-8";
|
| +
|
| + CGPoint relativeTapPoint = point;
|
| + CGSize contentSize = [_webViewProxy scrollViewProxy].contentSize;
|
| + relativeTapPoint.x += [_webViewProxy scrollViewProxy].contentOffset.x;
|
| + relativeTapPoint.y += [_webViewProxy scrollViewProxy].contentOffset.y;
|
| +
|
| + relativeTapPoint.x /= contentSize.width;
|
| + relativeTapPoint.y /= contentSize.height;
|
| +
|
| + base::WeakNSProtocol<id<CRWWebViewProxy>> weakWebViewProxy(
|
| + _webViewProxy.get());
|
| + void (^handler)(NSString*) = ^(NSString* result) {
|
| + [_tapRecognizer setEnabled:YES];
|
| + // If there has been an error in the javascript, return can be nil.
|
| + if (!result || _currentTapCancelled)
|
| + return;
|
| +
|
| + // Parse JSON.
|
| + const std::string json = base::SysNSStringToUTF8(result);
|
| + std::unique_ptr<base::Value> parsedResult(
|
| + base::JSONReader::Read(json, false));
|
| + if (!parsedResult.get() ||
|
| + !parsedResult->IsType(base::Value::Type::DICTIONARY)) {
|
| + return;
|
| + }
|
| +
|
| + base::DictionaryValue* resultDict =
|
| + static_cast<base::DictionaryValue*>(parsedResult.get());
|
| + const base::DictionaryValue* context = nullptr;
|
| + BOOL contextError = NO;
|
| + if (!resultDict->GetDictionary("context", &context)) {
|
| + // No context returned -- the tap wasn't on a word.
|
| + DVLOG(1) << "Contextual search results did not include a context.";
|
| + contextError = YES;
|
| + } else {
|
| + std::string error;
|
| + context->GetString("error", &error);
|
| + if (!error.empty()) {
|
| + // Something went wrong!
|
| + DVLOG(0) << "Contextual search error: " << error;
|
| + contextError = YES;
|
| + }
|
| + }
|
| +
|
| + if (contextError) {
|
| + _searchContext.reset();
|
| + [self updateUI];
|
| + // The JavaScript will have taken care of clearing the highlighting.
|
| + return;
|
| + }
|
| +
|
| + // Marshall the retrieved context.
|
| + std::string url, selectedText;
|
| + BOOL marshallingOK = YES;
|
| + GURL sentUrl;
|
| + if ([self.permissions canSendPageURLs]) {
|
| + marshallingOK = marshallingOK && context->GetString("url", &url);
|
| + sentUrl = GURL(url);
|
| + }
|
| + marshallingOK =
|
| + marshallingOK && context->GetString("selectedText", &selectedText);
|
| +
|
| + if (!marshallingOK) {
|
| + _searchContext.reset();
|
| + [self updateUI];
|
| + // The JavaScript will have taken care of clearing the highlighting.
|
| + return;
|
| + }
|
| + _searchContext.reset(
|
| + new ContextualSearchContext(selectedText, true, sentUrl, encoding));
|
| +
|
| + if ([self canExtractTapContext]) {
|
| + marshallingOK =
|
| + marshallingOK &&
|
| + context->GetString("surroundingText",
|
| + &_searchContext->surrounding_text) &&
|
| + context->GetInteger("offsetStart", &_searchContext->start_offset) &&
|
| + context->GetInteger("offsetEnd", &_searchContext->end_offset);
|
| + }
|
| +
|
| + if (!marshallingOK) {
|
| + _searchContext.reset();
|
| + [self updateUI];
|
| + // The JavaScript will have taken care of clearing the highlighting.
|
| + return;
|
| + }
|
| +
|
| + DVLOG(1) << "Contextual search results:\n"
|
| + << " URL: " << _searchContext->page_url.spec() << "\n"
|
| + << " selectedText: " << _searchContext->selected_text << "\n"
|
| + << " offsets: " << _searchContext->start_offset << "-"
|
| + << _searchContext->end_offset << "\n"
|
| + << " surroundingText: " << _searchContext->surrounding_text;
|
| +
|
| + std::string rects;
|
| + if (!context->GetString("rects", &rects)) {
|
| + _searchContext.reset();
|
| + [self updateUI];
|
| + return;
|
| + }
|
| + NSArray* rectsArray = StringValueToRectArray(rects);
|
| + if (!rectsArray) {
|
| + _searchContext.reset();
|
| + [self updateUI];
|
| + return;
|
| + }
|
| + [self highlightRects:rectsArray];
|
| +
|
| + [self scrollToShowSelection:[weakWebViewProxy scrollViewProxy]];
|
| +
|
| + // Update the content view and the state of the UI.
|
| + [self updateUI];
|
| + _preventPreload = NO;
|
| +
|
| + _delegate->PostSearchTermRequest(_searchContext);
|
| + _searchTriggeredBySelection = NO;
|
| + };
|
| + [_contextualSearchJsManager fetchContextFromSelectionAtPoint:relativeTapPoint
|
| + completionHandler:handler];
|
| +}
|
| +
|
| +- (void)handleHighlightJSResult:(id)result withError:(NSError*)error {
|
| + if (error) {
|
| + [self highlightRects:nil];
|
| + [_contextualSearchJsManager clearHighlight];
|
| + return;
|
| + }
|
| + std::string JSON(
|
| + base::SysNSStringToUTF8(base::mac::ObjCCastStrict<NSString>(result)));
|
| + // |json| is a JSON dicionary containing at list 2 entries:
|
| + // - 'rects': containing a list of rect dictionaries representing the zone of
|
| + // the page to highlight as a string in the format
|
| + // top1 bottom1 left1 right1,top2 bottom2 left2 right2,...,
|
| + // - 'size': containing a dictionary containing the size of the document as
|
| + // seen in JavaScript.
|
| + // As the 'rects' coordinates are based on a document which size is contained
|
| + // in 'size', if the web content view does not have the same size, they should
|
| + // not be considered.
|
| +
|
| + std::unique_ptr<base::Value> parsedResult(
|
| + base::JSONReader::Read(JSON, false));
|
| + if (!parsedResult.get() ||
|
| + !parsedResult->IsType(base::Value::Type::DICTIONARY)) {
|
| + return;
|
| + }
|
| + base::DictionaryValue* resultDict =
|
| + static_cast<base::DictionaryValue*>(parsedResult.get());
|
| +
|
| + CGSize contentSize = [_webViewProxy scrollViewProxy].contentSize;
|
| + const base::DictionaryValue* contentSizeDict;
|
| + if (resultDict->GetDictionary("size", &contentSizeDict)) {
|
| + double width, height;
|
| + if (!contentSizeDict->GetDouble("height", &height) ||
|
| + !contentSizeDict->GetDouble("width", &width)) {
|
| + // Value is not correctly formatted. Early return.
|
| + return;
|
| + }
|
| + width *= [_webViewProxy scrollViewProxy].zoomScale;
|
| + height *= [_webViewProxy scrollViewProxy].zoomScale;
|
| + if (fabsl(contentSize.width - width) > 2 ||
|
| + fabsl(contentSize.height - height) > 2) {
|
| + // The coords in of the UIView and in JavaScript are not synced. A scroll
|
| + // now would be almost random.
|
| + _preventScrollToShowSelection = YES;
|
| + dispatch_after(
|
| + dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.1 * NSEC_PER_SEC)),
|
| + dispatch_get_main_queue(), ^{
|
| + [self updateHighlight];
|
| + });
|
| + return;
|
| + }
|
| + _preventScrollToShowSelection = NO;
|
| + }
|
| +
|
| + std::string rectsList;
|
| + if (resultDict->GetString("rects", &rectsList)) {
|
| + NSArray* rects = StringValueToRectArray(rectsList);
|
| + if (rects) {
|
| + [self highlightRects:rects];
|
| + [self scrollToShowSelection:[_webViewProxy scrollViewProxy]];
|
| + }
|
| + }
|
| +}
|
| +
|
| +- (void)updateForResolvedSearch:
|
| + (ContextualSearchDelegate::SearchResolution)resolution {
|
| + _resolvedSearch = resolution;
|
| +
|
| + DVLOG(1) << "is invalid: " << _resolvedSearch.is_invalid << "\n"
|
| + << "response code: " << _resolvedSearch.response_code << "\n"
|
| + << "search term: " << _resolvedSearch.search_term << "\n"
|
| + << "search term: " << _resolvedSearch.alternate_term << "\n"
|
| + << "display text: " << _resolvedSearch.display_text << "\n"
|
| + << "stop preload: " << _resolvedSearch.prevent_preload;
|
| +
|
| + if (_resolvedSearch.is_invalid) {
|
| + [self dismissPane:ContextualSearch::UNKNOWN];
|
| + } else {
|
| + _searchTermResolved = YES;
|
| + [_headerView
|
| + setSearchTerm:base::SysUTF8ToNSString(_resolvedSearch.display_text)
|
| + animated:[_panelView state] != ContextualSearch::DISMISSED];
|
| + if (_resolvedSearch.start_offset != -1 &&
|
| + _resolvedSearch.end_offset != -1) {
|
| + base::WeakNSObject<ContextualSearchController> weakSelf(self);
|
| + [_contextualSearchJsManager
|
| + expandHighlightToStartOffset:_resolvedSearch.start_offset
|
| + endOffset:_resolvedSearch.end_offset
|
| + completionHandler:^(id result, NSError* error) {
|
| + [weakSelf handleHighlightJSResult:result
|
| + withError:error];
|
| + }];
|
| + }
|
| + GURL url = _delegate->GetURLForResolvedSearch(_resolvedSearch, true);
|
| + [_searchResultsView createTabForSearch:url
|
| + preloadEnabled:!_resolvedSearch.prevent_preload];
|
| + // Record the tap-to-search interval.
|
| + ContextualSearch::RecordTimeToSearch(base::Time::Now() - _tapTime);
|
| + }
|
| +}
|
| +
|
| +- (void)updateUI {
|
| + if (_searchContext) {
|
| + ContextualSearch::RecordSelectionIsValid(true);
|
| + [self peekPane:ContextualSearch::TEXT_SELECT_TAP];
|
| + _searchInvolvedFirstRun =
|
| + self.permissions.preferenceState == TouchToSearch::UNDECIDED;
|
| +
|
| + if (_searchContext->surrounding_text.empty()) {
|
| + [_headerView
|
| + setSearchTerm:base::SysUTF8ToNSString(_searchContext->selected_text)
|
| + animated:[_panelView state] != ContextualSearch::DISMISSED];
|
| + } else {
|
| + NSString* surroundingText =
|
| + base::SysUTF16ToNSString(_searchContext->surrounding_text);
|
| + NSInteger startOffset =
|
| + CLAMP(0, _searchContext->start_offset,
|
| + static_cast<NSInteger>([surroundingText length]));
|
| + NSString* displayedText =
|
| + [surroundingText substringFromIndex:startOffset];
|
| + NSInteger adjusted_offset =
|
| + _searchContext->end_offset - _searchContext->start_offset;
|
| + NSInteger followingOffset = CLAMP(
|
| + 0, adjusted_offset, static_cast<NSInteger>([surroundingText length]));
|
| + NSRange followingTextRange =
|
| + NSMakeRange(followingOffset, displayedText.length - followingOffset);
|
| + [_headerView setText:displayedText
|
| + followingTextRange:followingTextRange
|
| + animated:[_panelView state] != ContextualSearch::DISMISSED];
|
| + }
|
| + } else {
|
| + ContextualSearch::RecordSelectionIsValid(false);
|
| + [self dismissPane:ContextualSearch::INVALID_SELECTION];
|
| + }
|
| +}
|
| +- (void)scrollToShowSelection:(CRWWebViewScrollViewProxy*)scrollView {
|
| + if (!scrollView || _preventScrollToShowSelection)
|
| + return;
|
| + if (!_contextualHighlightView.get()) {
|
| + return;
|
| + }
|
| + CGRect highlightBoundingRect = [_contextualHighlightView boundingRect];
|
| + if (CGRectIsNull(highlightBoundingRect)) {
|
| + return;
|
| + }
|
| +
|
| + // Do the maths without the insets.
|
| + CGPoint scrollPoint = [scrollView contentOffset];
|
| + scrollPoint.y += scrollView.contentInset.top;
|
| + scrollPoint.x += scrollView.contentInset.left;
|
| +
|
| + // Coordinates of the bounding box to show.
|
| + CGFloat top = CGRectGetMinY(highlightBoundingRect);
|
| + CGFloat bottom = CGRectGetMaxY(highlightBoundingRect);
|
| + CGFloat left = CGRectGetMinX(highlightBoundingRect);
|
| + CGFloat right = CGRectGetMaxX(highlightBoundingRect);
|
| +
|
| + CGSize displaySize = [_contextualHighlightView frame].size;
|
| +
|
| + CGFloat panelHeight = CGRectGetHeight(
|
| + CGRectIntersection([_panelView frame], [_panelView superview].bounds));
|
| +
|
| + displaySize.height -= scrollView.contentInset.top +
|
| + scrollView.contentInset.bottom + panelHeight;
|
| + displaySize.width -=
|
| + scrollView.contentInset.left + scrollView.contentInset.right;
|
| +
|
| + // Coordinates of the displayed frame in the same coordinates system.
|
| + CGFloat frameTop = scrollPoint.y;
|
| + CGFloat frameBottom = frameTop + displaySize.height;
|
| + CGFloat frameLeft = scrollPoint.x;
|
| + CGFloat frameRight = frameLeft + displaySize.width;
|
| +
|
| + CGSize contentSize = scrollView.contentSize;
|
| + CGFloat maxOffsetY = MAX(contentSize.height - displaySize.height, 0);
|
| + CGFloat maxOffsetX = MAX(contentSize.width - displaySize.width, 0);
|
| +
|
| + if (highlightBoundingRect.size.width + 2 * kXScrollMargin >
|
| + displaySize.width) {
|
| + // Selection does not fit in the screen. Center horizontal scroll.
|
| + if (contentSize.width > displaySize.width) {
|
| + scrollPoint.x = (left + right - displaySize.width) / 2;
|
| + }
|
| + } else {
|
| + // Make sure right is visible.
|
| + if (right + kXScrollMargin > frameRight) {
|
| + scrollPoint.x = right + kXScrollMargin - displaySize.width;
|
| + }
|
| +
|
| + // Make sure left is visible.
|
| + if (left - kXScrollMargin < frameLeft) {
|
| + scrollPoint.x = left - kXScrollMargin;
|
| + }
|
| + }
|
| +
|
| + // Make sure bottom is visible.
|
| + if (bottom + kYScrollMargin > frameBottom) {
|
| + scrollPoint.y = bottom + kYScrollMargin - displaySize.height;
|
| + }
|
| +
|
| + // Make sure top is visible.
|
| + if (top - kYScrollMargin - [_controllerDelegate currentHeaderHeight] <
|
| + frameTop) {
|
| + scrollPoint.y =
|
| + top - kYScrollMargin - [_controllerDelegate currentHeaderHeight];
|
| + }
|
| +
|
| + if (scrollPoint.x < 0)
|
| + scrollPoint.x = 0;
|
| + if (scrollPoint.x > maxOffsetX) {
|
| + scrollPoint.x = maxOffsetX;
|
| + }
|
| + if (scrollPoint.y < 0)
|
| + scrollPoint.y = 0;
|
| + if (scrollPoint.y > maxOffsetY)
|
| + scrollPoint.y = maxOffsetY;
|
| +
|
| + scrollPoint.y -= scrollView.contentInset.top;
|
| + scrollPoint.x -= scrollView.contentInset.left;
|
| + [scrollView setContentOffset:scrollPoint animated:YES];
|
| +}
|
| +
|
| +- (void)highlightRects:(NSArray*)rects {
|
| + if (![self webState]) {
|
| + return;
|
| + }
|
| + if (!_contextualHighlightView.get() && [rects count]) {
|
| + CGRect frame = [[self webState]->GetWebViewProxy() frame];
|
| + ContextualSearchHighlighterView* highlightView =
|
| + [[[ContextualSearchHighlighterView alloc] initWithFrame:frame
|
| + delegate:self]
|
| + autorelease];
|
| + _contextualHighlightView.reset(highlightView);
|
| + [[self webState]->GetWebViewProxy() addSubview:highlightView];
|
| + }
|
| + CGPoint scroll = [[_webViewProxy scrollViewProxy] contentOffset];
|
| + [_contextualHighlightView
|
| + highlightRects:rects
|
| + withOffset:[_controllerDelegate currentHeaderHeight]
|
| + zoom:[[_webViewProxy scrollViewProxy] zoomScale]
|
| + scroll:scroll];
|
| +}
|
| +
|
| +- (void)willShowMenuNotification {
|
| + if (!_preventActionMenu)
|
| + return;
|
| + BOOL dismiss = NO;
|
| + if ([_panelView state] > ContextualSearch::PEEKING) {
|
| + dismiss = YES;
|
| + }
|
| + if ([_panelView state] == ContextualSearch::PEEKING) {
|
| + CGPoint headerTop = [_headerView convertPoint:CGPointZero toView:nil];
|
| + CGRect menuRect = [[UIMenuController sharedMenuController] menuFrame];
|
| + if (headerTop.y < CGRectGetMaxY(menuRect)) {
|
| + dismiss = YES;
|
| + }
|
| + }
|
| + if (dismiss) {
|
| + dispatch_async(dispatch_get_main_queue(), ^{
|
| + [[UIMenuController sharedMenuController] setMenuVisible:NO];
|
| + });
|
| + }
|
| +}
|
| +
|
| +- (void)close {
|
| + if (_closed)
|
| + return;
|
| +
|
| + _closed = YES;
|
| + [self disableCurrentWebState];
|
| + [self setWebState:nil];
|
| + [_headerView removeGestureRecognizer:_copyGestureRecognizer];
|
| + [[_panelView window] removeGestureRecognizer:_dismissRecognizer];
|
| + _delegate.reset();
|
| + [_searchResultsView setActive:NO];
|
| + _searchResultsView.reset();
|
| +}
|
| +
|
| +#pragma mark - Promo view management
|
| +
|
| +- (void)userOptedInFromPromo:(BOOL)optIn {
|
| + if (optIn) {
|
| + self.permissions.preferenceState = TouchToSearch::ENABLED;
|
| + [_promoView closeAnimated:YES];
|
| + [_promoView setDisabled:YES];
|
| + } else {
|
| + [self dismissPane:ContextualSearch::OPTOUT];
|
| + self.permissions.preferenceState = TouchToSearch::DISABLED;
|
| + }
|
| + ContextualSearch::RecordFirstRunFlowOutcome(self.permissions.preferenceState);
|
| +}
|
| +
|
| +#pragma mark - ContextualSearchPreloadChecker
|
| +
|
| +- (BOOL)canPreloadSearchResults {
|
| + if (_preventPreload) {
|
| + return NO;
|
| + }
|
| + return [self.permissions canPreloadSearchResults];
|
| +}
|
| +
|
| +#pragma mark - ContextualSearchTabPromoter
|
| +
|
| +- (void)promoteTabHeaderPressed:(BOOL)headerPressed {
|
| + // Move the panel so it's covering before the transition.
|
| + if ([_panelView state] != ContextualSearch::COVERING) {
|
| + [self coverPane:ContextualSearch::SERP_NAVIGATION];
|
| + }
|
| + // TODO(crbug.com/455334): Make this transition look nicer.
|
| + [_searchResultsView scrollToTopAnimated:YES];
|
| +
|
| + [self cleanUpWebStateForDismissWithCompletion:nil];
|
| +
|
| + // Tell the BVC to handle the promotion, which will cause a new panel view
|
| + // to be created.
|
| + [_controllerDelegate promotePanelToTabProvidedBy:_searchResultsView
|
| + focusInput:NO];
|
| +}
|
| +
|
| +#pragma mark - ContextualSearchPanelMotionObserver
|
| +
|
| +- (void)panel:(ContextualSearchPanelView*)panel
|
| + didStopMovingWithMotion:(ContextualSearch::PanelMotion)motion {
|
| + if (motion.state == ContextualSearch::DISMISSED) {
|
| + [self dismissPane:ContextualSearch::SWIPE];
|
| + } else if (motion.state == ContextualSearch::PEEKING) {
|
| + // newOrigin is above peeking height but below preview height.
|
| + if ([_panelView state] >= ContextualSearch::PREVIEWING) {
|
| + // Dragged down from previewing or covering
|
| + [self peekPane:ContextualSearch::SWIPE];
|
| + } else {
|
| + // Dragged up or stayed the same.
|
| + [self previewPane:ContextualSearch::SWIPE];
|
| + }
|
| + } else {
|
| + if ([_panelView state] == ContextualSearch::COVERING) {
|
| + if (motion.state != ContextualSearch::COVERING) {
|
| + // Dragged down from covering.
|
| + [self previewPane:ContextualSearch::SWIPE];
|
| + }
|
| + } else {
|
| + // Dragged up.
|
| + [self coverPane:ContextualSearch::SWIPE];
|
| + }
|
| + }
|
| + [self updateHighlight];
|
| +}
|
| +
|
| +- (void)panelWillPromote:(ContextualSearchPanelView*)panel {
|
| + DCHECK(panel == _panelView);
|
| + [panel removeMotionObserver:self];
|
| + _panelView.reset();
|
| + [self setState:ContextualSearch::DISMISSED
|
| + reason:ContextualSearch::TAB_PROMOTION];
|
| +}
|
| +
|
| +#pragma mark - ContextualSearchPanelTapHandler
|
| +
|
| +- (void)panelWasTapped:(UIGestureRecognizer*)gesture {
|
| + // Tapping when peeking switches to previewing.
|
| + // Tapping otherwise turns the panel into a tab.
|
| + if ([_panelView state] == ContextualSearch::PEEKING) {
|
| + [self previewPane:ContextualSearch::SEARCH_BAR_TAP];
|
| + } else {
|
| + [self promoteTabHeaderPressed:YES];
|
| + }
|
| +}
|
| +
|
| +- (void)closePanel {
|
| + [self dismissPane:ContextualSearch::SEARCH_BAR_TAP];
|
| +}
|
| +
|
| +#pragma mark - State change methods
|
| +
|
| +- (void)setState:(ContextualSearch::PanelState)state
|
| + reason:(ContextualSearch::StateChangeReason)reason {
|
| + ContextualSearch::PanelState fromState = [_panelView state];
|
| +
|
| + // If we're moving to PEEKING as a result of text selection, that's starting
|
| + // a new search.
|
| + BOOL startingSearch = state == ContextualSearch::PEEKING &&
|
| + (reason == ContextualSearch::TEXT_SELECT_TAP ||
|
| + reason == ContextualSearch::TEXT_SELECT_LONG_PRESS);
|
| + // If we're showing anything, then there's an ongoing search.
|
| + BOOL ongoingSearch = fromState > ContextualSearch::DISMISSED;
|
| + // If there's an ongoing search and we're dismissing or starting a search,
|
| + // then we're ending a search.
|
| + BOOL endingSearch =
|
| + ongoingSearch && (state == ContextualSearch::DISMISSED || startingSearch);
|
| + // If we're starting a search while there's one already there, it's chained.
|
| + BOOL chained = startingSearch && endingSearch;
|
| +
|
| + BOOL sameState = fromState == state;
|
| + BOOL firstExitFromPeeking = fromState == ContextualSearch::PEEKING &&
|
| + !_exitedPeeking && (!sameState || startingSearch);
|
| + BOOL firstExitFromPreviewing = fromState == ContextualSearch::PREVIEWING &&
|
| + !_exitedPreviewing && !sameState;
|
| + BOOL firstExitFromCovering =
|
| + fromState == ContextualSearch::COVERING && !_exitedCovering && !sameState;
|
| +
|
| + _resultsVisible = _resultsVisible || [_searchResultsView contentVisible];
|
| +
|
| + if (endingSearch) {
|
| + if (_searchInvolvedFirstRun) {
|
| + // If the first run panel might have been shown, did the user see it?
|
| + ContextualSearch::RecordFirstRunPanelSeen(_firstRunPanelBecameVisible);
|
| + }
|
| + // Record search timing.
|
| + [_searchResultsView recordFinishedSearchChained:chained];
|
| + // Record if the user saw the search results.
|
| + if (_searchTriggeredBySelection) {
|
| + ContextualSearch::RecordSelectionResultsSeen(_resultsVisible);
|
| + } else {
|
| + ContextualSearch::RecordTapResultsSeen(_resultsVisible);
|
| + }
|
| + }
|
| +
|
| + // Log state change. We only log the first transition to a state within a
|
| + // contextual search. Note that when a user clicks on a link on the search
|
| + // content view, this will trigger a transition to COVERING (SERP_NAVIGATION)
|
| + // followed by a transition to DISMISSED (TAB_PROMOTION). For the purpose of
|
| + // logging, the reason for the second transition is reinterpreted to
|
| + // SERP_NAVIGATION, in order to distinguish it from a tab promotion caused
|
| + // when tapping on the header when the panel is maximized.
|
| + ContextualSearch::StateChangeReason loggedReason =
|
| + _usedSERPNavigation ? ContextualSearch::SERP_NAVIGATION : reason;
|
| + if (startingSearch || endingSearch ||
|
| + (!sameState && !_enteredPreviewing &&
|
| + state == ContextualSearch::PREVIEWING) ||
|
| + (!sameState && !_enteredCovering &&
|
| + state == ContextualSearch::COVERING)) {
|
| + ContextualSearch::RecordFirstStateEntry(fromState, state, loggedReason);
|
| + }
|
| + if ((startingSearch && !chained) || firstExitFromPeeking ||
|
| + firstExitFromPreviewing || firstExitFromCovering) {
|
| + ContextualSearch::RecordFirstStateExit(fromState, state, loggedReason);
|
| + }
|
| +
|
| + if (firstExitFromPeeking) {
|
| + _exitedPeeking = YES;
|
| + } else if (firstExitFromPreviewing) {
|
| + _exitedPreviewing = YES;
|
| + } else if (firstExitFromCovering) {
|
| + _exitedCovering = YES;
|
| + }
|
| +
|
| + [_panelView setState:state];
|
| + // If the panel is now visible, enable the window-tap detector.
|
| +
|
| + [self updateDismissRecognizer];
|
| +
|
| + if (state == ContextualSearch::PREVIEWING) {
|
| + _enteredPreviewing = YES;
|
| + } else if (state == ContextualSearch::COVERING) {
|
| + _enteredCovering = YES;
|
| + }
|
| +
|
| + if (reason == ContextualSearch::SERP_NAVIGATION) {
|
| + _usedSERPNavigation = YES;
|
| + }
|
| +
|
| + if (endingSearch) {
|
| + _enteredPreviewing = NO;
|
| + _enteredCovering = NO;
|
| + _resultsVisible = NO;
|
| + _exitedPeeking = NO;
|
| + _exitedPreviewing = NO;
|
| + _exitedCovering = NO;
|
| + _searchInvolvedFirstRun = NO;
|
| + _firstRunPanelBecameVisible = NO;
|
| + _searchTermResolved = NO;
|
| + _usedSERPNavigation = NO;
|
| + }
|
| +}
|
| +
|
| +- (void)
|
| +dismissPaneWithJavascriptCompletionHandler:(ProceduralBlock)completionHandler
|
| + reason:(ContextualSearch::StateChangeReason)
|
| + reason {
|
| + [self cleanUpWebStateForDismissWithCompletion:completionHandler];
|
| + [self setState:ContextualSearch::DISMISSED reason:reason];
|
| +}
|
| +
|
| +- (void)cleanUpWebStateForDismissWithCompletion:
|
| + (ProceduralBlock)completionHandler {
|
| + _lastDismiss.reset([[NSDate date] retain]);
|
| + _currentTapCancelled = YES;
|
| + ContextualSearch::PanelState originalState = [_panelView state];
|
| + if (originalState == ContextualSearch::DISMISSED) {
|
| + DCHECK(![_searchResultsView active]);
|
| + if ([self webState]) {
|
| + DOMAlteringLock* lock = DOMAlteringLock::FromWebState([self webState]);
|
| + if (lock) {
|
| + lock->Release(self);
|
| + }
|
| + }
|
| + if (completionHandler)
|
| + completionHandler();
|
| + return;
|
| + }
|
| +
|
| + [_doubleTapRecognizer setEnabled:YES];
|
| + _searchContext.reset();
|
| + [_searchResultsView setActive:NO];
|
| + _delegate->CancelSearchTermRequest();
|
| + _selectedText = "";
|
| +
|
| + ContextualSearchDelegate::SearchResolution blank;
|
| + _resolvedSearch = blank;
|
| + if (completionHandler) {
|
| + base::WeakNSObject<ContextualSearchController> weakSelf(self);
|
| + ProceduralBlock javaScriptCompletion = ^{
|
| + if ([self webState]) {
|
| + DOMAlteringLock::FromWebState([self webState])->Release(self);
|
| + completionHandler();
|
| + }
|
| + };
|
| + [self highlightRects:nil];
|
| + [_contextualSearchJsManager clearHighlight];
|
| + javaScriptCompletion();
|
| + } else {
|
| + [self highlightRects:nil];
|
| + [_contextualSearchJsManager clearHighlight];
|
| + DOMAlteringLock::FromWebState([self webState])->Release(self);
|
| + }
|
| +
|
| + _preventActionMenu = NO;
|
| +
|
| + // If the tapped word was at the bottom of the webview, and it was scrolled
|
| + // up to be displayed over the pane, scroll it back down now.
|
| + // (Ideally this "overscrolling" should just happen as the pane moves).
|
| + // TODO(crbug.com/546227): Handle this with a constraint.
|
| + CGPoint contentOffset = [[_webViewProxy scrollViewProxy] contentOffset];
|
| + CGSize contentSize = [[_webViewProxy scrollViewProxy] contentSize];
|
| + CGSize viewSize = [[_webViewProxy scrollViewProxy] frame].size;
|
| + CGFloat maxOffset = contentSize.height - viewSize.height;
|
| + if (contentOffset.y > maxOffset) {
|
| + contentOffset.y = maxOffset;
|
| + [[_webViewProxy scrollViewProxy] setContentOffset:contentOffset
|
| + animated:YES];
|
| + }
|
| +}
|
| +
|
| +- (void)dismissPane:(ContextualSearch::StateChangeReason)reason {
|
| + [self dismissPaneWithJavascriptCompletionHandler:nil reason:reason];
|
| +}
|
| +
|
| +- (void)peekPane:(ContextualSearch::StateChangeReason)reason {
|
| + [self setState:ContextualSearch::PEEKING reason:reason];
|
| + [_doubleTapRecognizer setEnabled:NO];
|
| + [self scrollToShowSelection:[_webViewProxy scrollViewProxy]];
|
| +}
|
| +
|
| +- (void)previewPane:(ContextualSearch::StateChangeReason)reason {
|
| + if (_searchInvolvedFirstRun) {
|
| + _firstRunPanelBecameVisible = YES;
|
| + }
|
| + [self setState:ContextualSearch::PREVIEWING reason:reason];
|
| + [_doubleTapRecognizer setEnabled:NO];
|
| + [self scrollToShowSelection:[_webViewProxy scrollViewProxy]];
|
| + _delegate->StartPendingSearchTermRequest();
|
| +}
|
| +
|
| +- (void)coverPane:(ContextualSearch::StateChangeReason)reason {
|
| + [self setState:ContextualSearch::COVERING reason:reason];
|
| +}
|
| +
|
| +- (void)movePanelOffscreen {
|
| + [self dismissPane:ContextualSearch::RESET];
|
| +}
|
| +
|
| +#pragma mark - ContextualSearchPromoViewDelegate methods
|
| +
|
| +- (void)promoViewAcceptTapped {
|
| + [self userOptedInFromPromo:YES];
|
| +}
|
| +
|
| +- (void)promoViewDeclineTapped {
|
| + [self userOptedInFromPromo:NO];
|
| +}
|
| +
|
| +- (void)promoViewSettingsTapped {
|
| + base::scoped_nsobject<GenericChromeCommand> command(
|
| + [[GenericChromeCommand alloc]
|
| + initWithTag:IDC_SHOW_CONTEXTUAL_SEARCH_SETTINGS]);
|
| + UIWindow* main_window = [[UIApplication sharedApplication] keyWindow];
|
| + [main_window chromeExecuteCommand:command];
|
| +}
|
| +
|
| +#pragma mark - ContextualSearchWebStateObserver methods
|
| +
|
| +- (void)webState:(web::WebState*)webState
|
| + pageLoadedWithStatus:(web::PageLoadCompletionStatus)loadStatus {
|
| + if (loadStatus != web::PageLoadCompletionStatus::SUCCESS)
|
| + return;
|
| +
|
| + [self movePanelOffscreen];
|
| + _isScriptInjected = NO;
|
| + [self enableCurrentWebState];
|
| +}
|
| +
|
| +- (void)webStateDestroyed:(web::WebState*)webState {
|
| + [self updateWebViewProxy:nil];
|
| +}
|
| +
|
| +#pragma mark - UIGestureRecognizerDelegate Methods
|
| +
|
| +// Ensures that |_tapRecognizer| and |_doubleTapRecognizer| cooperate with all
|
| +// other gesture recognizers.
|
| +- (BOOL)gestureRecognizer:(UIGestureRecognizer*)gestureRecognizer
|
| + shouldRecognizeSimultaneouslyWithGestureRecognizer:
|
| + (UIGestureRecognizer*)otherGestureRecognizer {
|
| + return gestureRecognizer == _tapRecognizer.get() ||
|
| + gestureRecognizer == _doubleTapRecognizer.get();
|
| +}
|
| +
|
| +#pragma mark - CRWWebViewScrollViewObserver methods
|
| +
|
| +- (void)webViewScrollViewWillBeginDragging:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
|
| + [self dismissPane:ContextualSearch::BASE_PAGE_SCROLL];
|
| + [_tapRecognizer setEnabled:NO];
|
| +}
|
| +
|
| +- (void)webViewScrollViewDidEndDragging:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy
|
| + willDecelerate:(BOOL)decelerate {
|
| + if (!decelerate)
|
| + [_tapRecognizer setEnabled:YES];
|
| +}
|
| +
|
| +- (void)webViewScrollViewDidEndDecelerating:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
|
| + [_tapRecognizer setEnabled:YES];
|
| +}
|
| +
|
| +- (void)webViewScrollViewDidScroll:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
|
| + _currentTapCancelled = YES;
|
| + [_contextualHighlightView
|
| + setScroll:[webViewScrollViewProxy contentOffset]
|
| + zoom:[webViewScrollViewProxy zoomScale]
|
| + offset:[_controllerDelegate currentHeaderHeight]];
|
| +}
|
| +
|
| +- (void)webViewScrollViewDidZoom:
|
| + (CRWWebViewScrollViewProxy*)webViewScrollViewProxy {
|
| + _currentTapCancelled = YES;
|
| + [_contextualHighlightView
|
| + setScroll:[webViewScrollViewProxy contentOffset]
|
| + zoom:[webViewScrollViewProxy zoomScale]
|
| + offset:[_controllerDelegate currentHeaderHeight]];
|
| + [self scrollToShowSelection:webViewScrollViewProxy];
|
| +}
|
| +
|
| +#pragma mark - DOMAltering methods
|
| +
|
| +- (BOOL)canReleaseDOMLock {
|
| + return YES;
|
| +}
|
| +
|
| +- (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler {
|
| + [self dismissPaneWithJavascriptCompletionHandler:completionHandler
|
| + reason:ContextualSearch::RESET];
|
| +}
|
| +
|
| +#pragma mark - TouchToSearchPermissionsChangeAudience methods
|
| +
|
| +- (void)touchToSearchDidChangePreferenceState:
|
| + (TouchToSearch::TouchToSearchPreferenceState)preferenceState {
|
| + if (preferenceState != TouchToSearch::UNDECIDED) {
|
| + ContextualSearch::RecordPreferenceChanged(preferenceState ==
|
| + TouchToSearch::ENABLED);
|
| + }
|
| +}
|
| +
|
| +- (void)touchToSearchPermissionsUpdated {
|
| + // This method is already invoked asynchronously, so it's safe to
|
| + // synchronously attempt to enable the feature.
|
| + [self enableContextualSearch:YES];
|
| +}
|
| +
|
| +#pragma mark - ContextualSearchHighlighterDelegate methods
|
| +
|
| +- (void)updateHighlight {
|
| + base::WeakNSObject<ContextualSearchController> weakSelf(self);
|
| + [_contextualSearchJsManager
|
| + highlightRectsWithCompletionHandler:^void(id result, NSError* error) {
|
| + [weakSelf handleHighlightJSResult:result withError:error];
|
| + }];
|
| +}
|
| +
|
| +@end
|
|
|