OLD | NEW |
(Empty) | |
| 1 // Copyright 2016 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 #include "ios/chrome/browser/ui/contextual_search/contextual_search_results_view
.h" |
| 6 |
| 7 #include <memory> |
| 8 |
| 9 #import "base/ios/weak_nsobject.h" |
| 10 #include "base/mac/scoped_nsobject.h" |
| 11 #import "ios/chrome/browser/tabs/tab.h" |
| 12 #import "ios/chrome/browser/ui/contextual_search/contextual_search_metrics.h" |
| 13 #import "ios/chrome/browser/ui/contextual_search/contextual_search_panel_view.h" |
| 14 #import "ios/chrome/browser/ui/contextual_search/contextual_search_web_state_obs
erver.h" |
| 15 #import "ios/chrome/common/material_timing.h" |
| 16 #include "ios/web/public/load_committed_details.h" |
| 17 #import "ios/web/public/web_state/crw_web_view_proxy.h" |
| 18 #import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h" |
| 19 #import "ios/web/public/web_state/ui/crw_native_content_provider.h" |
| 20 #import "ios/web/web_state/ui/crw_web_controller.h" |
| 21 |
| 22 namespace { |
| 23 enum SearchResultsViewVisibility { OFFSCREEN, PRELOAD, VISIBLE }; |
| 24 } |
| 25 |
| 26 @interface ContextualSearchResultsView ()<ContextualSearchWebStateDelegate, |
| 27 CRWNativeContentProvider> |
| 28 |
| 29 // Can the search results be scrolled currently? |
| 30 @property(nonatomic, assign, getter=isScrollEnabled) BOOL scrollEnabled; |
| 31 @end |
| 32 |
| 33 @implementation ContextualSearchResultsView { |
| 34 base::WeakNSProtocol<id<ContextualSearchTabPromoter>> _promoter; |
| 35 base::WeakNSProtocol<id<ContextualSearchPreloadChecker>> _preloadChecker; |
| 36 std::unique_ptr<ContextualSearchWebStateObserver> _webStateObserver; |
| 37 |
| 38 // Tab that loads the search results. |
| 39 base::scoped_nsobject<Tab> _tab; |
| 40 |
| 41 // Access to the search tab's web view proxy. |
| 42 base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy; |
| 43 |
| 44 BOOL _loaded; |
| 45 BOOL _displayed; |
| 46 BOOL _loadingError; |
| 47 BOOL _waitingForInitialSearchTabLoad; |
| 48 BOOL _loadInProgress; |
| 49 BOOL _shouldScroll; |
| 50 |
| 51 BOOL _preloadEnabled; |
| 52 |
| 53 SearchResultsViewVisibility _visibility; |
| 54 |
| 55 // Time of starting a search results page load. |
| 56 base::Time _loadStartTime; |
| 57 } |
| 58 |
| 59 @synthesize active = _active; |
| 60 @synthesize opener = _opener; |
| 61 |
| 62 - (instancetype)initWithFrame:(CGRect)frame { |
| 63 if ((self = [super initWithFrame:frame])) { |
| 64 self.translatesAutoresizingMaskIntoConstraints = NO; |
| 65 self.backgroundColor = [UIColor whiteColor]; |
| 66 self.accessibilityIdentifier = @"contextualSearchResultsView"; |
| 67 self.clipsToBounds = YES; |
| 68 _webStateObserver.reset(new ContextualSearchWebStateObserver(self)); |
| 69 _visibility = OFFSCREEN; |
| 70 } |
| 71 return self; |
| 72 } |
| 73 |
| 74 #pragma mark - private properties |
| 75 |
| 76 - (BOOL)isScrollEnabled { |
| 77 return [[_webViewProxy scrollViewProxy] isScrollEnabled]; |
| 78 } |
| 79 |
| 80 - (void)setScrollEnabled:(BOOL)scrollEnabled { |
| 81 [[_webViewProxy scrollViewProxy] setScrollEnabled:scrollEnabled]; |
| 82 [[_webViewProxy scrollViewProxy] setBounces:NO]; |
| 83 } |
| 84 |
| 85 #pragma mark - public properties |
| 86 |
| 87 - (id<ContextualSearchTabPromoter>)promoter { |
| 88 return _promoter; |
| 89 } |
| 90 |
| 91 - (void)setPromoter:(id<ContextualSearchTabPromoter>)promoter { |
| 92 _promoter.reset(promoter); |
| 93 } |
| 94 |
| 95 - (id<ContextualSearchPreloadChecker>)preloadChecker { |
| 96 return _preloadChecker; |
| 97 } |
| 98 |
| 99 - (void)setPreloadChecker:(id<ContextualSearchPreloadChecker>)preloadChecker { |
| 100 _preloadChecker.reset(preloadChecker); |
| 101 } |
| 102 |
| 103 - (BOOL)contentVisible { |
| 104 return _loaded && _visibility == VISIBLE; |
| 105 } |
| 106 |
| 107 - (void)setActive:(BOOL)active { |
| 108 if (active) { |
| 109 // Start watching the embedded Tab's web activity. |
| 110 _webStateObserver->ObserveWebState([_tab webState]); |
| 111 [[_tab webController] setShouldSuppressDialogs:NO]; |
| 112 _webViewProxy.reset([[[_tab webController] webViewProxy] retain]); |
| 113 [[_webViewProxy scrollViewProxy] setBounces:NO]; |
| 114 } else { |
| 115 // Stop watching the embedded Tab's web activity. |
| 116 _webStateObserver->ObserveWebState(nullptr); |
| 117 _webViewProxy.reset(nil); |
| 118 } |
| 119 |
| 120 _active = active; |
| 121 } |
| 122 |
| 123 #pragma mark - public methods |
| 124 |
| 125 - (void)scrollToTopAnimated:(BOOL)animated { |
| 126 [[_webViewProxy scrollViewProxy] setBounces:YES]; |
| 127 // Scroll the search tab's view to the top. |
| 128 [[_webViewProxy scrollViewProxy] setContentOffset:CGPointZero |
| 129 animated:animated]; |
| 130 } |
| 131 |
| 132 - (void)createTabForSearch:(GURL)url preloadEnabled:(BOOL)preloadEnabled { |
| 133 DCHECK(self.opener); |
| 134 if (_tab) |
| 135 [self cancelLoad]; |
| 136 |
| 137 void (^searchTabConfiguration)(Tab*) = ^(Tab* tab) { |
| 138 [tab setIsLinkLoadingPrerenderTab:YES]; |
| 139 [[tab webController] setDelegate:tab]; |
| 140 }; |
| 141 |
| 142 ui::PageTransition transition = ui::PAGE_TRANSITION_FROM_ADDRESS_BAR; |
| 143 Tab* tab = [Tab newPreloadingTabWithBrowserState:self.opener.browserState |
| 144 url:url |
| 145 referrer:web::Referrer() |
| 146 transition:transition |
| 147 provider:self |
| 148 opener:self.opener |
| 149 desktopUserAgent:false |
| 150 configuration:searchTabConfiguration]; |
| 151 _tab.reset([tab retain]); |
| 152 // Don't actually start the page load yet -- that happens in -loadTab |
| 153 |
| 154 _preloadEnabled = preloadEnabled; |
| 155 [self loadPendingSearchIfPossible]; |
| 156 } |
| 157 |
| 158 - (Tab*)releaseTab { |
| 159 [self disconnectTab]; |
| 160 // Allow the search tab to be sized by autoresizing mask again. |
| 161 [[_tab view] setTranslatesAutoresizingMaskIntoConstraints:YES]; |
| 162 return [_tab.release() autorelease]; |
| 163 } |
| 164 |
| 165 - (void)recordFinishedSearchChained:(BOOL)chained { |
| 166 base::TimeDelta duration = base::Time::Now() - _loadStartTime; |
| 167 ContextualSearch::RecordDuration(self.contentVisible, chained, duration); |
| 168 } |
| 169 |
| 170 #pragma mark - private methods |
| 171 |
| 172 - (void)disconnectTab { |
| 173 [[_tab view] removeFromSuperview]; |
| 174 [[_tab webController] setNativeProvider:nil]; |
| 175 self.active = NO; |
| 176 _webViewProxy.reset(); |
| 177 } |
| 178 |
| 179 - (void)cancelLoad { |
| 180 [self disconnectTab]; |
| 181 _loadInProgress = NO; |
| 182 _loaded = NO; |
| 183 [_tab close]; |
| 184 _tab.reset(); |
| 185 } |
| 186 |
| 187 - (void)loadPendingSearchIfPossible { |
| 188 // If the search tab hasn't been created, or if it's already loaded, no-op. |
| 189 if (!_tab.get() || _loadInProgress || self.active || _visibility == OFFSCREEN) |
| 190 return; |
| 191 |
| 192 // If this view is in a position where loading would be "preloading", check |
| 193 // if that's allowed. |
| 194 if (_visibility == PRELOAD && |
| 195 !([_preloadChecker canPreloadSearchResults] && _preloadEnabled)) { |
| 196 return; |
| 197 } |
| 198 [self loadTab]; |
| 199 } |
| 200 |
| 201 - (void)loadTab { |
| 202 DCHECK(_tab.get()); |
| 203 // Start observing the search tab. |
| 204 self.active = YES; |
| 205 // TODO(crbug.com/546223): See if |_waitingForInitialSearchTabLoad| and |
| 206 // |_loadInProgress| can be consolidated. |
| 207 _waitingForInitialSearchTabLoad = YES; |
| 208 // Mark the start of the time for the search. |
| 209 _loadStartTime = base::Time::Now(); |
| 210 // Start the load by asking for the tab's view, but making it hidden. |
| 211 [_tab view].hidden = YES; |
| 212 [self addSubview:[_tab view]]; |
| 213 _loadInProgress = YES; |
| 214 } |
| 215 |
| 216 - (void)displayTab { |
| 217 [_tab view].frame = self.bounds; |
| 218 _loadInProgress = NO; |
| 219 |
| 220 void (^insertTab)(void) = ^{ |
| 221 [_tab view].hidden = NO; |
| 222 self.scrollEnabled = _shouldScroll; |
| 223 }; |
| 224 |
| 225 if (_visibility == VISIBLE) { |
| 226 // Fade it in quickly. |
| 227 UIViewAnimationOptions options = |
| 228 UIViewAnimationOptionTransitionCrossDissolve; |
| 229 [UIView cr_transitionWithView:self |
| 230 duration:ios::material::kDuration2 |
| 231 curve:ios::material::CurveEaseInOut |
| 232 options:options |
| 233 animations:insertTab |
| 234 completion:nil]; |
| 235 } else { |
| 236 // Swap it in. |
| 237 insertTab(); |
| 238 } |
| 239 } |
| 240 |
| 241 #pragma mark - ContextualSearchScrollSynchronizer |
| 242 |
| 243 - (UIGestureRecognizer*)scrollRecognizer { |
| 244 if (_webViewProxy) { |
| 245 UIGestureRecognizer* recognizer = |
| 246 [[_webViewProxy scrollViewProxy] panGestureRecognizer]; |
| 247 if ([recognizer isEnabled]) |
| 248 return recognizer; |
| 249 } |
| 250 return nil; |
| 251 } |
| 252 |
| 253 - (BOOL)scrolled { |
| 254 if (!_webViewProxy) |
| 255 return NO; |
| 256 CGPoint offset = [[_webViewProxy scrollViewProxy] contentOffset]; |
| 257 return !CGPointEqualToPoint(offset, CGPointZero); |
| 258 } |
| 259 |
| 260 #pragma mark - ContextualSearchWebStateDelegate methods |
| 261 |
| 262 - (void)webState:(web::WebState*)webState |
| 263 navigatedWithDetails:(const web::LoadCommittedDetails&)details { |
| 264 if (_loaded) { |
| 265 if (self.active && !details.is_in_page && !_loadingError) { |
| 266 // Use async dispatch so the rest of the OnNavigationItemCommitted() |
| 267 // handlers can complete before the tab changes owners. |
| 268 dispatch_async(dispatch_get_main_queue(), ^{ |
| 269 [_promoter promoteTabHeaderPressed:NO]; |
| 270 }); |
| 271 } |
| 272 _loadingError = NO; |
| 273 } |
| 274 } |
| 275 |
| 276 - (void)webState:(web::WebState*)webState |
| 277 pageLoadedWithStatus:(web::PageLoadCompletionStatus)loadStatus { |
| 278 if (!_loaded) { |
| 279 _loaded = YES; |
| 280 if (_waitingForInitialSearchTabLoad) { |
| 281 _waitingForInitialSearchTabLoad = NO; |
| 282 [self displayTab]; |
| 283 } |
| 284 } |
| 285 } |
| 286 |
| 287 #pragma mark - ContextualSearchPanelMotionObserver |
| 288 |
| 289 - (void)panel:(ContextualSearchPanelView*)panel |
| 290 didChangeToState:(ContextualSearch::PanelState)toState |
| 291 fromState:(ContextualSearch::PanelState)fromState { |
| 292 // Update visibility. |
| 293 switch (toState) { |
| 294 case ContextualSearch::DISMISSED: |
| 295 _visibility = OFFSCREEN; |
| 296 break; |
| 297 case ContextualSearch::PEEKING: |
| 298 _visibility = PRELOAD; |
| 299 break; |
| 300 default: |
| 301 _visibility = VISIBLE; |
| 302 break; |
| 303 } |
| 304 |
| 305 // Load any pending search if now visible. |
| 306 if (_visibility != OFFSCREEN) { |
| 307 [self loadPendingSearchIfPossible]; |
| 308 } |
| 309 |
| 310 // If the drag takes the panel down from covering, and the search tab's |
| 311 // scrolling is enabled, then disable it and reset any scrolling. |
| 312 if (toState <= ContextualSearch::PREVIEWING && |
| 313 fromState == ContextualSearch::COVERING && self.isScrollEnabled) { |
| 314 self.scrollEnabled = NO; |
| 315 [self scrollToTopAnimated:YES]; |
| 316 _shouldScroll = NO; |
| 317 } else if (toState == ContextualSearch::COVERING) { |
| 318 self.scrollEnabled = YES; |
| 319 _shouldScroll = YES; |
| 320 } |
| 321 } |
| 322 |
| 323 - (void)panelWillPromote:(ContextualSearchPanelView*)panel { |
| 324 [panel removeMotionObserver:self]; |
| 325 } |
| 326 |
| 327 #pragma mark - CRWNativeContentProvider methods |
| 328 |
| 329 // Native pages should never be loaded this way. |
| 330 - (BOOL)hasControllerForURL:(const GURL&)url { |
| 331 NOTREACHED(); |
| 332 [self cancelLoad]; |
| 333 return NO; |
| 334 } |
| 335 |
| 336 // Native pages should never be loaded this way. |
| 337 - (id<CRWNativeContent>)controllerForURL:(const GURL&)url { |
| 338 NOTREACHED(); |
| 339 [self cancelLoad]; |
| 340 return nil; |
| 341 } |
| 342 |
| 343 - (id<CRWNativeContent>)controllerForURL:(const GURL&)url |
| 344 withError:(NSError*)error |
| 345 isPost:(BOOL)isPost { |
| 346 // Display the error page in the contextual search tab. |
| 347 _loadingError = YES; |
| 348 [self displayTab]; |
| 349 |
| 350 [[_webViewProxy scrollViewProxy] setScrollEnabled:NO]; |
| 351 id errorController = |
| 352 [self.opener.webController.nativeProvider controllerForURL:url |
| 353 withError:error |
| 354 isPost:isPost]; |
| 355 |
| 356 if ([errorController respondsToSelector:@selector(setScrollEnabled:)]) { |
| 357 [errorController setScrollEnabled:NO]; |
| 358 } |
| 359 return errorController; |
| 360 } |
| 361 |
| 362 @end |
OLD | NEW |