| // Copyright 2025 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/omnibox/model/omnibox_autocomplete_controller.h" |
| |
| #import <optional> |
| #import <string> |
| |
| #import "base/memory/raw_ptr.h" |
| #import "base/metrics/histogram_functions.h" |
| #import "base/metrics/user_metrics.h" |
| #import "base/metrics/user_metrics_action.h" |
| #import "base/trace_event/trace_event.h" |
| #import "components/bookmarks/browser/bookmark_model.h" |
| #import "components/omnibox/browser/autocomplete_controller.h" |
| #import "components/omnibox/browser/autocomplete_match.h" |
| #import "components/omnibox/browser/clipboard_provider.h" |
| #import "components/omnibox/browser/omnibox_client.h" |
| #import "components/omnibox/browser/omnibox_popup_selection.h" |
| #import "components/omnibox/browser/page_classification_functions.h" |
| #import "components/open_from_clipboard/clipboard_recent_content.h" |
| #import "ios/chrome/browser/omnibox/model/autocomplete_controller_observer_bridge.h" |
| #import "ios/chrome/browser/omnibox/model/autocomplete_result_wrapper.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_autocomplete_controller_debugger_delegate.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_autocomplete_controller_delegate.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_controller_ios.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_edit_model_ios.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_metrics_recorder.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_text_controller.h" |
| #import "ios/chrome/browser/omnibox/model/omnibox_text_model.h" |
| #import "ios/chrome/browser/shared/model/application_context/application_context.h" |
| #import "ios/chrome/browser/shared/model/prefs/pref_backed_boolean.h" |
| #import "ios/chrome/browser/shared/model/prefs/pref_names.h" |
| #import "third_party/omnibox_proto/groups.pb.h" |
| #import "ui/gfx/image/image.h" |
| #import "url/gurl.h" |
| |
| using base::UserMetricsAction; |
| |
| @interface OmniboxAutocompleteController () <AutocompleteControllerObserver, |
| BooleanObserver> |
| |
| /// Redefined as a readwrite |
| @property(nonatomic, assign, readwrite) BOOL hasSuggestions; |
| |
| /// Autocomplete controller is accessed from OmniboxController. It might be |
| /// changed by `SetAutocompleteControllerForTesting`. |
| @property(nonatomic, assign, readonly) |
| AutocompleteController* autocompleteController; |
| |
| @end |
| |
| @implementation OmniboxAutocompleteController { |
| /// Client of the omnibox. |
| raw_ptr<OmniboxClient> _omniboxClient; |
| /// Controller of the omnibox. |
| raw_ptr<OmniboxControllerIOS> _omniboxController; |
| /// Omnibox edit model. Should only be used for autocomplete interactions. |
| raw_ptr<OmniboxEditModelIOS> _omniboxEditModel; |
| /// Omnibox text model. |
| raw_ptr<OmniboxTextModel> _omniboxTextModel; |
| |
| /// Autocomplete controller observer. |
| std::unique_ptr<AutocompleteControllerObserverBridge> |
| _autocompleteControllerObserverBridge; |
| /// Pref tracking if the bottom omnibox is enabled. |
| PrefBackedBoolean* _bottomOmniboxEnabled; |
| /// Preferred omnibox position, logged in omnibox logs. |
| metrics::OmniboxEventProto::OmniboxPosition _preferredOmniboxPosition; |
| } |
| |
| - (instancetype)initWithOmniboxController: |
| (OmniboxControllerIOS*)omniboxController |
| omniboxClient:(OmniboxClient*)omniboxClient |
| omniboxEditModel:(OmniboxEditModelIOS*)omniboxEditModel |
| omniboxTextModel:(OmniboxTextModel*)omniboxTextModel { |
| self = [super init]; |
| if (self) { |
| _omniboxClient = omniboxClient; |
| _omniboxController = omniboxController; |
| _omniboxEditModel = omniboxEditModel; |
| _omniboxTextModel = omniboxTextModel; |
| |
| _autocompleteControllerObserverBridge = |
| std::make_unique<AutocompleteControllerObserverBridge>(self); |
| if (_omniboxController && _omniboxController->autocomplete_controller()) { |
| _omniboxController->autocomplete_controller()->AddObserver( |
| _autocompleteControllerObserverBridge.get()); |
| } |
| _preferredOmniboxPosition = metrics::OmniboxEventProto::UNKNOWN_POSITION; |
| _bottomOmniboxEnabled = [[PrefBackedBoolean alloc] |
| initWithPrefService:GetApplicationContext()->GetLocalState() |
| prefName:prefs::kBottomOmnibox]; |
| [_bottomOmniboxEnabled setObserver:self]; |
| // Initialize to the correct value. |
| [self booleanDidChange:_bottomOmniboxEnabled]; |
| } |
| return self; |
| } |
| |
| - (void)disconnect { |
| if (_autocompleteControllerObserverBridge && _omniboxController && |
| _omniboxController->autocomplete_controller()) { |
| _omniboxController->autocomplete_controller()->RemoveObserver( |
| _autocompleteControllerObserverBridge.get()); |
| _autocompleteControllerObserverBridge.reset(); |
| } |
| [self.autocompleteResultWrapper disconnect]; |
| [_bottomOmniboxEnabled stop]; |
| [_bottomOmniboxEnabled setObserver:nil]; |
| _bottomOmniboxEnabled = nil; |
| _autocompleteResultWrapper = nil; |
| _omniboxEditModel = nullptr; |
| _omniboxController = nullptr; |
| _omniboxTextModel = nullptr; |
| _omniboxClient = nullptr; |
| } |
| |
| - (AutocompleteController*)autocompleteController { |
| return _omniboxController ? _omniboxController->autocomplete_controller() |
| : nullptr; |
| } |
| |
| - (void)updatePopupSuggestions { |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| BOOL isFocusing = autocompleteController->input().focus_type() == |
| metrics::OmniboxFocusType::INTERACTION_FOCUS; |
| |
| self.hasSuggestions = !autocompleteController->result().empty(); |
| [self.delegate |
| omniboxAutocompleteControllerDidUpdateSuggestions:self |
| hasSuggestions:self.hasSuggestions |
| isFocusing:isFocusing]; |
| [self.debuggerDelegate omniboxAutocompleteController:self |
| didUpdateWithSuggestionsAvailable:self.hasSuggestions]; |
| } |
| } |
| |
| - (void)stopAutocompleteWithClearSuggestions:(BOOL)clearSuggestions { |
| TRACE_EVENT0("omnibox", "OmniboxAutocompleteController::StopAutocomplete"); |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| autocompleteController->Stop(clearSuggestions |
| ? AutocompleteStopReason::kClobbered |
| : AutocompleteStopReason::kInteraction); |
| } |
| } |
| |
| - (void)openSelection:(OmniboxPopupSelection)selection |
| timestamp:(base::TimeTicks)timestamp |
| disposition:(WindowOpenDisposition)disposition { |
| // Intentionally accept input when selection has no line. |
| // This will usually reach `OpenMatch` indirectly. |
| if (selection.line >= self.autocompleteController->result().size()) { |
| [self acceptInputWithDisposition:disposition timestamp:timestamp]; |
| return; |
| } |
| |
| const AutocompleteMatch& match = |
| self.autocompleteController->result().match_at(selection.line); |
| |
| // Open the match. |
| GURL alternate_nav_url = AutocompleteResult::ComputeAlternateNavUrl( |
| _omniboxTextModel->input, match, |
| self.autocompleteController->autocomplete_provider_client()); |
| _omniboxEditModel->OpenMatch(selection, match, disposition, alternate_nav_url, |
| std::u16string(), timestamp); |
| } |
| |
| - (void)openCurrentSelectionWithDisposition:(WindowOpenDisposition)disposition |
| timestamp:(base::TimeTicks)timestamp { |
| [self openSelection:OmniboxPopupSelection(OmniboxPopupSelection::kNoMatch, |
| OmniboxPopupSelection::NORMAL) |
| timestamp:timestamp |
| disposition:disposition]; |
| } |
| |
| #pragma mark - AutocompleteControllerObserver |
| |
| - (void)autocompleteController:(AutocompleteController*)autocompleteController |
| didUpdateResultChangingDefaultMatch:(BOOL)defaultMatchChanged { |
| TRACE_EVENT0("omnibox", "OmniboxAutocompleteController::OnResultChanged"); |
| DCHECK(autocompleteController == self.autocompleteController); |
| DCHECK(_omniboxClient); |
| |
| const bool popup_was_open = _omniboxEditModel->PopupIsOpen(); |
| |
| [self updatePopupSuggestions]; |
| if (defaultMatchChanged) { |
| // The default match has changed, we need to let the OmniboxEditModelIOS |
| // know about new inline autocomplete text (blue highlight). |
| if (const AutocompleteMatch* match = |
| autocompleteController->result().default_match()) { |
| // onPopupDataChanged resets text model's `current_match_` early |
| // on. Therefore, copy match.inline_autocompletion to a temp to preserve |
| // its value across the entire call. |
| [self.omniboxTextController |
| onPopupDataChanged:match->inline_autocompletion |
| additionalText:match->additional_text |
| newMatch:*match]; |
| } else { |
| [self.omniboxTextController onPopupDataChanged:std::u16string() |
| additionalText:std::u16string() |
| newMatch:AutocompleteMatch()]; |
| } |
| } |
| |
| const bool popup_is_open = _omniboxEditModel->PopupIsOpen(); |
| if (popup_was_open != popup_is_open && _omniboxClient) { |
| _omniboxClient->OnPopupVisibilityChanged(popup_is_open); |
| } |
| |
| if (popup_was_open && !popup_is_open) { |
| // Closing the popup can change the default suggestion. This usually occurs |
| // when it's unclear whether the input represents a search or URL; e.g., |
| // 'a.com/b c' or when title autocompleting. Clear the additional text to |
| // avoid suggesting the omnibox contains a URL suggestion when that may no |
| // longer be the case; i.e. when the default suggestion changed from a URL |
| // to a search suggestion upon closing the popup. |
| _omniboxEditModel->ClearAdditionalText(); |
| } |
| |
| // Note: The client outlives `this`, so bind a weak pointer to the callback |
| // passed in to eliminate the potential for crashes on shutdown. |
| // `should_preload` is set to `controller->done()` as prerender may only want |
| // to start preloading a result after all Autocomplete results are ready. |
| if (_omniboxClient) { |
| _omniboxClient->OnResultChanged( |
| autocompleteController->result(), defaultMatchChanged, |
| /*should_preload=*/autocompleteController->done(), |
| /*on_bitmap_fetched=*/base::DoNothing()); |
| } |
| } |
| |
| #pragma mark - AutocompleteResultWrapperDelegate |
| |
| - (void)autocompleteResultWrapper:(AutocompleteResultWrapper*)wrapper |
| didInvalidatePedals:(NSArray<id<AutocompleteSuggestionGroup>>*) |
| nonPedalSuggestionsGroups { |
| [self.delegate omniboxAutocompleteController:self |
| didUpdateSuggestionsGroups:nonPedalSuggestionsGroups]; |
| } |
| |
| #pragma mark - Boolean Observer |
| |
| - (void)booleanDidChange:(id<ObservableBoolean>)observableBoolean { |
| if (observableBoolean == _bottomOmniboxEnabled) { |
| _preferredOmniboxPosition = |
| _bottomOmniboxEnabled.value |
| ? metrics::OmniboxEventProto::BOTTOM_POSITION |
| : metrics::OmniboxEventProto::TOP_POSITION; |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| autocompleteController->SetSteadyStateOmniboxPosition( |
| _preferredOmniboxPosition); |
| } |
| } |
| } |
| |
| #pragma mark - OmniboxPopup event |
| |
| - (void)requestSuggestionsWithVisibleSuggestionCount: |
| (NSUInteger)visibleSuggestionCount { |
| AutocompleteController* autocompleteController = self.autocompleteController; |
| if (!autocompleteController) { |
| return; |
| } |
| size_t resultSize = autocompleteController->result().size(); |
| // If no suggestions are visible, consider all of them visible. |
| if (visibleSuggestionCount == 0) { |
| visibleSuggestionCount = resultSize; |
| } |
| NSUInteger visibleSuggestions = MIN(visibleSuggestionCount, resultSize); |
| if (visibleSuggestions > 0) { |
| // Groups visible suggestions by search vs url. Skip the first suggestion |
| // because it's the omnibox content. |
| autocompleteController->GroupSuggestionsBySearchVsURL(1, |
| visibleSuggestions); |
| } |
| // Groups hidden suggestions by search vs url. |
| if (visibleSuggestions < resultSize) { |
| autocompleteController->GroupSuggestionsBySearchVsURL(visibleSuggestions, |
| resultSize); |
| } |
| |
| [self updateWithSortedResults:autocompleteController->result()]; |
| } |
| |
| - (void)selectMatchForOpening:(const AutocompleteMatch&)match |
| inRow:(NSUInteger)row |
| openIn:(WindowOpenDisposition)disposition { |
| const auto matchSelectionTimestamp = base::TimeTicks(); |
| base::RecordAction(UserMetricsAction("MobileOmniboxUse")); |
| |
| if (match.type == AutocompleteMatchType::CLIPBOARD_URL) { |
| base::RecordAction(UserMetricsAction("MobileOmniboxClipboardToURL")); |
| base::UmaHistogramLongTimes100( |
| "MobileOmnibox.PressedClipboardSuggestionAge", |
| ClipboardRecentContent::GetInstance()->GetClipboardContentAge()); |
| } |
| |
| AutocompleteController* autocompleteController = self.autocompleteController; |
| if (!autocompleteController || !_omniboxEditModel) { |
| return; |
| } |
| |
| // Sometimes the match provided does not correspond to the autocomplete |
| // result match specified by `index`. Most Visited Tiles, for example, |
| // provide ad hoc matches that are not in the result at all. |
| if (row >= autocompleteController->result().size() || |
| autocompleteController->result().match_at(row).destination_url != |
| match.destination_url) { |
| [self openCustomMatch:match |
| disposition:disposition |
| selectionTimestamp:matchSelectionTimestamp]; |
| return; |
| } |
| |
| // Clipboard match handling. |
| if (match.destination_url.is_empty() && |
| AutocompleteMatch::IsClipboardType(match.type)) { |
| [self openClipboardMatch:match |
| disposition:disposition |
| selectionTimestamp:matchSelectionTimestamp]; |
| return; |
| } |
| |
| if (_omniboxEditModel) { |
| [self openSelection:OmniboxPopupSelection(row) |
| timestamp:matchSelectionTimestamp |
| disposition:disposition]; |
| } |
| } |
| |
| - (void)selectMatchForAppending:(const AutocompleteMatch&)match { |
| // Make a defensive copy of `match.fill_into_edit`, as CopyToOmnibox() will |
| // trigger a new round of autocomplete and modify `match`. |
| std::u16string fill_into_edit(match.fill_into_edit); |
| |
| // If the match is not a URL, append a whitespace to the end of it. |
| if (AutocompleteMatch::IsSearchType(match.type)) { |
| fill_into_edit.append(1, ' '); |
| } |
| |
| [self.omniboxTextController refineWithText:fill_into_edit]; |
| } |
| |
| - (void)selectMatchForDeletion:(const AutocompleteMatch&)match { |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| autocompleteController->DeleteMatch(match); |
| } |
| } |
| |
| - (void)onScroll { |
| [self.omniboxTextController onScroll]; |
| } |
| |
| - (void)onCallAction { |
| [self.omniboxTextController hideKeyboard]; |
| } |
| |
| #pragma mark - OmniboxText events |
| |
| - (void)startAutocompleteWithText:(const std::u16string&)text |
| cursorPosition:(size_t)cursorPosition |
| preventInlineAutocomplete:(bool)preventInlineAutocomplete { |
| if (!_omniboxClient || !_omniboxTextModel) { |
| return; |
| } |
| |
| // Use text_model()->input during the refactoring while the edit model is |
| // still using it crbug.com/390409559. |
| _omniboxTextModel->input = AutocompleteInput( |
| text, cursorPosition, |
| _omniboxClient->GetPageClassification(/*is_prefetch=*/false), |
| _omniboxClient->GetSchemeClassifier(), |
| _omniboxClient->ShouldDefaultTypedNavigationsToHttps(), |
| _omniboxClient->GetHttpsPortForTesting(), |
| _omniboxClient->IsUsingFakeHttpsForHttpsUpgradeTesting()); |
| AutocompleteInput& input = _omniboxTextModel->input; |
| input.set_current_url(_omniboxClient->GetURL()); |
| input.set_current_title(_omniboxClient->GetTitle()); |
| input.set_prevent_inline_autocomplete(preventInlineAutocomplete); |
| if (std::optional<lens::proto::LensOverlaySuggestInputs> suggestInputs = |
| _omniboxClient->GetLensOverlaySuggestInputs()) { |
| input.set_lens_overlay_suggest_inputs(*suggestInputs); |
| } |
| |
| [self startAutocompleteWithInput:input]; |
| } |
| |
| - (void)startZeroSuggestRequestWithText:(const std::u16string&)text |
| userClobbered:(BOOL)userClobberedPermanentText { |
| AutocompleteController* autocompleteController = self.autocompleteController; |
| if (!autocompleteController || !_omniboxClient || !_omniboxTextModel) { |
| return; |
| } |
| |
| // Early exit if a query is already in progress or the popup is already open. |
| // This is what allows this method to be called multiple times in multiple |
| // code locations without harm. |
| if (!autocompleteController->done() || self.hasSuggestions) { |
| return; |
| } |
| |
| // Early exit if the page has not loaded yet, so we don't annoy users. |
| if (!_omniboxClient->CurrentPageExists()) { |
| return; |
| } |
| |
| // Early exit if the user already has a navigation or search query in mind. |
| if (_omniboxTextModel->user_input_in_progress && |
| !userClobberedPermanentText) { |
| return; |
| } |
| |
| TRACE_EVENT0("omnibox", |
| "OmniboxTextController::startZeroSuggestRequestWithClobber"); |
| |
| // Send the textfield contents exactly as-is, as otherwise the verbatim |
| // match can be wrong. The full page URL is anyways in set_current_url(). |
| // Don't attempt to use https as the default scheme for these requests. |
| _omniboxTextModel->input = AutocompleteInput( |
| text, _omniboxClient->GetPageClassification(/*is_prefetch=*/false), |
| _omniboxClient->GetSchemeClassifier(), |
| /*should_use_https_as_default_scheme=*/false, |
| _omniboxClient->GetHttpsPortForTesting(), |
| _omniboxClient->IsUsingFakeHttpsForHttpsUpgradeTesting()); |
| AutocompleteInput& input = _omniboxTextModel->input; |
| input.set_current_url(_omniboxClient->GetURL()); |
| input.set_current_title(_omniboxClient->GetTitle()); |
| input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS); |
| // Set the lens overlay suggest inputs, if available. |
| if (std::optional<lens::proto::LensOverlaySuggestInputs> suggestInputs = |
| _omniboxClient->GetLensOverlaySuggestInputs()) { |
| input.set_lens_overlay_suggest_inputs(*suggestInputs); |
| } |
| [self startAutocompleteWithInput:input]; |
| } |
| |
| - (void)closeOmniboxPopup { |
| [self stopAutocompleteWithClearSuggestions:YES]; |
| } |
| |
| - (void)setTextAlignment:(NSTextAlignment)alignment { |
| [self.delegate omniboxAutocompleteController:self |
| didUpdateTextAlignment:alignment]; |
| } |
| |
| - (void)setSemanticContentAttribute: |
| (UISemanticContentAttribute)semanticContentAttribute { |
| [self.delegate omniboxAutocompleteController:self |
| didUpdateSemanticContentAttribute:semanticContentAttribute]; |
| } |
| |
| - (void)setHasThumbnail:(BOOL)hasThumbnail { |
| self.autocompleteResultWrapper.hasThumbnail = hasThumbnail; |
| } |
| |
| - (void)previewSuggestion:(id<AutocompleteSuggestion>)suggestion |
| isFirstUpdate:(BOOL)isFirstUpdate { |
| [self.omniboxTextController previewSuggestion:suggestion |
| isFirstUpdate:isFirstUpdate]; |
| } |
| |
| #pragma mark - Prefetch events |
| |
| - (void)startZeroSuggestPrefetch { |
| TRACE_EVENT0("omnibox", |
| "OmniboxAutocompleteController::StartZeroSuggestPrefetch"); |
| |
| AutocompleteController* autocompleteController = self.autocompleteController; |
| if (!autocompleteController || !_omniboxClient) { |
| return; |
| } |
| |
| auto page_classification = |
| _omniboxClient->GetPageClassification(/*is_prefetch=*/true); |
| GURL currentURL = _omniboxClient->GetURL(); |
| std::u16string text = base::UTF8ToUTF16(currentURL.spec()); |
| |
| if (omnibox::IsNTPPage(page_classification)) { |
| text.clear(); |
| } |
| |
| AutocompleteInput input(text, page_classification, |
| _omniboxClient->GetSchemeClassifier()); |
| input.set_current_url(currentURL); |
| input.set_focus_type(metrics::OmniboxFocusType::INTERACTION_FOCUS); |
| autocompleteController->StartPrefetch(input); |
| } |
| |
| - (void)setBackgroundStateForProviders:(BOOL)inBackground { |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| autocompleteController->autocomplete_provider_client() |
| ->set_in_background_state(inBackground); |
| } |
| } |
| |
| #pragma mark - Private |
| |
| /// Opens a match created outside of autocomplete controller. |
| - (void)openCustomMatch:(std::optional<AutocompleteMatch>)match |
| disposition:(WindowOpenDisposition)disposition |
| selectionTimestamp:(base::TimeTicks)timestamp { |
| AutocompleteController* autocompleteController = self.autocompleteController; |
| if (!autocompleteController || !_omniboxEditModel || !match) { |
| return; |
| } |
| OmniboxPopupSelection selection( |
| autocompleteController->InjectAdHocMatch(match.value())); |
| [self openSelection:selection timestamp:timestamp disposition:disposition]; |
| } |
| |
| /// Wraps the suggestions and send them to the delegate. |
| - (void)updateWithSortedResults:(const AutocompleteResult&)results { |
| NSArray<id<AutocompleteSuggestionGroup>>* suggestionGroups = |
| [self.autocompleteResultWrapper wrapAutocompleteResultInGroups:results]; |
| [self.delegate omniboxAutocompleteController:self |
| didUpdateSuggestionsGroups:suggestionGroups]; |
| } |
| |
| /// Starts autocomplete with `input`. |
| - (void)startAutocompleteWithInput:(const AutocompleteInput&)input { |
| TRACE_EVENT0("omnibox", "OmniboxAutocompleteController::StartAutocomplete"); |
| |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| autocompleteController->Start(input); |
| } |
| } |
| |
| // Asks the browser to load the popup's currently selected item, using the |
| // supplied disposition. This may close the popup. |
| - (void)acceptInputWithDisposition:(WindowOpenDisposition)disposition |
| timestamp:(base::TimeTicks)timestamp { |
| // Get the URL and transition type for the selected entry. |
| GURL alternate_nav_url; |
| AutocompleteMatch match = |
| [self.omniboxTextController currentMatch:&alternate_nav_url]; |
| |
| if (!match.destination_url.is_valid()) { |
| return; |
| } |
| |
| if (_omniboxTextModel->paste_state != OmniboxPasteState::kNone && |
| match.type == AutocompleteMatchType::URL_WHAT_YOU_TYPED) { |
| // When the user pasted in a URL and hit enter, score it like a link click |
| // rather than a normal typed URL, so it doesn't get inline autocompleted |
| // as aggressively later. |
| match.transition = ui::PAGE_TRANSITION_LINK; |
| } |
| |
| _omniboxEditModel->OpenMatch( |
| OmniboxPopupSelection(OmniboxPopupSelection::kNoMatch), match, |
| disposition, alternate_nav_url, std::u16string(), timestamp); |
| } |
| |
| #pragma mark Clipboard match handling |
| |
| /// Creates a match with the clipboard URL and open it. |
| - (void)openClipboardURL:(std::optional<GURL>)optionalURL |
| disposition:(WindowOpenDisposition)disposition |
| timestamp:(base::TimeTicks)timestamp { |
| if (!optionalURL) { |
| return; |
| } |
| GURL URL = std::move(optionalURL).value(); |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| [self openCustomMatch:autocompleteController->clipboard_provider() |
| ->NewClipboardURLMatch(URL) |
| disposition:disposition |
| selectionTimestamp:timestamp]; |
| } |
| } |
| |
| /// Creates a match with the clipboard text and open it. |
| - (void)openClipboardText:(std::optional<std::u16string>)optionalText |
| disposition:(WindowOpenDisposition)disposition |
| timestamp:(base::TimeTicks)timestamp { |
| if (!optionalText) { |
| return; |
| } |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| [self openCustomMatch:autocompleteController->clipboard_provider() |
| ->NewClipboardTextMatch(optionalText.value()) |
| disposition:disposition |
| selectionTimestamp:timestamp]; |
| } |
| } |
| |
| /// Creates a match with the clipboard image and open it. |
| - (void)openClipboardImage:(std::optional<gfx::Image>)optionalImage |
| disposition:(WindowOpenDisposition)disposition |
| timestamp:(base::TimeTicks)timestamp { |
| if (!optionalImage) { |
| return; |
| } |
| |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| __weak __typeof(self) weakSelf = self; |
| autocompleteController->clipboard_provider()->NewClipboardImageMatch( |
| optionalImage, |
| base::BindOnce( |
| [](OmniboxAutocompleteController* controller, |
| WindowOpenDisposition disposition, base::TimeTicks timestamp, |
| std::optional<AutocompleteMatch> optionalMatch) { |
| [controller openCustomMatch:optionalMatch |
| disposition:disposition |
| selectionTimestamp:timestamp]; |
| }, |
| weakSelf, disposition, timestamp)); |
| } |
| } |
| |
| /// Opens a clipboard match. Fetches the content of the clipboard and creates a |
| /// new match with it. |
| - (void)openClipboardMatch:(const AutocompleteMatch&)match |
| disposition:(WindowOpenDisposition)disposition |
| selectionTimestamp:(base::TimeTicks)timestamp { |
| __weak __typeof__(self) weakSelf = self; |
| ClipboardRecentContent* clipboardRecentContent = |
| ClipboardRecentContent::GetInstance(); |
| CHECK(clipboardRecentContent); |
| |
| switch (match.type) { |
| case AutocompleteMatchType::CLIPBOARD_URL: { |
| clipboardRecentContent->GetRecentURLFromClipboard(base::BindOnce( |
| [](OmniboxAutocompleteController* controller, |
| WindowOpenDisposition disposition, base::TimeTicks timestamp, |
| std::optional<GURL> optionalURL) { |
| [controller openClipboardURL:optionalURL |
| disposition:disposition |
| timestamp:timestamp]; |
| }, |
| weakSelf, disposition, timestamp)); |
| break; |
| } |
| case AutocompleteMatchType::CLIPBOARD_TEXT: { |
| clipboardRecentContent->GetRecentTextFromClipboard(base::BindOnce( |
| [](OmniboxAutocompleteController* controller, |
| WindowOpenDisposition disposition, base::TimeTicks timestamp, |
| std::optional<std::u16string> optionalText) { |
| [controller openClipboardText:optionalText |
| disposition:disposition |
| timestamp:timestamp]; |
| }, |
| weakSelf, disposition, timestamp)); |
| break; |
| } |
| case AutocompleteMatchType::CLIPBOARD_IMAGE: { |
| clipboardRecentContent->GetRecentImageFromClipboard(base::BindOnce( |
| [](OmniboxAutocompleteController* controller, |
| WindowOpenDisposition disposition, base::TimeTicks timestamp, |
| std::optional<gfx::Image> optionalImage) { |
| [controller openClipboardImage:optionalImage |
| disposition:disposition |
| timestamp:timestamp]; |
| }, |
| weakSelf, disposition, timestamp)); |
| break; |
| } |
| default: |
| NOTREACHED() << "Unsupported clipboard match type"; |
| } |
| } |
| |
| #pragma mark - Testing |
| |
| - (void)setAutocompleteController: |
| (std::unique_ptr<AutocompleteController>)controller { |
| CHECK(_autocompleteControllerObserverBridge); |
| |
| if (!_omniboxController) { |
| return; |
| } |
| |
| // Remove observation on old controller. |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| autocompleteController->RemoveObserver( |
| _autocompleteControllerObserverBridge.get()); |
| } |
| // Set new controller. |
| _omniboxController->SetAutocompleteControllerForTesting( |
| std::move(controller)); |
| // Observe new controller. |
| if (AutocompleteController* autocompleteController = |
| self.autocompleteController) { |
| autocompleteController->AddObserver( |
| _autocompleteControllerObserverBridge.get()); |
| |
| // Update the autocomplete controller in the metrics recorder. |
| [self.omniboxMetricsRecorder |
| setAutocompleteController:autocompleteController]; |
| } |
| } |
| |
| @end |