Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / ios / chrome / browser / find_in_page / find_in_page_controller.mm
blob1e18885d6aa2d54185377fdade261db5e6f754eb
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>
8 #import <cmath>
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";
28 namespace {
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|
54 // can be nil.
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;
67 @end
69 @implementation FindInPageController {
70  @private
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 {
90   self = [super init];
91   if (self) {
92     DCHECK(delegate);
93     _findInPageJsManager = base::mac::ObjCCastStrict<JsFindinpageManager>(
94         [webState->GetJSInjectionReceiver()
95             instanceOfClass:[JsFindinpageManager class]]);
96     _delegate = delegate;
97     _webStateObserverBridge.reset(
98         new web::WebStateObserverBridge(webState, self));
99     _webViewProxy.reset([webState->GetWebViewProxy() retain]);
100     [[NSNotificationCenter defaultCenter]
101         addObserver:self
102            selector:@selector(findBarTextFieldWillBecomeFirstResponder:)
103                name:kFindBarTextFieldWillBecomeFirstResponderNotification
104              object:nil];
105     [[NSNotificationCenter defaultCenter]
106         addObserver:self
107            selector:@selector(findBarTextFieldDidResignFirstResponder:)
108                name:kFindBarTextFieldDidResignFirstResponderNotification
109              object:nil];
110     DOMAlteringLock::CreateForWebState(webState);
111   }
112   return self;
115 - (void)dealloc {
116   [[NSNotificationCenter defaultCenter] removeObserver:self];
117   [super dealloc];
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) {
146     point.y = maxScroll;
147   }
148   return point;
151 - (void)processPumpResult:(BOOL)finished
152               scrollPoint:(CGPoint)scrollPoint
153         completionHandler:(ProceduralBlock)completionHandler {
154   if (finished) {
155     [_delegate willAdjustScrollPosition];
156     scrollPoint = [self limitOverscroll:[_webViewProxy scrollViewProxy]
157                                 atPoint:scrollPoint];
158     [[_webViewProxy scrollViewProxy] setContentOffset:scrollPoint animated:YES];
159     if (completionHandler)
160       completionHandler();
161   } else {
162     [self performSelector:@selector(startPumpingWithCompletionHandler:)
163                withObject:completionHandler
164                afterDelay:kRecurringPumpDelay];
165   }
168 - (void)findStringInPage:(NSString*)query
169        completionHandler:(ProceduralBlock)completionHandler {
170   ProceduralBlockWithBool lockAction = ^(BOOL hasLock) {
171     if (!hasLock) {
172       if (completionHandler) {
173         completionHandler();
174       }
175       return;
176     }
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
187                                      scrollPoint:point
188                                completionHandler:completionHandler];
189                    }];
190   };
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) {
197     if (findFinished) {
198       // Pumping complete. Nothing else to do.
199       if (completionHandler)
200         completionHandler();
201       return;
202     }
203     // Further pumping is required.
204     [weakSelf performSelector:@selector(startPumpingWithCompletionHandler:)
205                    withObject:completionHandler
206                    afterDelay:kRecurringPumpDelay];
207   };
208   [self pumpFindStringInPageWithCompletionHandler:completionHandlerBlock];
211 - (void)pumpFindStringInPageWithCompletionHandler:
212     (void (^)(BOOL))completionHandler {
213   base::WeakNSObject<FindInPageController> weakSelf(self);
214   [_findInPageJsManager pumpWithCompletionHandler:^(BOOL finished,
215                                                     CGPoint point) {
216     base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]);
217     if (finished) {
218       [[strongSelf delegate] willAdjustScrollPosition];
219       point = [strongSelf limitOverscroll:[strongSelf webViewScrollView]
220                                   atPoint:point];
221       [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
222     }
223     completionHandler(finished);
224   }];
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]
235                                 atPoint:point];
236     [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
237     if (completionHandler)
238       completionHandler();
239   }];
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]
251                                 atPoint:point];
252     [[strongSelf webViewScrollView] setContentOffset:point animated:YES];
253     if (completionHandler)
254       completionHandler();
255   }];
258 // Remove highlights from the page and disable the model.
259 - (void)disableFindInPageWithCompletionHandler:
260     (ProceduralBlock)completionHandler {
261   if (![self canFindInPage])
262     return;
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]);
268     if (strongSelf) {
269       [strongSelf.get().findInPageModel setEnabled:NO];
270       web::WebState* webState = [strongSelf webState];
271       if (webState)
272         DOMAlteringLock::FromWebState(webState)->Release(strongSelf);
273     }
274     if (completionHandler)
275       completionHandler();
276   };
277   // Only run JSFindInPageManager disable if there is a string in progress to
278   // avoid WKWebView crash on deallocation due to outstanding completion
279   // handler.
280   if (_findStringStarted) {
281     [_findInPageJsManager disableWithCompletionHandler:handler];
282     _findStringStarted = NO;
283   } else {
284     handler();
285   }
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()
303                                  : nullptr;
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
314                       object:nil];
315   [defaultCenter addObserver:self
316                     selector:@selector(keyboardWillHide:)
317                         name:UIKeyboardWillHideNotification
318                       object:nil];
321 - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note {
322   // Resign from the keyboard appearance notifications on the next turn of the
323   // runloop.
324   dispatch_async(dispatch_get_main_queue(), ^{
325     NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter];
326     [defaultCenter removeObserver:self
327                              name:UIKeyboardDidShowNotification
328                            object:nil];
329     [defaultCenter removeObserver:self
330                              name:UIKeyboardWillHideNotification
331                            object:nil];
332   });
335 - (void)keyboardDidShow:(NSNotification*)note {
336   NSDictionary* info = [note userInfo];
337   CGSize kbSize =
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;
347   }
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 {
371   return NO;
374 - (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler {
375   NOTREACHED();
378 @end