| // Copyright 2018 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/ui/omnibox/omnibox_coordinator.h" |
| |
| #import "base/check.h" |
| #import "base/ios/ios_util.h" |
| #import "base/metrics/user_metrics.h" |
| #import "base/metrics/user_metrics_action.h" |
| #import "base/strings/sys_string_conversions.h" |
| #import "components/feature_engagement/public/tracker.h" |
| #import "components/omnibox/browser/omnibox_controller.h" |
| #import "components/omnibox/browser/omnibox_edit_model.h" |
| #import "components/omnibox/common/omnibox_features.h" |
| #import "components/omnibox/common/omnibox_focus_state.h" |
| #import "components/open_from_clipboard/clipboard_recent_content.h" |
| #import "components/search_engines/template_url_service.h" |
| #import "components/strings/grit/components_strings.h" |
| #import "ios/chrome/browser/favicon/model/ios_chrome_favicon_loader_factory.h" |
| #import "ios/chrome/browser/feature_engagement/model/tracker_factory.h" |
| #import "ios/chrome/browser/location_bar/ui_bundled/location_bar_constants.h" |
| #import "ios/chrome/browser/search_engines/model/template_url_service_factory.h" |
| #import "ios/chrome/browser/shared/coordinator/layout_guide/layout_guide_util.h" |
| #import "ios/chrome/browser/shared/model/browser/browser.h" |
| #import "ios/chrome/browser/shared/model/profile/profile_ios.h" |
| #import "ios/chrome/browser/shared/public/commands/application_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/command_dispatcher.h" |
| #import "ios/chrome/browser/shared/public/commands/help_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/lens_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/load_query_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/omnibox_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/qr_scanner_commands.h" |
| #import "ios/chrome/browser/shared/public/commands/toolbar_commands.h" |
| #import "ios/chrome/browser/shared/public/features/features.h" |
| #import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_assistive_keyboard_delegate.h" |
| #import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_assistive_keyboard_mediator.h" |
| #import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_assistive_keyboard_views.h" |
| #import "ios/chrome/browser/ui/omnibox/keyboard_assist/omnibox_keyboard_accessory_view.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_mediator.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_return_key_forwarding_delegate.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_text_field_ios.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_text_field_paste_delegate.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_util.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_view_controller.h" |
| #import "ios/chrome/browser/ui/omnibox/omnibox_view_ios.h" |
| #import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_coordinator.h" |
| #import "ios/chrome/browser/ui/omnibox/popup/omnibox_popup_view_ios.h" |
| #import "ios/chrome/browser/ui/omnibox/popup/pedal_section_extractor.h" |
| #import "ios/chrome/browser/ui/omnibox/text_field_view_containing.h" |
| #import "ios/chrome/browser/ui/omnibox/zero_suggest_prefetch_helper.h" |
| #import "ios/chrome/browser/url_loading/model/image_search_param_generator.h" |
| #import "ios/chrome/browser/url_loading/model/url_loading_browser_agent.h" |
| #import "ios/chrome/browser/url_loading/model/url_loading_params.h" |
| #import "ios/web/public/navigation/navigation_manager.h" |
| |
| @interface OmniboxCoordinator () <OmniboxViewControllerTextInputDelegate, |
| OmniboxAssistiveKeyboardMediatorDelegate> |
| |
| // View controller managed by this coordinator. |
| @property(nonatomic, strong) OmniboxViewController* viewController; |
| |
| // The mediator for the omnibox. |
| @property(nonatomic, strong) OmniboxMediator* mediator; |
| |
| // The paste delegate for the omnibox that prevents multipasting. |
| @property(nonatomic, strong) OmniboxTextFieldPasteDelegate* pasteDelegate; |
| |
| // The return delegate. |
| @property(nonatomic, strong) ForwardingReturnDelegate* returnDelegate; |
| |
| // Helper that starts ZPS prefetch when the user opens a NTP. |
| @property(nonatomic, strong) |
| ZeroSuggestPrefetchHelper* zeroSuggestPrefetchHelper; |
| |
| // The keyboard accessory view. Will be nil if the app is running on an iPad. |
| @property(nonatomic, strong) |
| OmniboxKeyboardAccessoryView* keyboardAccessoryView; |
| |
| // Redefined as readwrite. |
| @property(nonatomic, strong) OmniboxPopupCoordinator* popupCoordinator; |
| |
| @end |
| |
| @implementation OmniboxCoordinator { |
| // TODO(crbug.com/40565663): use a slimmer subclass of OmniboxView, |
| // OmniboxPopupViewSuggestionsDelegate instead of OmniboxViewIOS. |
| std::unique_ptr<OmniboxViewIOS> _editView; |
| |
| // Omnibox client. Stored between init and start, then ownership is passed to |
| // OmniboxView. |
| std::unique_ptr<OmniboxClient> _client; |
| |
| /// Object handling interactions in the keyboard accessory view. |
| OmniboxAssistiveKeyboardMediator* _keyboardMediator; |
| |
| // The handler for ToolbarCommands. |
| id<ToolbarCommands> _toolbarHandler; |
| } |
| @synthesize viewController = _viewController; |
| @synthesize mediator = _mediator; |
| |
| #pragma mark - public |
| |
| - (instancetype)initWithBaseViewController:(UIViewController*)viewController |
| browser:(Browser*)browser |
| omniboxClient: |
| (std::unique_ptr<OmniboxClient>)client { |
| self = [super initWithBaseViewController:viewController browser:browser]; |
| if (self) { |
| _client = std::move(client); |
| } |
| return self; |
| } |
| |
| - (void)start { |
| DCHECK(!self.popupCoordinator); |
| |
| _toolbarHandler = |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), ToolbarCommands); |
| |
| self.viewController = [[OmniboxViewController alloc] init]; |
| self.viewController.defaultLeadingImage = |
| GetOmniboxSuggestionIcon(OmniboxSuggestionIconType::kDefaultFavicon); |
| self.viewController.textInputDelegate = self; |
| self.viewController.layoutGuideCenter = |
| LayoutGuideCenterForBrowser(self.browser); |
| |
| BOOL isIncognito = self.browser->GetBrowserState()->IsOffTheRecord(); |
| self.mediator = [[OmniboxMediator alloc] |
| initWithIncognito:isIncognito |
| tracker:feature_engagement::TrackerFactory::GetForBrowserState( |
| self.browser->GetBrowserState())]; |
| |
| TemplateURLService* templateURLService = |
| ios::TemplateURLServiceFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| self.mediator.templateURLService = templateURLService; |
| self.mediator.faviconLoader = |
| IOSChromeFaviconLoaderFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| self.mediator.consumer = self.viewController; |
| self.mediator.omniboxCommandsHandler = |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), OmniboxCommands); |
| self.mediator.lensCommandsHandler = |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), LensCommands); |
| self.mediator.loadQueryCommandsHandler = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), LoadQueryCommands); |
| self.mediator.sceneState = self.browser->GetSceneState(); |
| self.mediator.URLLoadingBrowserAgent = |
| UrlLoadingBrowserAgent::FromBrowser(self.browser); |
| self.viewController.pasteDelegate = self.mediator; |
| |
| DCHECK(_client.get()); |
| |
| id<OmniboxCommands> omniboxHandler = |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), OmniboxCommands); |
| _editView = std::make_unique<OmniboxViewIOS>( |
| self.textField, std::move(_client), self.browser->GetBrowserState(), |
| omniboxHandler, self.focusDelegate, _toolbarHandler, self.viewController); |
| self.pasteDelegate = [[OmniboxTextFieldPasteDelegate alloc] init]; |
| [self.textField setPasteDelegate:self.pasteDelegate]; |
| |
| self.viewController.textChangeDelegate = _editView.get(); |
| |
| _keyboardMediator = [[OmniboxAssistiveKeyboardMediator alloc] init]; |
| _keyboardMediator.applicationCommandsHandler = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), ApplicationCommands); |
| _keyboardMediator.lensCommandsHandler = |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), LensCommands); |
| _keyboardMediator.qrScannerCommandsHandler = HandlerForProtocol( |
| self.browser->GetCommandDispatcher(), QRScannerCommands); |
| _keyboardMediator.layoutGuideCenter = |
| LayoutGuideCenterForBrowser(self.browser); |
| // TODO(crbug.com/40670043): Use HandlerForProtocol after commands protocol |
| // clean up. |
| _keyboardMediator.browserCoordinatorCommandsHandler = |
| static_cast<id<BrowserCoordinatorCommands>>( |
| self.browser->GetCommandDispatcher()); |
| _keyboardMediator.omniboxTextField = self.textField; |
| _keyboardMediator.delegate = self; |
| |
| if (base::FeatureList::IsEnabled(omnibox::kZeroSuggestPrefetching)) { |
| self.zeroSuggestPrefetchHelper = [[ZeroSuggestPrefetchHelper alloc] |
| initWithWebStateList:self.browser->GetWebStateList() |
| controller:_editView->controller()]; |
| } |
| |
| self.popupCoordinator = [self createPopupCoordinator:self.presenterDelegate]; |
| [self.popupCoordinator start]; |
| } |
| |
| - (void)stop { |
| [self.popupCoordinator stop]; |
| self.popupCoordinator = nil; |
| |
| self.viewController.textChangeDelegate = nil; |
| self.returnDelegate.acceptDelegate = nil; |
| _editView.reset(); |
| self.viewController = nil; |
| self.mediator.templateURLService = nullptr; // Unregister the observer. |
| if (self.keyboardAccessoryView) { |
| // Unregister the observer. |
| self.keyboardAccessoryView.templateURLService = nil; |
| } |
| |
| _keyboardMediator = nil; |
| self.keyboardAccessoryView = nil; |
| self.mediator = nil; |
| self.returnDelegate = nil; |
| self.zeroSuggestPrefetchHelper = nil; |
| |
| [NSNotificationCenter.defaultCenter removeObserver:self]; |
| } |
| |
| - (void)updateOmniboxState { |
| _editView->UpdateAppearance(); |
| } |
| |
| - (BOOL)isOmniboxFirstResponder { |
| return [self.textField isFirstResponder]; |
| } |
| |
| - (void)focusOmnibox { |
| if (!self.keyboardAccessoryView) { |
| TemplateURLService* templateURLService = |
| ios::TemplateURLServiceFactory::GetForBrowserState( |
| self.browser->GetBrowserState()); |
| self.keyboardAccessoryView = ConfigureAssistiveKeyboardViews( |
| self.textField, kDotComTLD, _keyboardMediator, templateURLService, |
| HandlerForProtocol(self.browser->GetCommandDispatcher(), HelpCommands)); |
| } |
| |
| if (![self.textField isFirstResponder]) { |
| base::RecordAction(base::UserMetricsAction("MobileOmniboxFocused")); |
| |
| // In multiwindow context, -becomeFirstRepsonder is not enough to get the |
| // keyboard input. The window will not automatically become key. Make it key |
| // manually. UITextField does this under the hood when tapped from |
| // -[UITextInteractionAssistant(UITextInteractionAssistant_Internal) |
| // setFirstResponderIfNecessaryActivatingSelection:] |
| if (base::ios::IsMultipleScenesSupported()) { |
| [self.textField.window makeKeyAndVisible]; |
| } |
| |
| [self.textField becomeFirstResponder]; |
| // Ensures that the accessibility system focuses the text field instead of |
| // the popup crbug.com/1469173. |
| UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, |
| self.textField); |
| } |
| } |
| |
| - (void)endEditing { |
| [self.textField resignFirstResponder]; |
| _editView->EndEditing(); |
| } |
| |
| - (void)insertTextToOmnibox:(NSString*)text { |
| [self.textField insertTextWhileEditing:text]; |
| // The call to `setText` shouldn't be needed, but without it the "Go" button |
| // of the keyboard is disabled. |
| [self.textField setText:text]; |
| // Notify the accessibility system to start reading the new contents of the |
| // Omnibox. |
| UIAccessibilityPostNotification(UIAccessibilityScreenChangedNotification, |
| self.textField); |
| } |
| |
| - (OmniboxPopupCoordinator*)createPopupCoordinator: |
| (id<OmniboxPopupPresenterDelegate>)presenterDelegate { |
| DCHECK(!_popupCoordinator); |
| std::unique_ptr<OmniboxPopupViewIOS> popupView = |
| std::make_unique<OmniboxPopupViewIOS>(_editView->controller(), |
| _editView.get()); |
| |
| _editView->SetPopupProvider(popupView.get()); |
| |
| OmniboxPopupCoordinator* coordinator = [[OmniboxPopupCoordinator alloc] |
| initWithBaseViewController:nil |
| browser:self.browser |
| autocompleteController:_editView->controller() |
| ->autocomplete_controller() |
| popupView:std::move(popupView)]; |
| coordinator.presenterDelegate = presenterDelegate; |
| |
| self.returnDelegate = [[ForwardingReturnDelegate alloc] init]; |
| self.returnDelegate.acceptDelegate = _editView.get(); |
| |
| coordinator.popupMatchPreviewDelegate = self.mediator; |
| coordinator.acceptReturnDelegate = self.returnDelegate; |
| self.viewController.returnKeyDelegate = coordinator.popupReturnDelegate; |
| self.viewController.popupKeyboardDelegate = coordinator.KeyboardDelegate; |
| |
| _popupCoordinator = coordinator; |
| return coordinator; |
| } |
| |
| - (UIViewController*)managedViewController { |
| return self.viewController; |
| } |
| |
| - (id<LocationBarOffsetProvider>)offsetProvider { |
| return self.viewController; |
| } |
| |
| - (id<EditViewAnimatee>)animatee { |
| return self.viewController; |
| } |
| |
| - (id<ToolbarOmniboxConsumer>)toolbarOmniboxConsumer { |
| return self.popupCoordinator.toolbarOmniboxConsumer; |
| } |
| |
| - (UIView<TextFieldViewContaining>*)editView { |
| return self.viewController.viewContainingTextField; |
| } |
| |
| - (void)setThumbnailImage:(UIImage*)image { |
| if (_editView) { |
| _editView->SetThumbnailImage(image); |
| } |
| } |
| |
| #pragma mark Scribble |
| |
| - (void)focusOmniboxForScribble { |
| [self.textField becomeFirstResponder]; |
| [self.viewController prepareOmniboxForScribble]; |
| } |
| |
| - (UIResponder<UITextInput>*)scribbleInput { |
| return self.viewController.textField; |
| } |
| |
| #pragma mark - OmniboxAssistiveKeyboardMediatorDelegate |
| |
| - (void)omniboxAssistiveKeyboardDidTapDebuggerButton { |
| [self.popupCoordinator toggleOmniboxDebuggerView]; |
| } |
| |
| #pragma mark - OmniboxViewControllerTextInputDelegate |
| |
| - (void)omniboxViewControllerTextInputModeDidChange: |
| (OmniboxViewController*)omniboxViewController { |
| _editView->UpdatePopupAppearance(); |
| } |
| |
| #pragma mark - private |
| |
| // Convenience accessor. |
| - (OmniboxTextFieldIOS*)textField { |
| return self.viewController.textField; |
| } |
| |
| @end |