| // Copyright 2012 The Chromium Authors |
| // 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/java_script_find_in_page_controller.h" |
| |
| #import <UIKit/UIKit.h> |
| |
| #import <cmath> |
| #import <memory> |
| |
| #import "base/apple/foundation_util.h" |
| #import "base/check_op.h" |
| #import "base/notreached.h" |
| #import "components/ukm/ios/ukm_url_recorder.h" |
| #import "ios/chrome/browser/find_in_page/constants.h" |
| #import "ios/chrome/browser/find_in_page/find_in_page_model.h" |
| #import "ios/chrome/browser/find_in_page/find_in_page_response_delegate.h" |
| #import "ios/web/public/find_in_page/find_in_page_manager_delegate_bridge.h" |
| #import "ios/web/public/find_in_page/java_script_find_in_page_manager.h" |
| #import "ios/web/public/ui/crw_web_view_proxy.h" |
| #import "ios/web/public/ui/crw_web_view_scroll_view_proxy.h" |
| #import "ios/web/public/web_state.h" |
| #import "services/metrics/public/cpp/ukm_builders.h" |
| |
| namespace { |
| // Keeps find in page search term to be shared between different tabs. Never |
| // reset, not stored on disk. |
| static NSString* gSearchTerm; |
| |
| // Accessibility announcement delay, so VoiceOver does not cancel the context |
| // string announcement when a new match has been selected. |
| // TODO(crbug.com/1395828): This is a temporary workaround. The context string |
| // announcement might still fail. A retry mechanism needs to be implemented. |
| const int64_t kContextStringAnnouncementDelayInNanoseconds = 0.1 * NSEC_PER_SEC; |
| } // namespace |
| |
| @interface JavaScriptFindInPageController () <CRWFindInPageManagerDelegate> |
| |
| // The web view's scroll view. |
| - (CRWWebViewScrollViewProxy*)webViewScrollView; |
| // Find in Page text field listeners. |
| - (void)findBarTextFieldWillBecomeFirstResponder:(NSNotification*)note; |
| - (void)findBarTextFieldDidResignFirstResponder:(NSNotification*)note; |
| // Keyboard listeners. |
| - (void)keyboardDidShow:(NSNotification*)note; |
| - (void)keyboardWillHide:(NSNotification*)note; |
| // Records UKM metric for Find in Page search matches. |
| - (void)logFindInPageSearchUKM; |
| // Prevent scrolling past the end of the page. |
| - (CGPoint)limitOverscroll:(CRWWebViewScrollViewProxy*)scrollViewProxy |
| atPoint:(CGPoint)point; |
| @end |
| |
| @implementation JavaScriptFindInPageController { |
| // Object that manages searches and match traversals. |
| web::JavaScriptFindInPageManager* _findInPageManager; |
| |
| // Access to the web view from the web state. |
| 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; |
| |
| // The WebState this instance is observing. Will be null after |
| // -webStateDestroyed: has been called. |
| web::WebState* _webState; |
| |
| // Bridge to observe FindInPageManager from Objective-C. |
| std::unique_ptr<web::FindInPageManagerDelegateBridge> |
| _findInPageDelegateBridge; |
| } |
| |
| @synthesize findInPageModel = _findInPageModel; |
| |
| + (void)setSearchTerm:(NSString*)string { |
| gSearchTerm = [string copy]; |
| } |
| |
| + (NSString*)searchTerm { |
| return gSearchTerm; |
| } |
| |
| - (id)initWithWebState:(web::WebState*)webState { |
| self = [super init]; |
| if (self) { |
| DCHECK(webState); |
| DCHECK(webState->IsRealized()); |
| |
| _webState = webState; |
| _findInPageModel = [[FindInPageModel alloc] init]; |
| _findInPageDelegateBridge = |
| std::make_unique<web::FindInPageManagerDelegateBridge>(self); |
| _findInPageManager = |
| web::JavaScriptFindInPageManager::FromWebState(_webState); |
| _findInPageManager->SetDelegate(_findInPageDelegateBridge.get()); |
| |
| _webViewProxy = _webState->GetWebViewProxy(); |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(findBarTextFieldWillBecomeFirstResponder:) |
| name:kFindBarTextFieldWillBecomeFirstResponderNotification |
| object:nil]; |
| [[NSNotificationCenter defaultCenter] |
| addObserver:self |
| selector:@selector(findBarTextFieldDidResignFirstResponder:) |
| name:kFindBarTextFieldDidResignFirstResponderNotification |
| object:nil]; |
| } |
| return self; |
| } |
| |
| - (void)dealloc { |
| DCHECK(!_webState) << "-detachFromWebState must be called before -dealloc"; |
| } |
| |
| - (BOOL)canFindInPage { |
| return _findInPageManager->CanSearchContent(); |
| } |
| |
| - (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)logFindInPageSearchUKM { |
| ukm::SourceId sourceID = ukm::GetSourceIdForWebStateDocument(_webState); |
| if (sourceID != ukm::kInvalidSourceId) { |
| ukm::builders::IOS_FindInPageSearchMatches(sourceID) |
| .SetHasMatches(_findInPageModel.matches > 0) |
| .Record(ukm::UkmRecorder::Get()); |
| } |
| } |
| |
| - (void)findStringInPage:(NSString*)query { |
| // Keep track of whether a find is in progress so to avoid running |
| // JavaScript during disable if unnecessary. |
| _findStringStarted = YES; |
| // Save the query in the model before searching. TODO:(crbug.com/963908): |
| // Remove as part of refactoring. |
| [self.findInPageModel updateQuery:query matches:0]; |
| _findInPageManager->Find(query, web::FindInPageOptions::FindInPageSearch); |
| } |
| |
| - (void)findNextStringInPage { |
| _findInPageManager->Find(nil, web::FindInPageOptions::FindInPageNext); |
| } |
| |
| // Highlight the previous search match, update model and scroll to match. |
| - (void)findPreviousStringInPage { |
| _findInPageManager->Find(nil, web::FindInPageOptions::FindInPagePrevious); |
| } |
| |
| // Remove highlights from the page and disable the model. |
| - (void)disableFindInPage { |
| if (![self canFindInPage]) { |
| return; |
| } |
| |
| // Only run FindInPageManager::StopFinding() if there is a string in progress |
| // to avoid WKWebView crash on deallocation due to outstanding completion |
| // handler. |
| if (_findStringStarted) { |
| _findInPageManager->StopFinding(); |
| _findStringStarted = NO; |
| } |
| } |
| |
| - (void)saveSearchTerm { |
| [[self class] setSearchTerm:[[self findInPageModel] text]]; |
| } |
| |
| - (void)restoreSearchTerm { |
| // Pasteboards always return nil in background: |
| if ([[UIApplication sharedApplication] applicationState] != |
| UIApplicationStateActive) { |
| return; |
| } |
| |
| NSString* term = [[self class] searchTerm]; |
| [[self findInPageModel] updateQuery:(term ? term : @"") matches:0]; |
| } |
| |
| #pragma mark - CRWFindInPageManagerDelegate |
| |
| - (void)findInPageManager:(web::AbstractFindInPageManager*)manager |
| didHighlightMatchesOfQuery:(NSString*)query |
| withMatchCount:(NSInteger)matchCount |
| forWebState:(web::WebState*)webState { |
| if (matchCount == 0 && !query) { |
| // StopFinding responds with `matchCount` as 0 and `query` as nil. |
| [self.responseDelegate findDidStop]; |
| [self logFindInPageSearchUKM]; |
| return; |
| } |
| [self.findInPageModel updateQuery:query matches:matchCount]; |
| [self.responseDelegate findDidFinishWithUpdatedModel:self.findInPageModel]; |
| } |
| |
| - (void)findInPageManager:(web::AbstractFindInPageManager*)manager |
| didSelectMatchAtIndex:(NSInteger)index |
| withContextString:(NSString*)contextString |
| forWebState:(web::WebState*)webState { |
| if (contextString) { |
| // TODO(crbug.com/1395828): When tapping the Previous or Next button in the |
| // Find Bar, VoiceOver will trigger the announcement of the title of the |
| // button, usually a fraction of a second after this method is called. As a |
| // result, the announcement triggered by the |
| // `UIAccessibilityAnnouncementNotification` posted here will be interrupted |
| // by the announcement of the button. Setting a delay on posting the context |
| // string announcement notification yields the opposite result i.e. the |
| // expected result: VoiceOver will not read "Previous" or "Next", but read |
| // the new context string instead. This is a temporary workaround. The |
| // context string announcement might still fail. Some kind of retry |
| // mechanism needs to be implemented. |
| dispatch_after(dispatch_time(DISPATCH_TIME_NOW, |
| kContextStringAnnouncementDelayInNanoseconds), |
| dispatch_get_main_queue(), ^{ |
| UIAccessibilityPostNotification( |
| UIAccessibilityAnnouncementNotification, |
| contextString); |
| }); |
| } |
| // Increment index so that match number show in FindBar ranges from 1...N as |
| // opposed to 0...N-1. |
| index++; |
| [self.findInPageModel updateIndex:index atPoint:CGPointZero]; |
| [self.responseDelegate findDidFinishWithUpdatedModel:self.findInPageModel]; |
| } |
| |
| - (void)userDismissedFindNavigatorForManager: |
| (web::AbstractFindInPageManager*)manager { |
| // There should not be any Find navigator in JavaScript Find in Page. |
| NOTREACHED(); |
| } |
| |
| #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; |
| CGFloat kbHeight = kbSize.height; |
| UIEdgeInsets insets = UIEdgeInsetsZero; |
| insets.bottom = kbHeight; |
| [_webViewProxy registerInsets:insets forCaller:self]; |
| } |
| |
| - (void)keyboardWillHide:(NSNotification*)note { |
| [_webViewProxy unregisterInsetsForCaller:self]; |
| } |
| |
| - (void)detachFromWebState { |
| _findInPageManager->SetDelegate(nullptr); |
| _findInPageDelegateBridge.reset(); |
| _findInPageManager = nullptr; |
| _webState = nullptr; |
| } |
| |
| @end |