Index: ios/chrome/browser/find_in_page/find_in_page_controller.mm |
diff --git a/ios/chrome/browser/find_in_page/find_in_page_controller.mm b/ios/chrome/browser/find_in_page/find_in_page_controller.mm |
new file mode 100644 |
index 0000000000000000000000000000000000000000..0d1621e15ae12f2e5c816d8048dc24394cdec2ad |
--- /dev/null |
+++ b/ios/chrome/browser/find_in_page/find_in_page_controller.mm |
@@ -0,0 +1,357 @@ |
+// 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> |
+ |
+#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 successfull. |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; |
+// 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]; |
+} |
+ |
+- (void)processPumpResult:(BOOL)finished |
+ scrollPoint:(CGPoint)scrollPoint |
+ completionHandler:(ProceduralBlock)completionHandler { |
+ if (finished) { |
+ [_delegate willAdjustScrollPosition]; |
+ [[_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]; |
+ [[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]; |
+ CGFloat contentHeight = [strongSelf webViewScrollView].contentSize.height; |
+ CGFloat frameHeight = [strongSelf webViewScrollView].frame.size.height; |
+ CGFloat maxScroll = fmax(0, contentHeight - frameHeight); |
+ if (point.y > maxScroll) { |
+ point.y = maxScroll; |
+ } |
+ [[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]; |
+ [[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 = ^{ |
+ web::WebState* webState = [self webState]; |
+ if (webState) |
+ DOMAlteringLock::FromWebState(webState)->Release(self); |
+ 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 { |
+ 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 |