chromium/ios/chrome/browser/ui/omnibox/omnibox_view_ios.mm

// 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/ui/omnibox/omnibox_view_ios.h"

#import <CoreText/CoreText.h>
#import <UniformTypeIdentifiers/UniformTypeIdentifiers.h>

#import <string>

#import "base/command_line.h"
#import "base/ios/device_util.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/omnibox/browser/autocomplete_input.h"
#import "components/omnibox/browser/autocomplete_match.h"
#import "components/omnibox/browser/clipboard_provider.h"
#import "components/omnibox/browser/location_bar_model.h"
#import "components/omnibox/browser/omnibox_controller.h"
#import "components/omnibox/browser/omnibox_edit_model.h"
#import "components/omnibox/common/omnibox_focus_state.h"
#import "components/open_from_clipboard/clipboard_recent_content.h"
#import "ios/chrome/browser/autocomplete/model/autocomplete_scheme_classifier_impl.h"
#import "ios/chrome/browser/feature_engagement/model/tracker_factory.h"
#import "ios/chrome/browser/shared/model/profile/profile_ios.h"
#import "ios/chrome/browser/shared/public/commands/omnibox_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/shared/ui/util/pasteboard_util.h"
#import "ios/chrome/browser/shared/ui/util/uikit_ui_util.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_focus_delegate.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_metrics_helper.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_ui_features.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_util.h"
#import "ios/chrome/browser/ui/omnibox/omnibox_view_consumer.h"
#import "ios/chrome/grit/ios_strings.h"
#import "ios/chrome/grit/ios_theme_resources.h"
#import "ios/web/public/navigation/referrer.h"
#import "net/base/apple/url_conversions.h"
#import "ui/base/device_form_factor.h"
#import "ui/base/page_transition_types.h"
#import "ui/base/resource/resource_bundle.h"
#import "ui/base/window_open_disposition.h"
#import "ui/gfx/image/image.h"

using base::UserMetricsAction;

#pragma mark - OminboxViewIOS

OmniboxViewIOS::OmniboxViewIOS(OmniboxTextFieldIOS* field,
                               std::unique_ptr<OmniboxClient> client,
                               ChromeBrowserState* browser_state,
                               id<OmniboxCommands> omnibox_focuser,
                               id<OmniboxFocusDelegate> focus_delegate,
                               id<ToolbarCommands> toolbar_commands_handler,
                               id<OmniboxViewConsumer> consumer)
    : OmniboxView(std::move(client)),
      field_(field),
      omnibox_focuser_(omnibox_focuser),
      focus_delegate_(focus_delegate),
      toolbar_commands_handler_(toolbar_commands_handler),
      consumer_(consumer),
      ignore_popup_updates_(false),
      popup_provider_(nullptr) {
  DCHECK(field_);
}

OmniboxViewIOS::~OmniboxViewIOS() = default;

void OmniboxViewIOS::OnReceiveClipboardURLForOpenMatch(
    const AutocompleteMatch& match,
    WindowOpenDisposition disposition,
    const GURL& alternate_nav_url,
    const std::u16string& pasted_text,
    size_t selected_line,
    base::TimeTicks match_selection_timestamp,
    std::optional<GURL> optional_gurl) {
  if (!optional_gurl) {
    return;
  }

  GURL url = std::move(optional_gurl).value();

  AutocompleteController* autocomplete_controller =
      controller()->autocomplete_controller();

  AcceptThumbnailEdits();
  OmniboxPopupSelection selection(autocomplete_controller->InjectAdHocMatch(
      autocomplete_controller->clipboard_provider()->NewClipboardURLMatch(
          url)));
  model()->OpenSelection(selection, match_selection_timestamp, disposition);
}

void OmniboxViewIOS::OnReceiveClipboardTextForOpenMatch(
    const AutocompleteMatch& match,
    WindowOpenDisposition disposition,
    const GURL& alternate_nav_url,
    const std::u16string& pasted_text,
    size_t selected_line,
    base::TimeTicks match_selection_timestamp,
    std::optional<std::u16string> optional_text) {
  if (!optional_text) {
    return;
  }

  std::u16string text = std::move(optional_text).value();

  ClipboardProvider* clipboard_provider =
      controller()->autocomplete_controller()->clipboard_provider();
  std::optional<AutocompleteMatch> new_match =
      clipboard_provider->NewClipboardTextMatch(text);

  if (!new_match) {
    return;
  }

  AcceptThumbnailEdits();
  OmniboxPopupSelection selection(
      controller()->autocomplete_controller()->InjectAdHocMatch(
          new_match.value()));
  model()->OpenSelection(selection, match_selection_timestamp, disposition);
}

void OmniboxViewIOS::OnReceiveClipboardImageForOpenMatch(
    const AutocompleteMatch& match,
    WindowOpenDisposition disposition,
    const GURL& alternate_nav_url,
    const std::u16string& pasted_text,
    size_t selected_line,
    base::TimeTicks match_selection_timestamp,
    std::optional<gfx::Image> optional_image) {
  ClipboardProvider* clipboard_provider =
      controller()->autocomplete_controller()->clipboard_provider();
  clipboard_provider->NewClipboardImageMatch(
      optional_image,
      base::BindOnce(&OmniboxViewIOS::OnReceiveImageMatchForOpenMatch,
                     weak_ptr_factory_.GetWeakPtr(), disposition,
                     alternate_nav_url, pasted_text, selected_line,
                     match_selection_timestamp));
}

void OmniboxViewIOS::OnReceiveImageMatchForOpenMatch(
    WindowOpenDisposition disposition,
    const GURL& alternate_nav_url,
    const std::u16string& pasted_text,
    size_t selected_line,
    base::TimeTicks match_selection_timestamp,
    std::optional<AutocompleteMatch> optional_match) {
  if (!optional_match) {
    return;
  }
  AcceptThumbnailEdits();
  OmniboxPopupSelection selection(
      controller()->autocomplete_controller()->InjectAdHocMatch(
          optional_match.value()));
  model()->OpenSelection(selection, match_selection_timestamp, disposition);
}

std::u16string OmniboxViewIOS::GetText() const {
  return base::SysNSStringToUTF16([field_ displayedText]);
}

void OmniboxViewIOS::SetWindowTextAndCaretPos(const std::u16string& text,
                                              size_t caret_pos,
                                              bool update_popup,
                                              bool notify_text_changed) {
  // Do not call SetUserText() here, as the user has not triggered this change.
  // Instead, set the field's text directly.
  [field_ setText:base::SysUTF16ToNSString(text)];

  NSAttributedString* as = [[NSMutableAttributedString alloc]
      initWithString:base::SysUTF16ToNSString(text)];
  [field_ setText:as userTextLength:[as length]];

  if (update_popup)
    UpdatePopup();

  if (notify_text_changed && model())
    model()->OnChanged();

  SetCaretPos(caret_pos);
}

void OmniboxViewIOS::SetCaretPos(size_t caret_pos) {
  DCHECK(caret_pos <= field_.text.length || caret_pos == 0);
  UITextPosition* start = field_.beginningOfDocument;
  UITextPosition* newPosition =
      [field_ positionFromPosition:start offset:caret_pos];
  field_.selectedTextRange =
      [field_ textRangeFromPosition:newPosition toPosition:newPosition];
}

void OmniboxViewIOS::RevertAll() {
  ignore_popup_updates_ = true;
  OmniboxView::RevertAll();
  RevertThumbnailEdits();
  ignore_popup_updates_ = false;
}

void OmniboxViewIOS::UpdatePopup() {
  if (model())
    model()->SetInputInProgress(true);

  if (model() && !model()->has_focus())
    return;

  // Prevent inline-autocomplete if the IME is currently composing or if the
  // cursor is not at the end of the text.
  bool prevent_inline_autocomplete =
      IsImeComposing() ||
      NSMaxRange(current_selection_) != [field_.text length];
  if (model())
    model()->StartAutocomplete(current_selection_.length != 0,
                               prevent_inline_autocomplete);

  UpdatePopupAppearance();
}

void OmniboxViewIOS::UpdatePopupAppearance() {
  if (!popup_provider_) {
    return;
  }
  popup_provider_->SetTextAlignment([field_ bestTextAlignment]);
  popup_provider_->SetSemanticContentAttribute(
      [field_ bestSemanticContentAttribute]);
}

void OmniboxViewIOS::OnTemporaryTextMaybeChanged(
    const std::u16string& display_text,
    const AutocompleteMatch& match,
    bool save_original_selection,
    bool notify_text_changed) {
  SetWindowTextAndCaretPos(display_text, display_text.size(), false, false);
  if (model())
    model()->OnChanged();
}

void OmniboxViewIOS::OnInlineAutocompleteTextMaybeChanged(
    const std::u16string& display_text,
    std::vector<gfx::Range> selections,
    const std::u16string& prefix_autocompletion,
    const std::u16string& inline_autocompletion) {
  if (display_text == GetText())
    return;

  NSAttributedString* as = [[NSMutableAttributedString alloc]
      initWithString:base::SysUTF16ToNSString(display_text)];
  // TODO(crbug.com/40122891): This `user_text_length` calculation  isn't
  //  accurate when there's prefix autocompletion. This should be addressed
  //  before we experiment with prefix autocompletion on iOS.
  size_t user_text_length = display_text.size() - inline_autocompletion.size();
  [field_ setText:as userTextLength:user_text_length];
}

void OmniboxViewIOS::SetAdditionalText(const std::u16string& text) {
  if (!IsRichAutocompletionEnabled()) {
    return;
  }

  if (IsRichAutocompletionEnabled(
          RichAutocompletionImplementation::kNoAdditionalText)) {
    [consumer_ setOmniboxHasRichInline:text.length()];
    return;
  }

  if (!text.length()) {
    [consumer_ updateAdditionalText:nil];
    return;
  }

  // TODO(b/325035406): Temporary string and colors. Update if needed.
  NSString* additional_text = base::SysUTF16ToNSString(u" - " + text);
  [consumer_ updateAdditionalText:additional_text];
}

void OmniboxViewIOS::OnBeforePossibleChange() {
  GetState(&state_before_change_);
  marked_text_before_change_ = [[field_ markedText] copy];
}

bool OmniboxViewIOS::OnAfterPossibleChange(bool allow_keyword_ui_change) {
  State new_state;
  GetState(&new_state);
  // Manually update the selection state after calling GetState().
  new_state.sel_start = current_selection_.location;
  new_state.sel_end = current_selection_.location + current_selection_.length;

  OmniboxView::StateChanges state_changes =
      GetStateChanges(state_before_change_, new_state);

  // iOS does not supports KeywordProvider, so never allow keyword UI changes.
  const bool something_changed =
      model() &&
      model()->OnAfterPossibleChange(state_changes, allow_keyword_ui_change);

  if (model())
    model()->OnChanged();

  // TODO(justincohen): Find a different place to call this. Give the omnibox
  // a chance to update the alignment for a text direction change.
  [field_ updateTextDirection];
  return something_changed;
}

bool OmniboxViewIOS::IsImeComposing() const {
  return [field_ markedTextRange] != nil;
}

bool OmniboxViewIOS::IsIndicatingQueryRefinement() const {
  return false;
}

bool OmniboxViewIOS::IsSelectAll() const {
  return false;
}

void OmniboxViewIOS::GetSelectionBounds(std::u16string::size_type* start,
                                        std::u16string::size_type* end) const {
  if ([field_ isFirstResponder]) {
    NSRange selected_range = [field_ selectedNSRange];
    *start = selected_range.location;
    *end = selected_range.location + field_.autocompleteText.length;
  } else {
    *start = *end = 0;
  }
}

size_t OmniboxViewIOS::GetAllSelectionsLength() const {
  return 0;
}

gfx::NativeView OmniboxViewIOS::GetNativeView() const {
  return gfx::NativeView();
}

gfx::NativeView OmniboxViewIOS::GetRelativeWindowForPopup() const {
  return gfx::NativeView();
}

void OmniboxViewIOS::OnDidBeginEditing() {
  // If Open from Clipboard offers a suggestion, the popup may be opened when
  // `OnSetFocus` is called on the model. The state of the popup is saved early
  // to ignore that case.
  DCHECK(popup_provider_);
  bool popup_was_open_before_editing_began = popup_provider_->IsPopupOpen();

  // Make sure the omnibox popup's semantic content attribute is set correctly.
  popup_provider_->SetSemanticContentAttribute(
      [field_ bestSemanticContentAttribute]);

  OnBeforePossibleChange();

  if (model()) {
    model()->StartZeroSuggestRequest();
    model()->OnSetFocus(/*control_down=*/false);
  }

  // If the omnibox is displaying a URL and the popup is not showing, set the
  // field into pre-editing state.  If the omnibox is displaying search terms,
  // leave the default behavior of positioning the cursor at the end of the
  // text.  If the popup is already open, that means that the omnibox is
  // regaining focus after a popup scroll took focus away, so the pre-edit
  // behavior should not be invoked.
  if (!popup_was_open_before_editing_began)
    [field_ enterPreEditState];

  // `location_bar_` is only forwarding the call to the BVC. This should only
  // happen when the omnibox is being focused and it starts showing the popup;
  // if the popup was already open, no need to call this.
  if (!popup_was_open_before_editing_began)
    [focus_delegate_ omniboxDidBecomeFirstResponder];
}

bool OmniboxViewIOS::OnWillChange(NSRange range, NSString* new_text) {
  bool ok_to_change = true;

  if ([field_ isPreEditing]) {
    [field_ setClearingPreEditText:YES];

    // Exit the pre-editing state in OnWillChange() instead of OnDidChange(), as
    // that allows IME to continue working.  The following code selects the text
    // as if the pre-edit fake selection was real.
    [field_ exitPreEditState];

    // Reset `range` to be of zero-length at location zero, as the field will be
    // now cleared.
    range = NSMakeRange(0, 0);
  }

  // Figure out the old and current (new) selections.  Assume the new selection
  // will be of zero-length, located at the end of `new_text`.
  NSRange old_range = range;
  NSRange new_range = NSMakeRange(range.location + [new_text length], 0);

  // We may need to fix up the old and new ranges in the case where autocomplete
  // text was showing.  If there is autocomplete text, assume it was selected.
  // If the change is deleting one character from the end of the actual text,
  // disallow the change, but clear the autocomplete text and call OnDidChange
  // directly.  If there is autocomplete text AND a text field selection, or if
  // the user entered multiple characters, clear the autocomplete text and
  // pretend it never existed.
  if ([field_ hasAutocompleteText]) {
    bool adding_text = (range.length < [new_text length]);
    bool deleting_text = (range.length > [new_text length]);

    if (adding_text) {
      // TODO(rohitrao): What about cases where [new_text length] > 1?  This
      // could happen if an IME completion inserts multiple characters at once,
      // or if the user pastes some text in.  Let's loosen this test to allow
      // multiple characters, as long as the "old range" ends at the end of the
      // permanent text.
      NSString* userText = field_.userText;

      if (new_text.length == 1 && range.location == userText.length) {
        old_range =
            NSMakeRange(userText.length, field_.autocompleteText.length);
      }
    } else if (deleting_text) {
      NSString* userText = field_.userText;

      if ([new_text length] == 0 && range.location == [userText length] - 1) {
        ok_to_change = false;
      }
    }
  }

  // Update variables needed by OnDidChange() and GetState().
  old_selection_ = old_range;
  current_selection_ = new_range;

  // Store the displayed text.  Older version of Chrome used to clear the
  // autocomplete text here as well, but on iOS7 doing this causes the inline
  // autocomplete text to flicker, so the call was moved to the start on
  // OnDidChange().
  GetState(&state_before_change_);
  // Manually update the selection state after calling GetState().
  state_before_change_.sel_start = old_selection_.location;
  state_before_change_.sel_end =
      old_selection_.location + old_selection_.length;

  if (!ok_to_change) {
    // Force a change in the autocomplete system, since we won't get an
    // OnDidChange() message.
    OnDidChange(true);
  }

  return ok_to_change;
}

void OmniboxViewIOS::OnDidChange(bool processing_user_event) {
  // Sanitize pasted text.
  if (model() && model()->is_pasting()) {
    std::u16string pastedText = base::SysNSStringToUTF16(field_.text);
    std::u16string newText = OmniboxView::SanitizeTextForPaste(pastedText);
    if (pastedText != newText) {
      [field_ setText:base::SysUTF16ToNSString(newText)];
    }
  }

  // Clear the autocomplete text, since the omnibox model does not expect to see
  // it in OnAfterPossibleChange().  Clearing the text here should not cause
  // flicker as the UI will not get a chance to redraw before the new
  // autocomplete text is set by the model.
  [field_ clearAutocompleteText];
  [field_ setClearingPreEditText:NO];

  // Generally do not notify the autocomplete system of a text change unless the
  // change was a direct result of a user event.  One exception is if the marked
  // text changed, which could happen through a delayed IME recognition
  // callback.
  bool proceed_without_user_event = false;

  // The IME exception does not work for Korean text, because Korean does not
  // seem to ever have marked text.  It simply replaces or modifies previous
  // characters as you type.  Always proceed without user input if the
  // Korean keyboard is currently active.
  NSString* current_language = [[field_ textInputMode] primaryLanguage];

  if ([current_language hasPrefix:@"ko-"]) {
    proceed_without_user_event = true;
  } else {
    NSString* current_marked_text = [field_ markedText];

    // The IME exception kicks in if the current marked text is not equal to the
    // previous marked text.  Two nil strings should be considered equal, so
    // There is logic to avoid calling into isEqualToString: in that case.
    proceed_without_user_event =
        (marked_text_before_change_ || current_marked_text) &&
        ![current_marked_text isEqualToString:marked_text_before_change_];
  }

  if (!processing_user_event && !proceed_without_user_event)
    return;

  // TODO(crbug.com/41225237): OnAfterPossibleChange() now takes an argument. It
  // use to not take an argument and was defaulting to false, so as it is
  // unclear what the correct value is, using what was that before seems
  // consistent.
  OnAfterPossibleChange(false);
  OnBeforePossibleChange();
}

void OmniboxViewIOS::OnAccept() {
  base::RecordAction(UserMetricsAction("MobileOmniboxUse"));
  base::RecordAction(UserMetricsAction("IOS.Omnibox.AcceptDefaultSuggestion"));

  // TODO(crbug.com/359150039): handle accept with empty text.
  if (model()) {
    AcceptThumbnailEdits();
    model()->OpenSelection();
  }
  RevertAll();
}

void OmniboxViewIOS::OnClear() {
  [field_ clearAutocompleteText];
  [field_ exitPreEditState];
}

void OmniboxViewIOS::OnCopy() {
  NSString* selectedText = nil;
  NSInteger start_location = 0;
  if ([field_ isPreEditing]) {
    selectedText = field_.text;
    start_location = 0;
  } else {
    UITextRange* selected_range = [field_ selectedTextRange];
    selectedText = [field_ textInRange:selected_range];
    UITextPosition* start = [field_ beginningOfDocument];
    // The following call to `-offsetFromPosition:toPosition:` gives the offset
    // in terms of the number of "visible characters."  The documentation does
    // not specify whether this means glyphs or UTF16 chars.  This does not
    // matter for the current implementation of AdjustTextForCopy(), but it may
    // become an issue at some point.
    start_location =
        [field_ offsetFromPosition:start toPosition:[selected_range start]];
  }
  std::u16string text = base::SysNSStringToUTF16(selectedText);

  GURL url;
  bool write_url = false;
  // Model can be nullptr in tests.
  if (model())
    model()->AdjustTextForCopy(start_location, &text, &url, &write_url);

  // Create the pasteboard item manually because the pasteboard expects a single
  // item with multiple representations.  This is expressed as a single
  // NSDictionary with multiple keys, one for each representation.
  NSMutableDictionary* item = [NSMutableDictionary dictionaryWithCapacity:2];
  [item setObject:base::SysUTF16ToNSString(text)
           forKey:UTTypePlainText.identifier];

  if (write_url)
    [item setObject:net::NSURLWithGURL(url) forKey:UTTypeURL.identifier];

  StoreItemInPasteboard(item);

  [toolbar_commands_handler_ showShareButtonIPHAfterLocationBarUnfocus];
}

void OmniboxViewIOS::WillPaste() {
  if (model())
    model()->OnPaste();

  [field_ exitPreEditState];
}

void OmniboxViewIOS::UpdateAppearance() {
  // If Siri is thinking, treat that as user input being in progress.  It is
  // unsafe to modify the text field while voice entry is pending.
  if (model() && model()->ResetDisplayTexts()) {
    // Revert everything to the baseline look.
    RevertAll();
  } else if (model() && !model()->has_focus()) {
    // Even if the change wasn't "user visible" to the model, it still may be
    // necessary to re-color to the URL string.  Only do this if the omnibox is
    // not currently focused.
    NSAttributedString* as = [[NSMutableAttributedString alloc]
        initWithString:base::SysUTF16ToNSString(
                           model()->GetPermanentDisplayText())];
    [field_ setText:as userTextLength:[as length]];
  }
}

void OmniboxViewIOS::OnDeleteBackward() {
  if (field_.text.length == 0) {
    // If the user taps backspace while the pre-edit text is showing,
    // OnWillChange is invoked before this method and sets the text to an empty
    // string, so use the `clearingPreEditText` to determine if the chip should
    // be cleared or not.
    if ([field_ clearingPreEditText]) {
      // In the case where backspace is tapped while in pre-edit mode,
      // OnWillChange is called but OnDidChange is never called so ensure the
      // clearingPreEditText flag is set to false again.
      [field_ setClearingPreEditText:NO];
      // Explicitly set the input-in-progress flag. Normally this is set via
      // in model()->OnAfterPossibleChange, but in this case the text has been
      // set to the empty string by OnWillChange so when OnAfterPossibleChange
      // checks if the text has changed it does not see any difference so it
      // never sets the input-in-progress flag.
      if (model())
        model()->SetInputInProgress(YES);
    }
  }
}

void OmniboxViewIOS::OnAcceptAutocomplete() {
  current_selection_ = [field_ selectedNSRange];
  OnDidChange(/*processing_user_event=*/true);
}

void OmniboxViewIOS::OnRemoveAdditionalText() {
  if (model()) {
    model()->UpdateInput(/*has_selected_text=*/false,
                         /*prevent_inline_autocomplete=*/true);
  }
}

void OmniboxViewIOS::ClearText() {
  // Ensure omnibox is first responder. This will bring up the keyboard so the
  // user can start typing a new query.
  if (![field_ isFirstResponder])
    [field_ becomeFirstResponder];
  if (field_.text.length != 0) {
    // Remove the text in the omnibox.
    // Calling -[UITextField setText:] does not trigger
    // -[id<UITextFieldDelegate> textDidChange] so it must be called explicitly.
    OnClear();
    [field_ setText:@""];
    OnDidChange(YES);
  }
  // Calling OnDidChange() can trigger a scroll event, which removes focus from
  // the omnibox.
  [field_ becomeFirstResponder];
}

void OmniboxViewIOS::EndEditing() {
  if (model() && model()->has_focus()) {
    CloseOmniboxPopup();

    RecordSuggestionsListScrolled(model()->GetPageClassification(),
                                  suggestions_list_scrolled_);
    suggestions_list_scrolled_ = false;

    model()->OnWillKillFocus();
    model()->OnKillFocus();
    if ([field_ isPreEditing])
      [field_ exitPreEditState];

    // The controller looks at the current pre-edit state, so the call to
    // OnKillFocus() must come after exiting pre-edit.
    [focus_delegate_ omniboxDidResignFirstResponder];

    // Blow away any in-progress edits.
    RevertAll();
    DCHECK(![field_ hasAutocompleteText]);
  }
}

void OmniboxViewIOS::HideKeyboard() {
  [field_ resignFirstResponder];
}

void OmniboxViewIOS::OnCallActionTap() {
  this->HideKeyboard();
}

void OmniboxViewIOS::FocusOmnibox() {
  [field_ becomeFirstResponder];
}

BOOL OmniboxViewIOS::IsPopupOpen() {
  if (!popup_provider_) {
    return NO;
  }
  return popup_provider_->IsPopupOpen();
}

int OmniboxViewIOS::GetOmniboxTextLength() const {
  return [field_ displayedText].length;
}

#pragma mark - OmniboxPopupViewSuggestionsDelegate

void OmniboxViewIOS::OnPopupDidScroll() {
  this->HideKeyboard();
  suggestions_list_scrolled_ = true;
}

void OmniboxViewIOS::OnSelectedMatchForAppending(const std::u16string& str) {
  // Exit preedit state and append the match. Refocus if necessary.
  if ([field_ isPreEditing])
    [field_ exitPreEditState];
  this->SetUserText(str);
  // Calling setText: does not trigger UIControlEventEditingChanged, so
  // trigger that manually.
  [field_ sendActionsForControlEvents:UIControlEventEditingChanged];
  this->FocusOmnibox();
    if (@available(iOS 17, *)) {
      // Set the caret pos to the end of the text (crbug.com/331622199).
      this->SetCaretPos(str.length());
    }
}

void OmniboxViewIOS::OnSelectedMatchForOpening(
    AutocompleteMatch match,
    WindowOpenDisposition disposition,
    const GURL& alternate_nav_url,
    const std::u16string& pasted_text,
    size_t index) {
  const auto match_selection_timestamp = base::TimeTicks();

  // 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.
  auto* autocomplete_controller = controller()->autocomplete_controller();
  if (index >= autocomplete_controller->result().size() ||
      autocomplete_controller->result().match_at(index).destination_url !=
          match.destination_url) {
    AcceptThumbnailEdits();
    OmniboxPopupSelection selection(
        autocomplete_controller->InjectAdHocMatch(match));
    model()->OpenSelection(selection, match_selection_timestamp, disposition);
    return;
  }

  // Fill in clipboard matches if they don't have a destination URL.
  if (match.destination_url.is_empty()) {
    if (match.type == AutocompleteMatchType::CLIPBOARD_URL) {
      ClipboardRecentContent* clipboard_recent_content =
          ClipboardRecentContent::GetInstance();
      clipboard_recent_content->GetRecentURLFromClipboard(base::BindOnce(
          &OmniboxViewIOS::OnReceiveClipboardURLForOpenMatch,
          weak_ptr_factory_.GetWeakPtr(), match, disposition, alternate_nav_url,
          pasted_text, index, match_selection_timestamp));
      return;
    } else if (match.type == AutocompleteMatchType::CLIPBOARD_TEXT) {
      ClipboardRecentContent* clipboard_recent_content =
          ClipboardRecentContent::GetInstance();
      clipboard_recent_content->GetRecentTextFromClipboard(base::BindOnce(
          &OmniboxViewIOS::OnReceiveClipboardTextForOpenMatch,
          weak_ptr_factory_.GetWeakPtr(), match, disposition, alternate_nav_url,
          pasted_text, index, match_selection_timestamp));
      return;
    } else if (match.type == AutocompleteMatchType::CLIPBOARD_IMAGE) {
      ClipboardRecentContent* clipboard_recent_content =
          ClipboardRecentContent::GetInstance();
      clipboard_recent_content->GetRecentImageFromClipboard(base::BindOnce(
          &OmniboxViewIOS::OnReceiveClipboardImageForOpenMatch,
          weak_ptr_factory_.GetWeakPtr(), match, disposition, alternate_nav_url,
          pasted_text, index, match_selection_timestamp));
      return;
    }
  }
  AcceptThumbnailEdits();
  model()->OpenSelection(OmniboxPopupSelection(index),
                         match_selection_timestamp, disposition);
}

#pragma mark - Thumbnail

void OmniboxViewIOS::SetThumbnailImage(UIImage* image) {
  thumbnail_image_before_edit_ = image;
  thumbnail_deleted_ = NO;
  [consumer_ setThumbnailImage:image];
}

void OmniboxViewIOS::AcceptThumbnailEdits() {
  if (thumbnail_deleted_) {
    thumbnail_image_before_edit_ = nil;
    thumbnail_deleted_ = NO;
    // TODO(crbug.com/359150039): Signal to the client that the thumbnail has
    // been deleted.
  }
}

void OmniboxViewIOS::RevertThumbnailEdits() {
  if (thumbnail_deleted_) {
    [consumer_ setThumbnailImage:thumbnail_image_before_edit_];
    thumbnail_deleted_ = NO;
  }
}

void OmniboxViewIOS::RemoveThumbnail() {
  thumbnail_deleted_ = YES;
  [consumer_ setThumbnailImage:nil];
}