// Copyright 2012 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/find_in_page/find_in_page_controller.h" #import <UIKit/UIKit.h> #import <cmath> #include "base/ios/ios_util.h" #include "base/logging.h" #include "base/mac/foundation_util.h" #include "base/mac/scoped_nsobject.h" #import "ios/chrome/browser/find_in_page/find_in_page_model.h" #import "ios/chrome/browser/find_in_page/js_findinpage_manager.h" #import "ios/chrome/browser/web/dom_altering_lock.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" #import "ios/web/public/web_state/web_state.h" #import "ios/web/public/web_state/web_state_observer_bridge.h" NSString* const kFindBarTextFieldWillBecomeFirstResponderNotification = @"kFindBarTextFieldWillBecomeFirstResponderNotification"; NSString* const kFindBarTextFieldDidResignFirstResponderNotification = @"kFindBarTextFieldDidResignFirstResponderNotification"; namespace { // The delay (in secs) after which the find in page string will be pumped again. const NSTimeInterval kRecurringPumpDelay = .01; } @interface FindInPageController () <DOMAltering, CRWWebStateObserver> // The find in page controller delegate. @property(nonatomic, readonly) id<FindInPageControllerDelegate> delegate; // The web view's scroll view. @property(nonatomic, readonly) CRWWebViewScrollViewProxy* webViewScrollView; // Convenience method to obtain UIPasteboardNameFind from UIPasteBoard. - (UIPasteboard*)findPasteboard; // Find in Page text field listeners. - (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note; - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note; // Keyboard listeners. - (void)keyboardDidShow:(NSNotification*)note; - (void)keyboardWillHide:(NSNotification*)note; // Constantly injects the find string in page until // |disableFindInPageWithCompletionHandler:| is called or the find operation is // complete. Calls |completionHandler| if the find operation is complete. // |completionHandler| can be nil. - (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler; // Gives find in page more time to complete. Calls |completionHandler| with // a BOOL indicating if the find operation was successful. |completionHandler| // can be nil. - (void)pumpFindStringInPageWithCompletionHandler: (void (^)(BOOL))completionHandler; // Processes the result of a single find in page pump. Calls |completionHandler| // if pumping is done. Re-pumps if necessary. - (void)processPumpResult:(BOOL)finished scrollPoint:(CGPoint)scrollPoint completionHandler:(ProceduralBlock)completionHandler; // Prevent scrolling past the end of the page. - (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy atPoint:(CGPoint)point; // Returns the associated web state. May be null. - (web::WebState*)webState; @end @implementation FindInPageController { @private // Object that manages find_in_page.js injection into the web view. JsFindinpageManager* _findInPageJsManager; id<FindInPageControllerDelegate> _delegate; // Access to the web view from the web state. base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy; // True when a find is in progress. Used to avoid running JavaScript during // disable when there is nothing to clear. BOOL _findStringStarted; // Bridge to observe the web state from Objective-C. scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge; } @synthesize delegate = _delegate; - (id)initWithWebState:(web::WebState*)webState delegate:(id<FindInPageControllerDelegate>)delegate { self = [super init]; if (self) { DCHECK(delegate); _findInPageJsManager = base::mac::ObjCCastStrict<JsFindinpageManager>( [webState->GetJSInjectionReceiver() instanceOfClass:[JsFindinpageManager class]]); _delegate = delegate; _webStateObserverBridge.reset( new web::WebStateObserverBridge(webState, self)); _webViewProxy.reset([webState->GetWebViewProxy() retain]); [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(findBarTextFieldWillBecomeFirstResponder:) name:kFindBarTextFieldWillBecomeFirstResponderNotification object:nil]; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(findBarTextFieldDidResignFirstResponder:) name:kFindBarTextFieldDidResignFirstResponderNotification object:nil]; DOMAlteringLock::CreateForWebState(webState); } return self; } - (void)dealloc { [[NSNotificationCenter defaultCenter] removeObserver:self]; [super dealloc]; } - (FindInPageModel*)findInPageModel { return [_findInPageJsManager findInPageModel]; } - (BOOL)canFindInPage { return [_webViewProxy hasSearchableTextContent]; } - (void)initFindInPage { [_findInPageJsManager inject]; // Initialize the module with our frame size. CGRect frame = [_webViewProxy bounds]; [_findInPageJsManager setWidth:frame.size.width height:frame.size.height]; } - (CRWWebViewScrollViewProxy*)webViewScrollView { return [_webViewProxy scrollViewProxy]; } - (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy atPoint:(CGPoint)point { CGFloat contentHeight = scrollViewProxy.contentSize.height; CGFloat frameHeight = scrollViewProxy.frame.size.height; CGFloat maxScroll = std::max<CGFloat>(0, contentHeight - frameHeight); if (point.y > maxScroll) { point.y = maxScroll; } return point; } - (void)processPumpResult:(BOOL)finished scrollPoint:(CGPoint)scrollPoint completionHandler:(ProceduralBlock)completionHandler { if (finished) { [_delegate willAdjustScrollPosition]; scrollPoint = [self limitOverscroll:[_webViewProxy scrollViewProxy] atPoint:scrollPoint]; [[_webViewProxy scrollViewProxy] setContentOffset:scrollPoint animated:YES]; if (completionHandler) completionHandler(); } else { [self performSelector:@selector(startPumpingWithCompletionHandler:) withObject:completionHandler afterDelay:kRecurringPumpDelay]; } } - (void)findStringInPage:(NSString*)query completionHandler:(ProceduralBlock)completionHandler { ProceduralBlockWithBool lockAction = ^(BOOL hasLock) { if (!hasLock) { if (completionHandler) { completionHandler(); } return; } // Cancel any previous pumping. [NSObject cancelPreviousPerformRequestsWithTarget:self]; [self initFindInPage]; // Keep track of whether a find is in progress so to avoid running // JavaScript during disable if unnecessary. _findStringStarted = YES; base::WeakNSObject<FindInPageController> weakSelf(self); [_findInPageJsManager findString:query completionHandler:^(BOOL finished, CGPoint point) { [weakSelf processPumpResult:finished scrollPoint:point completionHandler:completionHandler]; }]; }; DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction); } - (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler { base::WeakNSObject<FindInPageController> weakSelf(self); id completionHandlerBlock = ^void(BOOL findFinished) { if (findFinished) { // Pumping complete. Nothing else to do. if (completionHandler) completionHandler(); return; } // Further pumping is required. [weakSelf performSelector:@selector(startPumpingWithCompletionHandler:) withObject:completionHandler afterDelay:kRecurringPumpDelay]; }; [self pumpFindStringInPageWithCompletionHandler:completionHandlerBlock]; } - (void)pumpFindStringInPageWithCompletionHandler: (void (^)(BOOL))completionHandler { base::WeakNSObject<FindInPageController> weakSelf(self); [_findInPageJsManager pumpWithCompletionHandler:^(BOOL finished, CGPoint point) { base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]); if (finished) { [[strongSelf delegate] willAdjustScrollPosition]; point = [strongSelf limitOverscroll:[strongSelf webViewScrollView] atPoint:point]; [[strongSelf webViewScrollView] setContentOffset:point animated:YES]; } completionHandler(finished); }]; } - (void)findNextStringInPageWithCompletionHandler: (ProceduralBlock)completionHandler { [self initFindInPage]; base::WeakNSObject<FindInPageController> weakSelf(self); [_findInPageJsManager nextMatchWithCompletionHandler:^(CGPoint point) { base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]); [[strongSelf delegate] willAdjustScrollPosition]; point = [strongSelf limitOverscroll:[strongSelf webViewScrollView] atPoint:point]; [[strongSelf webViewScrollView] setContentOffset:point animated:YES]; if (completionHandler) completionHandler(); }]; } // Highlight the previous search match, update model and scroll to match. - (void)findPreviousStringInPageWithCompletionHandler: (ProceduralBlock)completionHandler { [self initFindInPage]; base::WeakNSObject<FindInPageController> weakSelf(self); [_findInPageJsManager previousMatchWithCompletionHandler:^(CGPoint point) { base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]); [[strongSelf delegate] willAdjustScrollPosition]; point = [strongSelf limitOverscroll:[strongSelf webViewScrollView] atPoint:point]; [[strongSelf webViewScrollView] setContentOffset:point animated:YES]; if (completionHandler) completionHandler(); }]; } // Remove highlights from the page and disable the model. - (void)disableFindInPageWithCompletionHandler: (ProceduralBlock)completionHandler { if (![self canFindInPage]) return; // Cancel any queued calls to |recurringPumpWithCompletionHandler|. [NSObject cancelPreviousPerformRequestsWithTarget:self]; base::WeakNSObject<FindInPageController> weakSelf(self); ProceduralBlock handler = ^{ base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]); if (strongSelf) { [strongSelf.get().findInPageModel setEnabled:NO]; web::WebState* webState = [strongSelf webState]; if (webState) DOMAlteringLock::FromWebState(webState)->Release(strongSelf); } if (completionHandler) completionHandler(); }; // Only run JSFindInPageManager disable if there is a string in progress to // avoid WKWebView crash on deallocation due to outstanding completion // handler. if (_findStringStarted) { [_findInPageJsManager disableWithCompletionHandler:handler]; _findStringStarted = NO; } else { handler(); } } - (void)saveSearchTerm { [self findPasteboard].string = [[self findInPageModel] text]; } - (void)restoreSearchTerm { // Pasteboards always return nil in background: if ([[UIApplication sharedApplication] applicationState] != UIApplicationStateActive) { return; } NSString* term = [self findPasteboard].string; [[self findInPageModel] updateQuery:(term ? term : @"") matches:0]; } - (UIPasteboard*)findPasteboard { return [UIPasteboard pasteboardWithName:UIPasteboardNameFind create:NO]; } - (web::WebState*)webState { return _webStateObserverBridge ? _webStateObserverBridge->web_state() : nullptr; } #pragma mark - Notification listeners - (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note { // Listen to the keyboard appearance notifications. NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; [defaultCenter addObserver:self selector:@selector(keyboardDidShow:) name:UIKeyboardDidShowNotification object:nil]; [defaultCenter addObserver:self selector:@selector(keyboardWillHide:) name:UIKeyboardWillHideNotification object:nil]; } - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note { // Resign from the keyboard appearance notifications on the next turn of the // runloop. dispatch_async(dispatch_get_main_queue(), ^{ NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; [defaultCenter removeObserver:self name:UIKeyboardDidShowNotification object:nil]; [defaultCenter removeObserver:self name:UIKeyboardWillHideNotification object:nil]; }); } - (void)keyboardDidShow:(NSNotification*)note { NSDictionary* info = [note userInfo]; CGSize kbSize = [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; UIInterfaceOrientation orientation = [[UIApplication sharedApplication] statusBarOrientation]; CGFloat kbHeight = kbSize.height; // Prior to iOS 8, the keyboard frame was not dependent on interface // orientation, so height and width need to be swapped in landscape mode. if (UIInterfaceOrientationIsLandscape(orientation) && !base::ios::IsRunningOnIOS8OrLater()) { kbHeight = kbSize.width; } UIEdgeInsets insets = UIEdgeInsetsZero; insets.bottom = kbHeight; [_webViewProxy registerInsets:insets forCaller:self]; } - (void)keyboardWillHide:(NSNotification*)note { [_webViewProxy unregisterInsetsForCaller:self]; } - (void)detachFromWebState { _webStateObserverBridge.reset(); } #pragma mark - CRWWebStateObserver Methods - (void)webStateDestroyed:(web::WebState*)webState { [self detachFromWebState]; } #pragma mark - DOMAltering Methods - (BOOL)canReleaseDOMLock { return NO; } - (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler { NOTREACHED(); } @end