1 // Copyright 2012 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.
5 #import "ios/chrome/browser/find_in_page/find_in_page_controller.h"
7 #import <UIKit/UIKit.h>
10 #include "base/ios/ios_util.h"
11 #include "base/logging.h"
12 #include "base/mac/foundation_util.h"
13 #include "base/mac/scoped_nsobject.h"
14 #import "ios/chrome/browser/find_in_page/find_in_page_model.h"
15 #import "ios/chrome/browser/find_in_page/js_findinpage_manager.h"
16 #import "ios/chrome/browser/web/dom_altering_lock.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/js/crw_js_injection_receiver.h"
20 #import "ios/web/public/web_state/web_state.h"
21 #import "ios/web/public/web_state/web_state_observer_bridge.h"
23 NSString* const kFindBarTextFieldWillBecomeFirstResponderNotification =
24 @"kFindBarTextFieldWillBecomeFirstResponderNotification";
25 NSString* const kFindBarTextFieldDidResignFirstResponderNotification =
26 @"kFindBarTextFieldDidResignFirstResponderNotification";
29 // The delay (in secs) after which the find in page string will be pumped again.
30 const NSTimeInterval kRecurringPumpDelay = .01;
33 @interface FindInPageController () <DOMAltering, CRWWebStateObserver>
34 // The find in page controller delegate.
35 @property(nonatomic, readonly) id<FindInPageControllerDelegate> delegate;
36 // The web view's scroll view.
37 @property(nonatomic, readonly) CRWWebViewScrollViewProxy* webViewScrollView;
39 // Convenience method to obtain UIPasteboardNameFind from UIPasteBoard.
40 - (UIPasteboard*)findPasteboard;
41 // Find in Page text field listeners.
42 - (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note;
43 - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note;
44 // Keyboard listeners.
45 - (void)keyboardDidShow:(NSNotification*)note;
46 - (void)keyboardWillHide:(NSNotification*)note;
47 // Constantly injects the find string in page until
48 // |disableFindInPageWithCompletionHandler:| is called or the find operation is
49 // complete. Calls |completionHandler| if the find operation is complete.
50 // |completionHandler| can be nil.
51 - (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler;
52 // Gives find in page more time to complete. Calls |completionHandler| with
53 // a BOOL indicating if the find operation was successfull. |completionHandler|
55 - (void)pumpFindStringInPageWithCompletionHandler:
56 (void (^)(BOOL))completionHandler;
57 // Processes the result of a single find in page pump. Calls |completionHandler|
58 // if pumping is done. Re-pumps if necessary.
59 - (void)processPumpResult:(BOOL)finished
60 scrollPoint:(CGPoint)scrollPoint
61 completionHandler:(ProceduralBlock)completionHandler;
62 // Prevent scrolling past the end of the page.
63 - (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy
64 atPoint:(CGPoint)point;
65 // Returns the associated web state. May be null.
66 - (web::WebState*)webState;
69 @implementation FindInPageController {
71 // Object that manages find_in_page.js injection into the web view.
72 JsFindinpageManager* _findInPageJsManager;
73 id<FindInPageControllerDelegate> _delegate;
75 // Access to the web view from the web state.
76 base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy;
78 // True when a find is in progress. Used to avoid running JavaScript during
79 // disable when there is nothing to clear.
80 BOOL _findStringStarted;
82 // Bridge to observe the web state from Objective-C.
83 scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge;
86 @synthesize delegate = _delegate;
88 - (id)initWithWebState:(web::WebState*)webState
89 delegate:(id<FindInPageControllerDelegate>)delegate {
93 _findInPageJsManager = base::mac::ObjCCastStrict<JsFindinpageManager>(
94 [webState->GetJSInjectionReceiver()
95 instanceOfClass:[JsFindinpageManager class]]);
97 _webStateObserverBridge.reset(
98 new web::WebStateObserverBridge(webState, self));
99 _webViewProxy.reset([webState->GetWebViewProxy() retain]);
100 [[NSNotificationCenter defaultCenter]
102 selector:@selector(findBarTextFieldWillBecomeFirstResponder:)
103 name:kFindBarTextFieldWillBecomeFirstResponderNotification
105 [[NSNotificationCenter defaultCenter]
107 selector:@selector(findBarTextFieldDidResignFirstResponder:)
108 name:kFindBarTextFieldDidResignFirstResponderNotification
110 DOMAlteringLock::CreateForWebState(webState);
116 [[NSNotificationCenter defaultCenter] removeObserver:self];
120 - (FindInPageModel*)findInPageModel {
121 return [_findInPageJsManager findInPageModel];
124 - (BOOL)canFindInPage {
125 return [_webViewProxy hasSearchableTextContent];
128 - (void)initFindInPage {
129 [_findInPageJsManager inject];
131 // Initialize the module with our frame size.
132 CGRect frame = [_webViewProxy bounds];
133 [_findInPageJsManager setWidth:frame.size.width height:frame.size.height];
136 - (CRWWebViewScrollViewProxy*)webViewScrollView {
137 return [_webViewProxy scrollViewProxy];
140 - (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy
141 atPoint:(CGPoint)point {
142 CGFloat contentHeight = scrollViewProxy.contentSize.height;
143 CGFloat frameHeight = scrollViewProxy.frame.size.height;
144 CGFloat maxScroll = std::max<CGFloat>(0, contentHeight - frameHeight);
145 if (point.y > maxScroll) {
151 - (void)processPumpResult:(BOOL)finished
152 scrollPoint:(CGPoint)scrollPoint
153 completionHandler:(ProceduralBlock)completionHandler {
155 [_delegate willAdjustScrollPosition];
156 scrollPoint = [self limitOverscroll:[_webViewProxy scrollViewProxy]
157 atPoint:scrollPoint];
158 [[_webViewProxy scrollViewProxy] setContentOffset:scrollPoint animated:YES];
159 if (completionHandler)
162 [self performSelector:@selector(startPumpingWithCompletionHandler:)
163 withObject:completionHandler
164 afterDelay:kRecurringPumpDelay];
168 - (void)findStringInPage:(NSString*)query
169 completionHandler:(ProceduralBlock)completionHandler {
170 ProceduralBlockWithBool lockAction = ^(BOOL hasLock) {
172 if (completionHandler) {
177 // Cancel any previous pumping.
178 [NSObject cancelPreviousPerformRequestsWithTarget:self];
179 [self initFindInPage];
180 // Keep track of whether a find is in progress so to avoid running
181 // JavaScript during disable if unnecessary.
182 _findStringStarted = YES;
183 base::WeakNSObject<FindInPageController> weakSelf(self);
184 [_findInPageJsManager findString:query
185 completionHandler:^(BOOL finished, CGPoint point) {
186 [weakSelf processPumpResult:finished
188 completionHandler:completionHandler];
191 DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction);
194 - (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler {
195 base::WeakNSObject<FindInPageController> weakSelf(self);
196 id completionHandlerBlock = ^void(BOOL findFinished) {
198 // Pumping complete. Nothing else to do.
199 if (completionHandler)
203 // Further pumping is required.
204 [weakSelf performSelector:@selector(startPumpingWithCompletionHandler:)
205 withObject:completionHandler
206 afterDelay:kRecurringPumpDelay];
208 [self pumpFindStringInPageWithCompletionHandler:completionHandlerBlock];
211 - (void)pumpFindStringInPageWithCompletionHandler:
212 (void (^)(BOOL))completionHandler {
213 base::WeakNSObject<FindInPageController> weakSelf(self);
214 [_findInPageJsManager pumpWithCompletionHandler:^(BOOL finished,
216 base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
218 [[strongSelf delegate] willAdjustScrollPosition];
219 point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
221 [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
223 completionHandler(finished);
227 - (void)findNextStringInPageWithCompletionHandler:
228 (ProceduralBlock)completionHandler {
229 [self initFindInPage];
230 base::WeakNSObject<FindInPageController> weakSelf(self);
231 [_findInPageJsManager nextMatchWithCompletionHandler:^(CGPoint point) {
232 base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
233 [[strongSelf delegate] willAdjustScrollPosition];
234 point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
236 [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
237 if (completionHandler)
242 // Highlight the previous search match, update model and scroll to match.
243 - (void)findPreviousStringInPageWithCompletionHandler:
244 (ProceduralBlock)completionHandler {
245 [self initFindInPage];
246 base::WeakNSObject<FindInPageController> weakSelf(self);
247 [_findInPageJsManager previousMatchWithCompletionHandler:^(CGPoint point) {
248 base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
249 [[strongSelf delegate] willAdjustScrollPosition];
250 point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
252 [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
253 if (completionHandler)
258 // Remove highlights from the page and disable the model.
259 - (void)disableFindInPageWithCompletionHandler:
260 (ProceduralBlock)completionHandler {
261 if (![self canFindInPage])
263 // Cancel any queued calls to |recurringPumpWithCompletionHandler|.
264 [NSObject cancelPreviousPerformRequestsWithTarget:self];
265 base::WeakNSObject<FindInPageController> weakSelf(self);
266 ProceduralBlock handler = ^{
267 base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
269 [strongSelf.get().findInPageModel setEnabled:NO];
270 web::WebState* webState = [strongSelf webState];
272 DOMAlteringLock::FromWebState(webState)->Release(strongSelf);
274 if (completionHandler)
277 // Only run JSFindInPageManager disable if there is a string in progress to
278 // avoid WKWebView crash on deallocation due to outstanding completion
280 if (_findStringStarted) {
281 [_findInPageJsManager disableWithCompletionHandler:handler];
282 _findStringStarted = NO;
288 - (void)saveSearchTerm {
289 [self findPasteboard].string = [[self findInPageModel] text];
292 - (void)restoreSearchTerm {
293 NSString* term = [self findPasteboard].string;
294 [[self findInPageModel] updateQuery:(term ? term : @"") matches:0];
297 - (UIPasteboard*)findPasteboard {
298 return [UIPasteboard pasteboardWithName:UIPasteboardNameFind create:NO];
301 - (web::WebState*)webState {
302 return _webStateObserverBridge ? _webStateObserverBridge->web_state()
306 #pragma mark - Notification listeners
308 - (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note {
309 // Listen to the keyboard appearance notifications.
310 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
311 [defaultCenter addObserver:self
312 selector:@selector(keyboardDidShow:)
313 name:UIKeyboardDidShowNotification
315 [defaultCenter addObserver:self
316 selector:@selector(keyboardWillHide:)
317 name:UIKeyboardWillHideNotification
321 - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note {
322 // Resign from the keyboard appearance notifications on the next turn of the
324 dispatch_async(dispatch_get_main_queue(), ^{
325 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
326 [defaultCenter removeObserver:self
327 name:UIKeyboardDidShowNotification
329 [defaultCenter removeObserver:self
330 name:UIKeyboardWillHideNotification
335 - (void)keyboardDidShow:(NSNotification*)note {
336 NSDictionary* info = [note userInfo];
338 [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size;
339 UIInterfaceOrientation orientation =
340 [[UIApplication sharedApplication] statusBarOrientation];
341 CGFloat kbHeight = kbSize.height;
342 // Prior to iOS 8, the keyboard frame was not dependent on interface
343 // orientation, so height and width need to be swapped in landscape mode.
344 if (UIInterfaceOrientationIsLandscape(orientation) &&
345 !base::ios::IsRunningOnIOS8OrLater()) {
346 kbHeight = kbSize.width;
349 UIEdgeInsets insets = UIEdgeInsetsZero;
350 insets.bottom = kbHeight;
351 [_webViewProxy registerInsets:insets forCaller:self];
354 - (void)keyboardWillHide:(NSNotification*)note {
355 [_webViewProxy unregisterInsetsForCaller:self];
358 - (void)detachFromWebState {
359 _webStateObserverBridge.reset();
362 #pragma mark - CRWWebStateObserver Methods
364 - (void)webStateDestroyed:(web::WebState*)webState {
365 [self detachFromWebState];
368 #pragma mark - DOMAltering Methods
370 - (BOOL)canReleaseDOMLock {
374 - (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler {