Index: ios/chrome/browser/ui/contextual_search/contextual_search_results_view.mm |
diff --git a/ios/chrome/browser/ui/contextual_search/contextual_search_results_view.mm b/ios/chrome/browser/ui/contextual_search/contextual_search_results_view.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..50ba220b849dfc0585a366ed1211e093fe719cdf |
--- /dev/null |
+++ b/ios/chrome/browser/ui/contextual_search/contextual_search_results_view.mm |
@@ -0,0 +1,362 @@ |
+// Copyright 2016 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. |
+ |
+#include "ios/chrome/browser/ui/contextual_search/contextual_search_results_view.h" |
+ |
+#include <memory> |
+ |
+#import "base/ios/weak_nsobject.h" |
+#include "base/mac/scoped_nsobject.h" |
+#import "ios/chrome/browser/tabs/tab.h" |
+#import "ios/chrome/browser/ui/contextual_search/contextual_search_metrics.h" |
+#import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.h" |
+#import "ios/chrome/browser/ui/contextual_search/contextual_search_web_state_observer.h" |
+#import "ios/chrome/common/material_timing.h" |
+#include "ios/web/public/load_committed_details.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/ui/crw_native_content_provider.h" |
+#import "ios/web/web_state/ui/crw_web_controller.h" |
+ |
+namespace { |
+enum SearchResultsViewVisibility { OFFSCREEN, PRELOAD, VISIBLE }; |
+} |
+ |
+@interface ContextualSearchResultsView ()<ContextualSearchWebStateDelegate, |
+ CRWNativeContentProvider> |
+ |
+// Can the search results be scrolled currently? |
+@property(nonatomic, assign, getter=isScrollEnabled) BOOL scrollEnabled; |
+@end |
+ |
+@implementation ContextualSearchResultsView { |
+ base::WeakNSProtocol<id<ContextualSearchTabPromoter>> _promoter; |
+ base::WeakNSProtocol<id<ContextualSearchPreloadChecker>> _preloadChecker; |
+ std::unique_ptr<ContextualSearchWebStateObserver> _webStateObserver; |
+ |
+ // Tab that loads the search results. |
+ base::scoped_nsobject<Tab> _tab; |
+ |
+ // Access to the search tab's web view proxy. |
+ base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy; |
+ |
+ BOOL _loaded; |
+ BOOL _displayed; |
+ BOOL _loadingError; |
+ BOOL _waitingForInitialSearchTabLoad; |
+ BOOL _loadInProgress; |
+ BOOL _shouldScroll; |
+ |
+ BOOL _preloadEnabled; |
+ |
+ SearchResultsViewVisibility _visibility; |
+ |
+ // Time of starting a search results page load. |
+ base::Time _loadStartTime; |
+} |
+ |
+@synthesize active = _active; |
+@synthesize opener = _opener; |
+ |
+- (instancetype)initWithFrame:(CGRect)frame { |
+ if ((self = [super initWithFrame:frame])) { |
+ self.translatesAutoresizingMaskIntoConstraints = NO; |
+ self.backgroundColor = [UIColor whiteColor]; |
+ self.accessibilityIdentifier = @"contextualSearchResultsView"; |
+ self.clipsToBounds = YES; |
+ _webStateObserver.reset(new ContextualSearchWebStateObserver(self)); |
+ _visibility = OFFSCREEN; |
+ } |
+ return self; |
+} |
+ |
+#pragma mark - private properties |
+ |
+- (BOOL)isScrollEnabled { |
+ return [[_webViewProxy scrollViewProxy] isScrollEnabled]; |
+} |
+ |
+- (void)setScrollEnabled:(BOOL)scrollEnabled { |
+ [[_webViewProxy scrollViewProxy] setScrollEnabled:scrollEnabled]; |
+ [[_webViewProxy scrollViewProxy] setBounces:NO]; |
+} |
+ |
+#pragma mark - public properties |
+ |
+- (id<ContextualSearchTabPromoter>)promoter { |
+ return _promoter; |
+} |
+ |
+- (void)setPromoter:(id<ContextualSearchTabPromoter>)promoter { |
+ _promoter.reset(promoter); |
+} |
+ |
+- (id<ContextualSearchPreloadChecker>)preloadChecker { |
+ return _preloadChecker; |
+} |
+ |
+- (void)setPreloadChecker:(id<ContextualSearchPreloadChecker>)preloadChecker { |
+ _preloadChecker.reset(preloadChecker); |
+} |
+ |
+- (BOOL)contentVisible { |
+ return _loaded && _visibility == VISIBLE; |
+} |
+ |
+- (void)setActive:(BOOL)active { |
+ if (active) { |
+ // Start watching the embedded Tab's web activity. |
+ _webStateObserver->ObserveWebState([_tab webState]); |
+ [[_tab webController] setShouldSuppressDialogs:NO]; |
+ _webViewProxy.reset([[[_tab webController] webViewProxy] retain]); |
+ [[_webViewProxy scrollViewProxy] setBounces:NO]; |
+ } else { |
+ // Stop watching the embedded Tab's web activity. |
+ _webStateObserver->ObserveWebState(nullptr); |
+ _webViewProxy.reset(nil); |
+ } |
+ |
+ _active = active; |
+} |
+ |
+#pragma mark - public methods |
+ |
+- (void)scrollToTopAnimated:(BOOL)animated { |
+ [[_webViewProxy scrollViewProxy] setBounces:YES]; |
+ // Scroll the search tab's view to the top. |
+ [[_webViewProxy scrollViewProxy] setContentOffset:CGPointZero |
+ animated:animated]; |
+} |
+ |
+- (void)createTabForSearch:(GURL)url preloadEnabled:(BOOL)preloadEnabled { |
+ DCHECK(self.opener); |
+ if (_tab) |
+ [self cancelLoad]; |
+ |
+ void (^searchTabConfiguration)(Tab*) = ^(Tab* tab) { |
+ [tab setIsLinkLoadingPrerenderTab:YES]; |
+ [[tab webController] setDelegate:tab]; |
+ }; |
+ |
+ ui::PageTransition transition = ui::PAGE_TRANSITION_FROM_ADDRESS_BAR; |
+ Tab* tab = [Tab newPreloadingTabWithBrowserState:self.opener.browserState |
+ url:url |
+ referrer:web::Referrer() |
+ transition:transition |
+ provider:self |
+ opener:self.opener |
+ desktopUserAgent:false |
+ configuration:searchTabConfiguration]; |
+ _tab.reset([tab retain]); |
+ // Don't actually start the page load yet -- that happens in -loadTab |
+ |
+ _preloadEnabled = preloadEnabled; |
+ [self loadPendingSearchIfPossible]; |
+} |
+ |
+- (Tab*)releaseTab { |
+ [self disconnectTab]; |
+ // Allow the search tab to be sized by autoresizing mask again. |
+ [[_tab view] setTranslatesAutoresizingMaskIntoConstraints:YES]; |
+ return [_tab.release() autorelease]; |
+} |
+ |
+- (void)recordFinishedSearchChained:(BOOL)chained { |
+ base::TimeDelta duration = base::Time::Now() - _loadStartTime; |
+ ContextualSearch::RecordDuration(self.contentVisible, chained, duration); |
+} |
+ |
+#pragma mark - private methods |
+ |
+- (void)disconnectTab { |
+ [[_tab view] removeFromSuperview]; |
+ [[_tab webController] setNativeProvider:nil]; |
+ self.active = NO; |
+ _webViewProxy.reset(); |
+} |
+ |
+- (void)cancelLoad { |
+ [self disconnectTab]; |
+ _loadInProgress = NO; |
+ _loaded = NO; |
+ [_tab close]; |
+ _tab.reset(); |
+} |
+ |
+- (void)loadPendingSearchIfPossible { |
+ // If the search tab hasn't been created, or if it's already loaded, no-op. |
+ if (!_tab.get() || _loadInProgress || self.active || _visibility == OFFSCREEN) |
+ return; |
+ |
+ // If this view is in a position where loading would be "preloading", check |
+ // if that's allowed. |
+ if (_visibility == PRELOAD && |
+ !([_preloadChecker canPreloadSearchResults] && _preloadEnabled)) { |
+ return; |
+ } |
+ [self loadTab]; |
+} |
+ |
+- (void)loadTab { |
+ DCHECK(_tab.get()); |
+ // Start observing the search tab. |
+ self.active = YES; |
+ // TODO(crbug.com/546223): See if |_waitingForInitialSearchTabLoad| and |
+ // |_loadInProgress| can be consolidated. |
+ _waitingForInitialSearchTabLoad = YES; |
+ // Mark the start of the time for the search. |
+ _loadStartTime = base::Time::Now(); |
+ // Start the load by asking for the tab's view, but making it hidden. |
+ [_tab view].hidden = YES; |
+ [self addSubview:[_tab view]]; |
+ _loadInProgress = YES; |
+} |
+ |
+- (void)displayTab { |
+ [_tab view].frame = self.bounds; |
+ _loadInProgress = NO; |
+ |
+ void (^insertTab)(void) = ^{ |
+ [_tab view].hidden = NO; |
+ self.scrollEnabled = _shouldScroll; |
+ }; |
+ |
+ if (_visibility == VISIBLE) { |
+ // Fade it in quickly. |
+ UIViewAnimationOptions options = |
+ UIViewAnimationOptionTransitionCrossDissolve; |
+ [UIView cr_transitionWithView:self |
+ duration:ios::material::kDuration2 |
+ curve:ios::material::CurveEaseInOut |
+ options:options |
+ animations:insertTab |
+ completion:nil]; |
+ } else { |
+ // Swap it in. |
+ insertTab(); |
+ } |
+} |
+ |
+#pragma mark - ContextualSearchScrollSynchronizer |
+ |
+- (UIGestureRecognizer*)scrollRecognizer { |
+ if (_webViewProxy) { |
+ UIGestureRecognizer* recognizer = |
+ [[_webViewProxy scrollViewProxy] panGestureRecognizer]; |
+ if ([recognizer isEnabled]) |
+ return recognizer; |
+ } |
+ return nil; |
+} |
+ |
+- (BOOL)scrolled { |
+ if (!_webViewProxy) |
+ return NO; |
+ CGPoint offset = [[_webViewProxy scrollViewProxy] contentOffset]; |
+ return !CGPointEqualToPoint(offset, CGPointZero); |
+} |
+ |
+#pragma mark - ContextualSearchWebStateDelegate methods |
+ |
+- (void)webState:(web::WebState*)webState |
+ navigatedWithDetails:(const web::LoadCommittedDetails&)details { |
+ if (_loaded) { |
+ if (self.active && !details.is_in_page && !_loadingError) { |
+ // Use async dispatch so the rest of the OnNavigationItemCommitted() |
+ // handlers can complete before the tab changes owners. |
+ dispatch_async(dispatch_get_main_queue(), ^{ |
+ [_promoter promoteTabHeaderPressed:NO]; |
+ }); |
+ } |
+ _loadingError = NO; |
+ } |
+} |
+ |
+- (void)webState:(web::WebState*)webState |
+ pageLoadedWithStatus:(web::PageLoadCompletionStatus)loadStatus { |
+ if (!_loaded) { |
+ _loaded = YES; |
+ if (_waitingForInitialSearchTabLoad) { |
+ _waitingForInitialSearchTabLoad = NO; |
+ [self displayTab]; |
+ } |
+ } |
+} |
+ |
+#pragma mark - ContextualSearchPanelMotionObserver |
+ |
+- (void)panel:(ContextualSearchPanelView*)panel |
+ didChangeToState:(ContextualSearch::PanelState)toState |
+ fromState:(ContextualSearch::PanelState)fromState { |
+ // Update visibility. |
+ switch (toState) { |
+ case ContextualSearch::DISMISSED: |
+ _visibility = OFFSCREEN; |
+ break; |
+ case ContextualSearch::PEEKING: |
+ _visibility = PRELOAD; |
+ break; |
+ default: |
+ _visibility = VISIBLE; |
+ break; |
+ } |
+ |
+ // Load any pending search if now visible. |
+ if (_visibility != OFFSCREEN) { |
+ [self loadPendingSearchIfPossible]; |
+ } |
+ |
+ // If the drag takes the panel down from covering, and the search tab's |
+ // scrolling is enabled, then disable it and reset any scrolling. |
+ if (toState <= ContextualSearch::PREVIEWING && |
+ fromState == ContextualSearch::COVERING && self.isScrollEnabled) { |
+ self.scrollEnabled = NO; |
+ [self scrollToTopAnimated:YES]; |
+ _shouldScroll = NO; |
+ } else if (toState == ContextualSearch::COVERING) { |
+ self.scrollEnabled = YES; |
+ _shouldScroll = YES; |
+ } |
+} |
+ |
+- (void)panelWillPromote:(ContextualSearchPanelView*)panel { |
+ [panel removeMotionObserver:self]; |
+} |
+ |
+#pragma mark - CRWNativeContentProvider methods |
+ |
+// Native pages should never be loaded this way. |
+- (BOOL)hasControllerForURL:(const GURL&)url { |
+ NOTREACHED(); |
+ [self cancelLoad]; |
+ return NO; |
+} |
+ |
+// Native pages should never be loaded this way. |
+- (id<CRWNativeContent>)controllerForURL:(const GURL&)url { |
+ NOTREACHED(); |
+ [self cancelLoad]; |
+ return nil; |
+} |
+ |
+- (id<CRWNativeContent>)controllerForURL:(const GURL&)url |
+ withError:(NSError*)error |
+ isPost:(BOOL)isPost { |
+ // Display the error page in the contextual search tab. |
+ _loadingError = YES; |
+ [self displayTab]; |
+ |
+ [[_webViewProxy scrollViewProxy] setScrollEnabled:NO]; |
+ id errorController = |
+ [self.opener.webController.nativeProvider controllerForURL:url |
+ withError:error |
+ isPost:isPost]; |
+ |
+ if ([errorController respondsToSelector:@selector(setScrollEnabled:)]) { |
+ [errorController setScrollEnabled:NO]; |
+ } |
+ return errorController; |
+} |
+ |
+@end |