OLD | NEW |
(Empty) | |
| 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. |
| 4 |
| 5 #import "ios/chrome/browser/find_in_page/find_in_page_controller.h" |
| 6 |
| 7 #import <UIKit/UIKit.h> |
| 8 |
| 9 #include "base/ios/ios_util.h" |
| 10 #include "base/logging.h" |
| 11 #include "base/mac/foundation_util.h" |
| 12 #include "base/mac/scoped_nsobject.h" |
| 13 #import "ios/chrome/browser/find_in_page/find_in_page_model.h" |
| 14 #import "ios/chrome/browser/find_in_page/js_findinpage_manager.h" |
| 15 #import "ios/chrome/browser/web/dom_altering_lock.h" |
| 16 #import "ios/web/public/web_state/crw_web_view_proxy.h" |
| 17 #import "ios/web/public/web_state/crw_web_view_scroll_view_proxy.h" |
| 18 #import "ios/web/public/web_state/js/crw_js_injection_receiver.h" |
| 19 #import "ios/web/public/web_state/web_state.h" |
| 20 #import "ios/web/public/web_state/web_state_observer_bridge.h" |
| 21 |
| 22 NSString* const kFindBarTextFieldWillBecomeFirstResponderNotification = |
| 23 @"kFindBarTextFieldWillBecomeFirstResponderNotification"; |
| 24 NSString* const kFindBarTextFieldDidResignFirstResponderNotification = |
| 25 @"kFindBarTextFieldDidResignFirstResponderNotification"; |
| 26 |
| 27 namespace { |
| 28 // The delay (in secs) after which the find in page string will be pumped again. |
| 29 const NSTimeInterval kRecurringPumpDelay = .01; |
| 30 } |
| 31 |
| 32 @interface FindInPageController () <DOMAltering, CRWWebStateObserver> |
| 33 // The find in page controller delegate. |
| 34 @property(nonatomic, readonly) id<FindInPageControllerDelegate> delegate; |
| 35 // The web view's scroll view. |
| 36 @property(nonatomic, readonly) CRWWebViewScrollViewProxy* webViewScrollView; |
| 37 |
| 38 // Convenience method to obtain UIPasteboardNameFind from UIPasteBoard. |
| 39 - (UIPasteboard*)findPasteboard; |
| 40 // Find in Page text field listeners. |
| 41 - (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note; |
| 42 - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note; |
| 43 // Keyboard listeners. |
| 44 - (void)keyboardDidShow:(NSNotification*)note; |
| 45 - (void)keyboardWillHide:(NSNotification*)note; |
| 46 // Constantly injects the find string in page until |
| 47 // |disableFindInPageWithCompletionHandler:| is called or the find operation is |
| 48 // complete. Calls |completionHandler| if the find operation is complete. |
| 49 // |completionHandler| can be nil. |
| 50 - (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler; |
| 51 // Gives find in page more time to complete. Calls |completionHandler| with |
| 52 // a BOOL indicating if the find operation was successfull. |completionHandler| |
| 53 // can be nil. |
| 54 - (void)pumpFindStringInPageWithCompletionHandler: |
| 55 (void (^)(BOOL))completionHandler; |
| 56 // Processes the result of a single find in page pump. Calls |completionHandler| |
| 57 // if pumping is done. Re-pumps if necessary. |
| 58 - (void)processPumpResult:(BOOL)finished |
| 59 scrollPoint:(CGPoint)scrollPoint |
| 60 completionHandler:(ProceduralBlock)completionHandler; |
| 61 // Returns the associated web state. May be null. |
| 62 - (web::WebState*)webState; |
| 63 @end |
| 64 |
| 65 @implementation FindInPageController { |
| 66 @private |
| 67 // Object that manages find_in_page.js injection into the web view. |
| 68 JsFindinpageManager* _findInPageJsManager; |
| 69 id<FindInPageControllerDelegate> _delegate; |
| 70 |
| 71 // Access to the web view from the web state. |
| 72 base::scoped_nsprotocol<id<CRWWebViewProxy>> _webViewProxy; |
| 73 |
| 74 // True when a find is in progress. Used to avoid running JavaScript during |
| 75 // disable when there is nothing to clear. |
| 76 BOOL _findStringStarted; |
| 77 |
| 78 // Bridge to observe the web state from Objective-C. |
| 79 scoped_ptr<web::WebStateObserverBridge> _webStateObserverBridge; |
| 80 } |
| 81 |
| 82 @synthesize delegate = _delegate; |
| 83 |
| 84 - (id)initWithWebState:(web::WebState*)webState |
| 85 delegate:(id<FindInPageControllerDelegate>)delegate { |
| 86 self = [super init]; |
| 87 if (self) { |
| 88 DCHECK(delegate); |
| 89 _findInPageJsManager = base::mac::ObjCCastStrict<JsFindinpageManager>( |
| 90 [webState->GetJSInjectionReceiver() |
| 91 instanceOfClass:[JsFindinpageManager class]]); |
| 92 _delegate = delegate; |
| 93 _webStateObserverBridge.reset( |
| 94 new web::WebStateObserverBridge(webState, self)); |
| 95 _webViewProxy.reset([webState->GetWebViewProxy() retain]); |
| 96 [[NSNotificationCenter defaultCenter] |
| 97 addObserver:self |
| 98 selector:@selector(findBarTextFieldWillBecomeFirstResponder:) |
| 99 name:kFindBarTextFieldWillBecomeFirstResponderNotification |
| 100 object:nil]; |
| 101 [[NSNotificationCenter defaultCenter] |
| 102 addObserver:self |
| 103 selector:@selector(findBarTextFieldDidResignFirstResponder:) |
| 104 name:kFindBarTextFieldDidResignFirstResponderNotification |
| 105 object:nil]; |
| 106 DOMAlteringLock::CreateForWebState(webState); |
| 107 } |
| 108 return self; |
| 109 } |
| 110 |
| 111 - (void)dealloc { |
| 112 [[NSNotificationCenter defaultCenter] removeObserver:self]; |
| 113 [super dealloc]; |
| 114 } |
| 115 |
| 116 - (FindInPageModel*)findInPageModel { |
| 117 return [_findInPageJsManager findInPageModel]; |
| 118 } |
| 119 |
| 120 - (BOOL)canFindInPage { |
| 121 return [_webViewProxy hasSearchableTextContent]; |
| 122 } |
| 123 |
| 124 - (void)initFindInPage { |
| 125 [_findInPageJsManager inject]; |
| 126 |
| 127 // Initialize the module with our frame size. |
| 128 CGRect frame = [_webViewProxy bounds]; |
| 129 [_findInPageJsManager setWidth:frame.size.width height:frame.size.height]; |
| 130 } |
| 131 |
| 132 - (CRWWebViewScrollViewProxy*)webViewScrollView { |
| 133 return [_webViewProxy scrollViewProxy]; |
| 134 } |
| 135 |
| 136 - (void)processPumpResult:(BOOL)finished |
| 137 scrollPoint:(CGPoint)scrollPoint |
| 138 completionHandler:(ProceduralBlock)completionHandler { |
| 139 if (finished) { |
| 140 [_delegate willAdjustScrollPosition]; |
| 141 [[_webViewProxy scrollViewProxy] setContentOffset:scrollPoint animated:YES]; |
| 142 if (completionHandler) |
| 143 completionHandler(); |
| 144 } else { |
| 145 [self performSelector:@selector(startPumpingWithCompletionHandler:) |
| 146 withObject:completionHandler |
| 147 afterDelay:kRecurringPumpDelay]; |
| 148 } |
| 149 } |
| 150 |
| 151 - (void)findStringInPage:(NSString*)query |
| 152 completionHandler:(ProceduralBlock)completionHandler { |
| 153 ProceduralBlockWithBool lockAction = ^(BOOL hasLock) { |
| 154 if (!hasLock) { |
| 155 if (completionHandler) { |
| 156 completionHandler(); |
| 157 } |
| 158 return; |
| 159 } |
| 160 // Cancel any previous pumping. |
| 161 [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| 162 [self initFindInPage]; |
| 163 // Keep track of whether a find is in progress so to avoid running |
| 164 // JavaScript during disable if unnecessary. |
| 165 _findStringStarted = YES; |
| 166 base::WeakNSObject<FindInPageController> weakSelf(self); |
| 167 [_findInPageJsManager findString:query |
| 168 completionHandler:^(BOOL finished, CGPoint point) { |
| 169 [weakSelf processPumpResult:finished |
| 170 scrollPoint:point |
| 171 completionHandler:completionHandler]; |
| 172 }]; |
| 173 }; |
| 174 DOMAlteringLock::FromWebState([self webState])->Acquire(self, lockAction); |
| 175 } |
| 176 |
| 177 - (void)startPumpingWithCompletionHandler:(ProceduralBlock)completionHandler { |
| 178 base::WeakNSObject<FindInPageController> weakSelf(self); |
| 179 id completionHandlerBlock = ^void(BOOL findFinished) { |
| 180 if (findFinished) { |
| 181 // Pumping complete. Nothing else to do. |
| 182 if (completionHandler) |
| 183 completionHandler(); |
| 184 return; |
| 185 } |
| 186 // Further pumping is required. |
| 187 [weakSelf performSelector:@selector(startPumpingWithCompletionHandler:) |
| 188 withObject:completionHandler |
| 189 afterDelay:kRecurringPumpDelay]; |
| 190 }; |
| 191 [self pumpFindStringInPageWithCompletionHandler:completionHandlerBlock]; |
| 192 } |
| 193 |
| 194 - (void)pumpFindStringInPageWithCompletionHandler: |
| 195 (void (^)(BOOL))completionHandler { |
| 196 base::WeakNSObject<FindInPageController> weakSelf(self); |
| 197 [_findInPageJsManager pumpWithCompletionHandler:^(BOOL finished, |
| 198 CGPoint point) { |
| 199 base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]); |
| 200 if (finished) { |
| 201 [[strongSelf delegate] willAdjustScrollPosition]; |
| 202 [[strongSelf webViewScrollView] setContentOffset:point animated:YES]; |
| 203 } |
| 204 completionHandler(finished); |
| 205 }]; |
| 206 } |
| 207 |
| 208 - (void)findNextStringInPageWithCompletionHandler: |
| 209 (ProceduralBlock)completionHandler { |
| 210 [self initFindInPage]; |
| 211 base::WeakNSObject<FindInPageController> weakSelf(self); |
| 212 [_findInPageJsManager nextMatchWithCompletionHandler:^(CGPoint point) { |
| 213 base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]); |
| 214 [[strongSelf delegate] willAdjustScrollPosition]; |
| 215 CGFloat contentHeight = [strongSelf webViewScrollView].contentSize.height; |
| 216 CGFloat frameHeight = [strongSelf webViewScrollView].frame.size.height; |
| 217 CGFloat maxScroll = fmax(0, contentHeight - frameHeight); |
| 218 if (point.y > maxScroll) { |
| 219 point.y = maxScroll; |
| 220 } |
| 221 [[strongSelf webViewScrollView] setContentOffset:point animated:YES]; |
| 222 if (completionHandler) |
| 223 completionHandler(); |
| 224 }]; |
| 225 } |
| 226 |
| 227 // Highlight the previous search match, update model and scroll to match. |
| 228 - (void)findPreviousStringInPageWithCompletionHandler: |
| 229 (ProceduralBlock)completionHandler { |
| 230 [self initFindInPage]; |
| 231 base::WeakNSObject<FindInPageController> weakSelf(self); |
| 232 [_findInPageJsManager previousMatchWithCompletionHandler:^(CGPoint point) { |
| 233 base::scoped_nsobject<FindInPageController> strongSelf([weakSelf retain]); |
| 234 [[strongSelf delegate] willAdjustScrollPosition]; |
| 235 [[strongSelf webViewScrollView] setContentOffset:point animated:YES]; |
| 236 if (completionHandler) |
| 237 completionHandler(); |
| 238 }]; |
| 239 } |
| 240 |
| 241 // Remove highlights from the page and disable the model. |
| 242 - (void)disableFindInPageWithCompletionHandler: |
| 243 (ProceduralBlock)completionHandler { |
| 244 if (![self canFindInPage]) |
| 245 return; |
| 246 // Cancel any queued calls to |recurringPumpWithCompletionHandler|. |
| 247 [NSObject cancelPreviousPerformRequestsWithTarget:self]; |
| 248 base::WeakNSObject<FindInPageController> weakSelf(self); |
| 249 ProceduralBlock handler = ^{ |
| 250 web::WebState* webState = [self webState]; |
| 251 if (webState) |
| 252 DOMAlteringLock::FromWebState(webState)->Release(self); |
| 253 if (completionHandler) |
| 254 completionHandler(); |
| 255 }; |
| 256 // Only run JSFindInPageManager disable if there is a string in progress to |
| 257 // avoid WKWebView crash on deallocation due to outstanding completion |
| 258 // handler. |
| 259 if (_findStringStarted) { |
| 260 [_findInPageJsManager disableWithCompletionHandler:handler]; |
| 261 _findStringStarted = NO; |
| 262 } else { |
| 263 handler(); |
| 264 } |
| 265 } |
| 266 |
| 267 - (void)saveSearchTerm { |
| 268 [self findPasteboard].string = [[self findInPageModel] text]; |
| 269 } |
| 270 |
| 271 - (void)restoreSearchTerm { |
| 272 NSString* term = [self findPasteboard].string; |
| 273 [[self findInPageModel] updateQuery:(term ? term : @"") matches:0]; |
| 274 } |
| 275 |
| 276 - (UIPasteboard*)findPasteboard { |
| 277 return [UIPasteboard pasteboardWithName:UIPasteboardNameFind create:NO]; |
| 278 } |
| 279 |
| 280 - (web::WebState*)webState { |
| 281 return _webStateObserverBridge ? _webStateObserverBridge->web_state() |
| 282 : nullptr; |
| 283 } |
| 284 |
| 285 #pragma mark - Notification listeners |
| 286 |
| 287 - (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note { |
| 288 // Listen to the keyboard appearance notifications. |
| 289 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; |
| 290 [defaultCenter addObserver:self |
| 291 selector:@selector(keyboardDidShow:) |
| 292 name:UIKeyboardDidShowNotification |
| 293 object:nil]; |
| 294 [defaultCenter addObserver:self |
| 295 selector:@selector(keyboardWillHide:) |
| 296 name:UIKeyboardWillHideNotification |
| 297 object:nil]; |
| 298 } |
| 299 |
| 300 - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note { |
| 301 // Resign from the keyboard appearance notifications on the next turn of the |
| 302 // runloop. |
| 303 dispatch_async(dispatch_get_main_queue(), ^{ |
| 304 NSNotificationCenter* defaultCenter = [NSNotificationCenter defaultCenter]; |
| 305 [defaultCenter removeObserver:self |
| 306 name:UIKeyboardDidShowNotification |
| 307 object:nil]; |
| 308 [defaultCenter removeObserver:self |
| 309 name:UIKeyboardWillHideNotification |
| 310 object:nil]; |
| 311 }); |
| 312 } |
| 313 |
| 314 - (void)keyboardDidShow:(NSNotification*)note { |
| 315 NSDictionary* info = [note userInfo]; |
| 316 CGSize kbSize = |
| 317 [[info objectForKey:UIKeyboardFrameEndUserInfoKey] CGRectValue].size; |
| 318 UIInterfaceOrientation orientation = |
| 319 [[UIApplication sharedApplication] statusBarOrientation]; |
| 320 CGFloat kbHeight = kbSize.height; |
| 321 // Prior to iOS 8, the keyboard frame was not dependent on interface |
| 322 // orientation, so height and width need to be swapped in landscape mode. |
| 323 if (UIInterfaceOrientationIsLandscape(orientation) && |
| 324 !base::ios::IsRunningOnIOS8OrLater()) { |
| 325 kbHeight = kbSize.width; |
| 326 } |
| 327 |
| 328 UIEdgeInsets insets = UIEdgeInsetsZero; |
| 329 insets.bottom = kbHeight; |
| 330 [_webViewProxy registerInsets:insets forCaller:self]; |
| 331 } |
| 332 |
| 333 - (void)keyboardWillHide:(NSNotification*)note { |
| 334 [_webViewProxy unregisterInsetsForCaller:self]; |
| 335 } |
| 336 |
| 337 - (void)detachFromWebState { |
| 338 _webStateObserverBridge.reset(); |
| 339 } |
| 340 |
| 341 #pragma mark - CRWWebStateObserver Methods |
| 342 |
| 343 - (void)webStateDestroyed:(web::WebState*)webState { |
| 344 [self detachFromWebState]; |
| 345 } |
| 346 |
| 347 #pragma mark - DOMAltering Methods |
| 348 |
| 349 - (BOOL)canReleaseDOMLock { |
| 350 return NO; |
| 351 } |
| 352 |
| 353 - (void)releaseDOMLockWithCompletionHandler:(ProceduralBlock)completionHandler { |
| 354 NOTREACHED(); |
| 355 } |
| 356 |
| 357 @end |
OLD | NEW |