chromium/ui/base/ime/ash/input_method_ash.cc

// Copyright 2014 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "ui/base/ime/ash/input_method_ash.h"

#include <stddef.h>

#include <algorithm>
#include <cstring>
#include <set>
#include <string_view>
#include <tuple>
#include <utility>
#include <vector>

#include "ash/constants/ash_features.h"
#include "base/check.h"
#include "base/feature_list.h"
#include "base/functional/bind.h"
#include "base/i18n/char_iterator.h"
#include "base/strings/string_util.h"
#include "base/strings/utf_string_conversions.h"
#include "base/third_party/icu/icu_utf.h"
#include "base/time/default_clock.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/ash/ime_keyboard.h"
#include "ui/base/ime/ash/input_method_manager.h"
#include "ui/base/ime/ash/text_input_method.h"
#include "ui/base/ime/ash/typing_session_manager.h"
#include "ui/base/ime/composition_text.h"
#include "ui/base/ime/constants.h"
#include "ui/base/ime/events.h"
#include "ui/base/ime/ime_key_event_dispatcher.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/base/ime/text_input_type.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/event.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
#include "ui/events/ozone/events_ozone.h"
#include "ui/gfx/geometry/rect.h"

namespace ash {

namespace {

using ::ui::CompositionText;
using ::ui::TextInputClient;

template <typename T>
T ConvertTextInputFlagToEnum(int flags, int flag_on_value, int flag_off_value) {
  if (flags & flag_on_value) {
    return T::kEnabled;
  }
  if (flags & flag_off_value) {
    return T::kDisabled;
  }
  return T::kUnspecified;
}

AutocapitalizationMode ConvertAutocapitalizationMode(int flags) {
  if (flags & ui::TEXT_INPUT_FLAG_AUTOCAPITALIZE_NONE) {
    return AutocapitalizationMode::kNone;
  }
  if (flags & ui::TEXT_INPUT_FLAG_AUTOCAPITALIZE_CHARACTERS) {
    return AutocapitalizationMode::kCharacters;
  }
  if (flags & ui::TEXT_INPUT_FLAG_AUTOCAPITALIZE_WORDS) {
    return AutocapitalizationMode::kWords;
  }
  if (flags & ui::TEXT_INPUT_FLAG_AUTOCAPITALIZE_SENTENCES) {
    return AutocapitalizationMode::kSentences;
  }
  return AutocapitalizationMode::kUnspecified;
}

// Returns whether `url` refers to Terminal/crosh.
bool IsTerminalOrCrosh(const GURL& url) {
  return base::StartsWith(url.spec(), "chrome-untrusted://terminal") ||
         base::StartsWith(url.spec(), "chrome-untrusted://crosh");
}

}  // namespace

TextInputMethod* GetEngine() {
  auto* bridge = IMEBridge::Get();
  return bridge ? bridge->GetCurrentEngineHandler() : nullptr;
}

// InputMethodAsh implementation -----------------------------------------
InputMethodAsh::InputMethodAsh(
    ui::ImeKeyEventDispatcher* ime_key_event_dispatcher)
    : InputMethodBase(ime_key_event_dispatcher),
      typing_session_manager_(base::DefaultClock::GetInstance()) {
  ResetContext();
}

InputMethodAsh::~InputMethodAsh() {
  ConfirmComposition(/* reset_engine */ true);
  // We are dead, so we need to ask the client to stop relying on us.
  OnInputMethodChanged();

  if (IMEBridge::Get() && IMEBridge::Get()->GetInputContextHandler() == this) {
    IMEBridge::Get()->SetInputContextHandler(nullptr);
  }
  typing_session_manager_.EndAndRecordSession();
}

InputMethodAsh::PendingSetCompositionRange::PendingSetCompositionRange(
    const gfx::Range& range,
    const std::vector<ui::ImeTextSpan>& text_spans)
    : range(range), text_spans(text_spans) {}

InputMethodAsh::PendingSetCompositionRange::PendingSetCompositionRange(
    const PendingSetCompositionRange& other) = default;

InputMethodAsh::PendingSetCompositionRange::~PendingSetCompositionRange() =
    default;

InputMethodAsh::PendingAutocorrectRange::PendingAutocorrectRange(
    const gfx::Range& range,
    TextInputTarget::SetAutocorrectRangeDoneCallback callback)
    : range(range), callback(std::move(callback)) {}

InputMethodAsh::PendingAutocorrectRange::~PendingAutocorrectRange() = default;

ui::EventDispatchDetails InputMethodAsh::DispatchKeyEvent(ui::KeyEvent* event) {
  DCHECK(!(event->flags() & ui::EF_IS_SYNTHESIZED));

  // For OS_CHROMEOS build of Chrome running on Linux, the IME keyboard cannot
  // track the Caps Lock state by itself, so need to call SetCapsLockEnabled()
  // method to reflect the Caps Lock state by the key event.
  auto* manager = input_method::InputMethodManager::Get();
  if (manager) {
    input_method::ImeKeyboard* keyboard = manager->GetImeKeyboard();
    if (keyboard && event->type() == ui::EventType::kKeyPressed &&
        event->key_code() != ui::VKEY_CAPITAL &&
        keyboard->IsCapsLockEnabled() != event->IsCapsLockOn()) {
      // Synchronize the keyboard state with event's state if they do not
      // match. Do not synchronize for Caps Lock key because it is already
      // handled in event rewriter.
      keyboard->SetCapsLockEnabled(event->IsCapsLockOn());
    }

    // For JP106 language input keys, makes sure they can be passed to the app
    // so that the VDI web apps can be supported. See https://crbug.com/816341.
    // VKEY_CONVERT: Henkan key
    // VKEY_NONCONVERT: Muhenkan key
    // VKEY_DBE_SBCSCHAR/VKEY_DBE_DBCSCHAR: ZenkakuHankaku key
    input_method::InputMethodManager::State* state =
        manager->GetActiveIMEState().get();
    if (event->type() == ui::EventType::kKeyPressed && state) {
      bool language_input_key = true;
      switch (event->key_code()) {
        case ui::VKEY_CONVERT:
          state->ChangeInputMethodToJpIme();
          break;
        case ui::VKEY_NONCONVERT:
          state->ChangeInputMethodToJpKeyboard();
          break;
        case ui::VKEY_DBE_SBCSCHAR:
        case ui::VKEY_DBE_DBCSCHAR:
          state->ToggleInputMethodForJpIme();
          break;
        default:
          language_input_key = false;
          break;
      }
      if (language_input_key) {
        // Dispatches the event to app/blink.
        // TODO(shuchen): Eventually, the language input keys should be handed
        // over to the IME extension to process. And IMF can handle if the IME
        // extension didn't handle.
        return DispatchKeyEventPostIME(event);
      }
    }
  }

  // Simply forward the key event if there's no focused TextInputClient.
  // Dead keys cannot be supported in this case because composition and commit
  // are not supported.
  if (base::FeatureList::IsEnabled(
          features::kInputMethodDeadKeyFixForNoInputField) &&
      GetTextInputClient() == nullptr) {
    return DispatchKeyEventPostIME(event);
  }

  // If |context_| is not usable, then we can only dispatch the key event as is.
  // We only dispatch the key event to input method when the |context_| is an
  // normal input field (not a password field).
  // Note: We need to send the key event to ibus even if the |context_| is not
  // enabled, so that ibus can have a chance to enable the |context_|.
  if (IsPasswordOrNoneInputFieldFocused() || !GetEngine()) {
    if (event->type() == ui::EventType::kKeyPressed) {
      if (ExecuteCharacterComposer(*event)) {
        // Treating as PostIME event if character composer handles key event and
        // generates some IME event,
        return ProcessKeyEventPostIME(
            event, ui::ime::KeyEventHandledState::kHandledByIME,
            /* stopped_propagation */ true);
      }
      return ProcessUnfilteredKeyPressEvent(event);
    }
    return DispatchKeyEventPostIME(event);
  }

  // Resets previous event dispatch details before invoking IME engine's
  // `ProcessKeyEvent`. `ProcessKeyEventDone` sets its value when it
  // re-dispatches the key events. If `ProcessKeyEventDone` is invoked
  // synchronously by `ProcessKeyEvent`, `dispatch_details_` would have correct
  // dispatch details data to return. This is important because an `EventTarget`
  // could be destroyed under `ProcessKeyEventDone`.
  // See http://crbug.com/1392491.
  dispatch_details_.reset();

  handling_key_event_ = true;
  GetEngine()->ProcessKeyEvent(
      *event, base::BindOnce(&InputMethodAsh::ProcessKeyEventDone,
                             weak_ptr_factory_.GetWeakPtr(),
                             // Pass the ownership of the new copied event.
                             base::Owned(new ui::KeyEvent(*event))));
  return dispatch_details_.value_or(ui::EventDispatchDetails());
}

void InputMethodAsh::ProcessKeyEventDone(
    ui::KeyEvent* event,
    ui::ime::KeyEventHandledState handled_state) {
  DCHECK(event);
  bool is_handled_by_char_composer = false;
  if (event->type() == ui::EventType::kKeyPressed) {
    if (handled_state != ui::ime::KeyEventHandledState::kNotHandled) {
      // IME event has a priority to be handled, so that character composer
      // should be reset.
      character_composer_.Reset();
    } else {
      // If IME does not handle key event, passes keyevent to character composer
      // to be able to compose complex characters.
      is_handled_by_char_composer = ExecuteCharacterComposer(*event);

      if (!is_handled_by_char_composer &&
          !ui::KeycodeConverter::IsDomKeyForModifier(event->GetDomKey())) {
        // If the character composer didn't handle it either, then confirm any
        // composition text before forwarding the key event. We ignore modifier
        // keys because, for example, if the IME handles Shift+A, then we don't
        // want the Shift key to confirm the composition text. Only confirm the
        // composition text when the IME does not handle the full key combo.
        ConfirmComposition(/* reset_engine */ true);
      }
    }
  }
  if (event->type() == ui::EventType::kKeyPressed ||
      event->type() == ui::EventType::kKeyReleased) {
    ui::ime::KeyEventHandledState handled_state_to_process =
        is_handled_by_char_composer
            ? ui::ime::KeyEventHandledState::kHandledByIME
            : handled_state;
    dispatch_details_ = ProcessKeyEventPostIME(event, handled_state_to_process,
                                               /* stopped_propagation */ false);
  }
  handling_key_event_ = false;
}

void InputMethodAsh::OnTextInputTypeChanged(TextInputClient* client) {
  if (!IsTextInputClientFocused(client)) {
    return;
  }

  UpdateContextFocusState();

  TextInputMethod* engine = GetEngine();
  if (engine) {
    const TextInputMethod::InputContext context = GetInputContext();
    // When focused input client is not changed, a text input type change
    // should cause blur/focus events to engine. The focus in to or out from
    // password field should also notify engine.
    engine->Blur();
    engine->Focus(context);
  }

  OnCaretBoundsChanged(client);

  InputMethodBase::OnTextInputTypeChanged(client);
}

void InputMethodAsh::OnCaretBoundsChanged(const TextInputClient* client) {
  if (IsTextInputTypeNone() || !IsTextInputClientFocused(client)) {
    return;
  }

  NotifyTextInputCaretBoundsChanged(client);

  if (IsPasswordOrNoneInputFieldFocused()) {
    return;
  }

  // The current text input type should not be NONE if |context_| is focused.
  DCHECK(client == GetTextInputClient());
  DCHECK(!IsTextInputTypeNone());

  TextInputMethod* engine = GetEngine();
  if (engine) {
    engine->SetCaretBounds(client->GetCaretBounds());
  }

  IMECandidateWindowHandlerInterface* candidate_window =
      IMEBridge::Get()->GetCandidateWindowHandler();
  IMEAssistiveWindowHandlerInterface* assistive_window =
      IMEBridge::Get()->GetAssistiveWindowHandler();
  if (!candidate_window && !assistive_window) {
    return;
  }

  const gfx::Rect caret_rect = client->GetCaretBounds();

  gfx::Rect composition_bounds;
  if (client->HasCompositionText()) {
    client->GetCompositionCharacterBounds(0, &composition_bounds);
  }

  // Pepper doesn't support composition bounds, so fall back to caret bounds to
  // avoid a bad user experience (the IME window moved to upper left corner).
  if (composition_bounds.IsEmpty()) {
    composition_bounds = caret_rect;
  }
  if (candidate_window) {
    candidate_window->SetCursorAndCompositionBounds(caret_rect,
                                                    composition_bounds);
  }

  if (assistive_window) {
    Bounds bounds;
    bounds.caret = caret_rect;
    bounds.autocorrect = client->GetAutocorrectCharacterBounds();
    assistive_window->SetBounds(bounds);
  }

  gfx::Range text_range;
  gfx::Range selection_range;
  std::u16string surrounding_text;
  if (!client->GetTextRange(&text_range) ||
      !client->GetTextFromRange(text_range, &surrounding_text) ||
      !client->GetEditableSelectionRange(&selection_range)) {
    previous_surrounding_text_.clear();
    previous_selection_range_ = gfx::Range::InvalidRange();
    return;
  }

  if (previous_selection_range_ == selection_range &&
      previous_surrounding_text_ == surrounding_text) {
    return;
  }

  previous_selection_range_ = selection_range;
  previous_surrounding_text_ = surrounding_text;

  if (!selection_range.IsValid()) {
    // TODO(nona): Ideally selection_range should not be invalid.
    // TODO(nona): If javascript changes the focus on page loading, even (0,0)
    //             can not be obtained. Need investigation.
    return;
  }

  // Here SetSurroundingText accepts relative position of |surrounding_text|, so
  // we have to convert |selection_range| from node coordinates to
  // |surrounding_text| coordinates.
  if (GetEngine()) {
    // TODO(b/245020074): Handle the case where selection is before the offset.
    const uint32_t offset = text_range.start();
    DCHECK_GE(selection_range.start(), offset);
    DCHECK_GE(selection_range.end(), offset);
    const gfx::Range relative_selection_range(selection_range.start() - offset,
                                              selection_range.end() - offset);
    GetEngine()->SetSurroundingText(surrounding_text, relative_selection_range,
                                    offset);
  }
}

void InputMethodAsh::CancelComposition(const TextInputClient* client) {
  if (!IsPasswordOrNoneInputFieldFocused() &&
      IsTextInputClientFocused(client)) {
    ResetContext();
  }
}

bool InputMethodAsh::IsCandidatePopupOpen() const {
  // TODO(yukishiino): Implement this method.
  return false;
}

ui::VirtualKeyboardController* InputMethodAsh::GetVirtualKeyboardController() {
  if (auto* engine = GetEngine()) {
    if (auto* controller = engine->GetVirtualKeyboardController()) {
      return controller;
    }
  }
  return InputMethodBase::GetVirtualKeyboardController();
}

void InputMethodAsh::OnFocus() {
  auto* bridge = IMEBridge::Get();
  if (bridge) {
    bridge->SetInputContextHandler(this);
  }
}

void InputMethodAsh::OnBlur() {
  if (IMEBridge::Get() && IMEBridge::Get()->GetInputContextHandler() == this) {
    IMEBridge::Get()->SetInputContextHandler(nullptr);
  }
}

void InputMethodAsh::OnWillChangeFocusedClient(TextInputClient* focused_before,
                                               TextInputClient* focused) {
  ConfirmComposition(/* reset_engine */ true);

  // Remove any autocorrect range in the unfocused TextInputClient.
  if (focused_before) {
    focused_before->SetAutocorrectRange(gfx::Range());
  }

  if (GetEngine()) {
    GetEngine()->Blur();
  }
}

void InputMethodAsh::OnDidChangeFocusedClient(TextInputClient* focused_before,
                                              TextInputClient* focused) {
  // Force to update the input type since client's TextInputStateChanged()
  // function might not be called if text input types before the client loses
  // focus and after it acquires focus again are the same.
  UpdateContextFocusState();

  if (GetEngine()) {
    GetEngine()->Focus(GetInputContext());
  }

  OnCaretBoundsChanged(GetTextInputClient());
}

bool InputMethodAsh::SetCompositionRange(
    uint32_t before,
    uint32_t after,
    const std::vector<ui::ImeTextSpan>& text_spans) {
  TextInputClient* client = GetTextInputClient();

  if (IsTextInputTypeNone()) {
    return false;
  }
  typing_session_manager_.Heartbeat();
  // The given range and spans are relative to the current selection.
  gfx::Range range;
  if (!client->GetEditableSelectionRange(&range)) {
    return false;
  }

  const gfx::Range composition_range(
      range.start() >= before ? range.start() - before : 0,
      range.end() + after);

  // Check that the composition range is valid.
  gfx::Range text_range;
  client->GetTextRange(&text_range);
  if (!text_range.Contains(composition_range)) {
    return false;
  }

  return SetComposingRange(composition_range.start(), composition_range.end(),
                           text_spans);
}

bool InputMethodAsh::SetComposingRange(
    uint32_t start,
    uint32_t end,
    const std::vector<ui::ImeTextSpan>& text_spans) {
  TextInputClient* client = GetTextInputClient();

  if (IsTextInputTypeNone()) {
    return false;
  }

  const auto ordered_range = std::minmax(start, end);
  const gfx::Range composition_range(ordered_range.first, ordered_range.second);

  // Use a default text span that spans across the whole composition range.
  auto non_empty_text_spans =
      !text_spans.empty()
          ? text_spans
          : std::vector<ui::ImeTextSpan>{ui::ImeTextSpan(
                ui::ImeTextSpan::Type::kComposition,
                /*start_offset=*/0, /*end_offset=*/composition_range.length())};

  // If we have pending key events, then delay the operation until
  // |ProcessKeyEventPostIME|. Otherwise, process it immediately.
  if (handling_key_event_) {
    composition_changed_ = true;
    pending_composition_range_ =
        PendingSetCompositionRange{composition_range, non_empty_text_spans};
    return true;
  } else {
    composing_text_ = true;
    return client->SetCompositionFromExistingText(composition_range,
                                                  non_empty_text_spans);
  }
}

gfx::Range InputMethodAsh::GetAutocorrectRange() {
  if (IsTextInputTypeNone()) {
    return gfx::Range();
  }
  return GetTextInputClient()->GetAutocorrectRange();
}

void InputMethodAsh::SetAutocorrectRange(
    const gfx::Range& range,
    SetAutocorrectRangeDoneCallback callback) {
  if (IsTextInputTypeNone()) {
    std::move(callback).Run(false);
    return;
  }

  // If we have pending key events, then delay the operation until
  // |ProcessKeyEventPostIME|. Otherwise, process it immediately.
  if (handling_key_event_) {
    if (pending_autocorrect_range_) {
      std::move(pending_autocorrect_range_->callback).Run(false);
    }

    pending_autocorrect_range_ =
        std::make_unique<InputMethodAsh::PendingAutocorrectRange>(
            range, std::move(callback));
  } else {
    std::move(callback).Run(GetTextInputClient()->SetAutocorrectRange(range));
  }
}

std::optional<ui::GrammarFragment>
InputMethodAsh::GetGrammarFragmentAtCursor() {
  if (IsTextInputTypeNone()) {
    return std::nullopt;
  }
  return GetTextInputClient()->GetGrammarFragmentAtCursor();
}

bool InputMethodAsh::ClearGrammarFragments(const gfx::Range& range) {
  if (IsTextInputTypeNone()) {
    return false;
  }
  return GetTextInputClient()->ClearGrammarFragments(range);
}

bool InputMethodAsh::AddGrammarFragments(
    const std::vector<ui::GrammarFragment>& fragments) {
  if (IsTextInputTypeNone()) {
    return false;
  }
  return GetTextInputClient()->AddGrammarFragments(fragments);
}

void InputMethodAsh::ConfirmComposition(bool reset_engine) {
  TextInputClient* client = GetTextInputClient();
  // TODO(b/223075193): Quick fix for the case where we have a pending commit.
  // Without this, then we would lose the pending commit after confirming the
  // composition text.
  // Fix this properly by getting rid of the pending mechanism completely.
  if (pending_commit_ && !pending_composition_range_ && !pending_composition_) {
    // Only a pending commit, so confirming the composition is a no-op.
    return;
  }
  // TODO(b/225723475): Similar to the comment above, this is a quick fix to
  // solve the autocorrect issue outlined in the linked bug. This is due to the
  // pending composition being reset before it could be applied to the current
  // text. Again we need to fix this properly by removing the pending mechanism.
  if (pending_composition_ && !pending_commit_ && !pending_composition_range_) {
    GetTextInputClient()->SetCompositionText(*pending_composition_);
    pending_composition_ = std::nullopt;
    composition_changed_ = false;
  }
  if (client && (client->HasCompositionText() ||
                 client->SupportsAlwaysConfirmComposition())) {
    const size_t characters_committed =
        client->ConfirmCompositionText(/*keep_selection*/ true);
    typing_session_manager_.CommitCharacters(characters_committed);
  }
  // See https://crbug.com/984472.
  ResetContext(reset_engine);
}

void InputMethodAsh::ResetContext(bool reset_engine) {
  if (IsPasswordOrNoneInputFieldFocused() || !GetTextInputClient()) {
    return;
  }

  const bool was_composing = composing_text_;

  pending_composition_ = std::nullopt;
  pending_commit_ = std::nullopt;
  composing_text_ = false;
  composition_changed_ = false;

  if (reset_engine && was_composing && GetEngine()) {
    GetEngine()->Reset();
  }

  character_composer_.Reset();
}

void InputMethodAsh::UpdateContextFocusState() {
  ResetContext();
  OnInputMethodChanged();

  // Propagate the focus event to the candidate window handler which also
  // manages the input method mode indicator.
  IMECandidateWindowHandlerInterface* candidate_window =
      IMEBridge::Get()->GetCandidateWindowHandler();
  if (candidate_window) {
    candidate_window->FocusStateChanged(!IsPasswordOrNoneInputFieldFocused());
  }

  // Propagate focus event to assistive window handler.
  IMEAssistiveWindowHandlerInterface* assistive_window =
      IMEBridge::Get()->GetAssistiveWindowHandler();
  if (assistive_window) {
    assistive_window->FocusStateChanged();
  }

  IMEBridge::Get()->SetCurrentInputContext(GetInputContext());

  TextInputClient* client = GetTextInputClient();
  focused_url_ = client && !IsPasswordOrNoneInputFieldFocused()
                     ? client->GetTextEditingContext().page_url
                     : GURL();
}

ui::EventDispatchDetails InputMethodAsh::ProcessKeyEventPostIME(
    ui::KeyEvent* event,
    ui::ime::KeyEventHandledState handled_state,
    bool stopped_propagation) {
  bool handled =
      handled_state == ui::ime::KeyEventHandledState::kHandledByIME ||
      handled_state ==
          ui::ime::KeyEventHandledState::kHandledByAssistiveSuggester;

  auto properties =
      event->properties() ? *event->properties() : ui::Event::Properties();
  // Mark whether the key is handled by IME or not.
  ui::SetKeyboardImeFlagProperty(&properties,
                                 handled ? ui::kPropertyKeyboardImeHandledFlag
                                         : ui::kPropertyKeyboardImeIgnoredFlag);
  // Mark whether autorepeat needs to be suppressed.
  if (handled_state ==
      ui::ime::KeyEventHandledState::kNotHandledSuppressAutoRepeat) {
    ui::SetKeyEventSuppressAutoRepeat(properties);
  }
  event->SetProperties(properties);

  TextInputClient* client = GetTextInputClient();
  if (!client) {
    // As ibus works asynchronously, there is a chance that the focused client
    // loses focus before this method gets called.
    return DispatchKeyEventPostIME(event);
  }

  if (event->type() == ui::EventType::kKeyPressed && handled) {
    bool only_dispatch_vkey_processkey =
        (handled_state ==
         ui::ime::KeyEventHandledState::kHandledByAssistiveSuggester);
    ui::EventDispatchDetails dispatch_details =
        ProcessFilteredKeyPressEvent(event, only_dispatch_vkey_processkey);
    if (event->stopped_propagation()) {
      ResetContext();
      return dispatch_details;
    }
  }
  ui::EventDispatchDetails dispatch_details;

  // In case the focus was changed by the key event. The |context_| should have
  // been reset when the focused window changed.
  if (client != GetTextInputClient()) {
    return dispatch_details;
  }

  MaybeProcessPendingInputMethodResult(event, handled);

  // In case the focus was changed when sending input method results to the
  // focused window.
  if (client != GetTextInputClient()) {
    return dispatch_details;
  }

  if (handled) {
    return dispatch_details;  // IME handled the key event. do not forward.
  }

  if (event->type() == ui::EventType::kKeyPressed) {
    return ProcessUnfilteredKeyPressEvent(event);
  }

  if (event->type() == ui::EventType::kKeyReleased) {
    return DispatchKeyEventPostIME(event);
  }
  return dispatch_details;
}

ui::EventDispatchDetails InputMethodAsh::ProcessFilteredKeyPressEvent(
    ui::KeyEvent* event,
    bool only_dispatch_vkey_processkey) {
  if (!only_dispatch_vkey_processkey) {
    if (NeedInsertChar()) {
      return DispatchKeyEventPostIME(event);
    }

    // For dead keys, it is possible to dispatch a fake Process key, but it is
    // better to dispatch the real dead key, as it is more specific and allows
    // apps to have dead key specific behavior.
    // TODO(b/289319217): Investigate if we need to distinguish between a dead
    // key that is handled by the character composer or is handled by the input
    // method.
    if ((base::FeatureList::IsEnabled(features::kInputMethodDeadKeyFix) ||
         (focused_url_.is_valid() && IsTerminalOrCrosh(focused_url_))) &&
        event->GetDomKey().IsDeadKey()) {
      return DispatchKeyEventPostIME(event);
    }
  }

  ui::KeyEvent fabricated_event(ui::EventType::kKeyPressed, ui::VKEY_PROCESSKEY,
                                event->code(), event->flags(),
                                ui::DomKey::PROCESS, event->time_stamp());
  if (const auto* properties = event->properties()) {
    fabricated_event.SetProperties(*properties);
  }
  ui::EventDispatchDetails dispatch_details =
      DispatchKeyEventPostIME(&fabricated_event);
  if (fabricated_event.stopped_propagation()) {
    event->StopPropagation();
  }
  return dispatch_details;
}

ui::EventDispatchDetails InputMethodAsh::ProcessUnfilteredKeyPressEvent(
    ui::KeyEvent* event) {
  TextInputClient* prev_client = GetTextInputClient();
  ui::EventDispatchDetails details = DispatchKeyEventPostIME(event);
  if (event->stopped_propagation()) {
    ResetContext();
    return details;
  }

  // We shouldn't dispatch the character anymore if the key event dispatch
  // caused focus change. For example, in the following scenario,
  // 1. visit a web page which has a <textarea>.
  // 2. click Omnibox.
  // 3. enable Korean IME, press A, then press Tab to move the focus to the web
  //    page.
  // We should return here not to send the Tab key event to RWHV.
  TextInputClient* client = GetTextInputClient();
  if (!client || client != prev_client) {
    return details;
  }

  // If a key event was not filtered by |context_| and |character_composer_|,
  // then it means the key event didn't generate any result text. So we need
  // to send corresponding character to the focused text input client.
  if (event->GetCharacter()) {
    client->InsertChar(*event);
    typing_session_manager_.CommitCharacters(1);
  }
  return details;
}

void InputMethodAsh::MaybeProcessPendingInputMethodResult(ui::KeyEvent* event,
                                                          bool handled) {
  TextInputClient* client = GetTextInputClient();
  DCHECK(client);

  if (pending_commit_) {
    if (handled && NeedInsertChar()) {
      for (const auto& ch : pending_commit_->text) {
        ui::KeyEvent ch_event(ui::EventType::kKeyPressed, ui::VKEY_UNKNOWN,
                              ui::EF_NONE);
        ch_event.set_character(ch);
        ui::SetKeyboardImeFlags(&ch_event, ui::kPropertyKeyboardImeHandledFlag);
        client->InsertChar(ch_event);
      }
    } else if (pending_commit_->text.empty()) {
      client->InsertText(
          u"", TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
      composing_text_ = false;
    } else {
      // Split the commit into two separate commits, one for the substring
      // before the cursor and one for the substring after.
      const std::u16string before_cursor =
          pending_commit_->text.substr(0, pending_commit_->cursor);
      if (!before_cursor.empty()) {
        client->InsertText(
            before_cursor,
            TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
      }
      const std::u16string after_cursor =
          pending_commit_->text.substr(pending_commit_->cursor);
      if (!after_cursor.empty()) {
        client->InsertText(
            after_cursor,
            TextInputClient::InsertTextCursorBehavior::kMoveCursorBeforeText);
      }
      composing_text_ = false;
    }
    typing_session_manager_.CommitCharacters(pending_commit_->text.length());
  }

  // TODO(crbug.com/40623107): Refactor this code to be clearer and less
  // error-prone.
  if (composition_changed_ && !IsTextInputTypeNone()) {
    if (pending_composition_range_) {
      client->SetCompositionFromExistingText(
          pending_composition_range_->range,
          pending_composition_range_->text_spans);
    }
    if (pending_composition_) {
      composing_text_ = true;
      client->SetCompositionText(*pending_composition_);
    } else if (!pending_commit_ && !pending_composition_range_) {
      client->ClearCompositionText();
    }

    pending_composition_ = std::nullopt;
    pending_composition_range_.reset();
  }

  if (pending_autocorrect_range_) {
    std::move(pending_autocorrect_range_->callback)
        .Run(client->SetAutocorrectRange(pending_autocorrect_range_->range));
    pending_autocorrect_range_.reset();
  }

  // We should not clear composition text here, as it may belong to the next
  // composition session.
  pending_commit_ = std::nullopt;
  composition_changed_ = false;
}

bool InputMethodAsh::NeedInsertChar() const {
  return GetTextInputClient() &&
         (IsTextInputTypeNone() || (!composing_text_ && pending_commit_ &&
                                    pending_commit_->text.length() == 1 &&
                                    pending_commit_->cursor == 1));
}

bool InputMethodAsh::HasInputMethodResult() const {
  return pending_commit_ || composition_changed_;
}

void InputMethodAsh::CommitText(
    const std::u16string& text,
    TextInputClient::InsertTextCursorBehavior cursor_behavior) {
  // We need to receive input method result even if the text input type is
  // `ui::TEXT_INPUT_TYPE_NONE`, to make sure we can always send correct
  // character for each key event to the focused text input client.
  if (!GetTextInputClient()) {
    return;
  }

  if (!GetTextInputClient()->CanComposeInline()) {
    // Hides the candidate window for preedit text.
    UpdateCompositionText(CompositionText(), 0, false);
  }

  // Append the text to the buffer, because commit signal might be fired
  // multiple times when processing a key event.
  if (!pending_commit_) {
    pending_commit_ = PendingCommit();
  }
  pending_commit_->text.insert(pending_commit_->cursor, text);
  if (cursor_behavior ==
      TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText) {
    pending_commit_->cursor += text.length();
  }

  // If we are not handling key event, do not bother sending text result if the
  // focused text input client does not support text input.
  if (!handling_key_event_ && !IsTextInputTypeNone()) {
    if (!SendFakeProcessKeyEvent(true)) {
      GetTextInputClient()->InsertText(text, cursor_behavior);
      typing_session_manager_.CommitCharacters(text.length());
    }
    SendFakeProcessKeyEvent(false);
    pending_commit_ = std::nullopt;
  }
}

void InputMethodAsh::UpdateCompositionText(const CompositionText& text,
                                           uint32_t cursor_pos,
                                           bool visible) {
  if (IsTextInputTypeNone()) {
    return;
  }

  if (!GetTextInputClient()->CanComposeInline()) {
    IMECandidateWindowHandlerInterface* candidate_window =
        IMEBridge::Get()->GetCandidateWindowHandler();
    if (candidate_window) {
      candidate_window->UpdatePreeditText(text.text, cursor_pos, visible);
    }
  }

  // |visible| argument is very confusing. For example, what's the correct
  // behavior when:
  // 1. OnUpdatePreeditText() is called with a text and visible == false, then
  // 2. OnShowPreeditText() is called afterwards.
  //
  // If it's only for clearing the current preedit text, then why not just use
  // OnHidePreeditText()?
  if (!visible) {
    HidePreeditText();
    return;
  }

  pending_composition_ = ExtractCompositionText(text, cursor_pos);
  composition_changed_ = true;

  // In case OnShowPreeditText() is not called.
  if (pending_composition_->text.length()) {
    composing_text_ = true;
  }

  if (!handling_key_event_) {
    // If we receive a composition text without pending key event, then we need
    // to send it to the focused text input client directly.
    if (!SendFakeProcessKeyEvent(true)) {
      GetTextInputClient()->SetCompositionText(*pending_composition_);
    }
    SendFakeProcessKeyEvent(false);
    composition_changed_ = false;
    pending_composition_ = std::nullopt;
  }
}

void InputMethodAsh::HidePreeditText() {
  if (IsTextInputTypeNone()) {
    return;
  }

  // Intentionally leaves |composing_text_| unchanged.
  composition_changed_ = true;
  pending_composition_ = std::nullopt;

  if (!handling_key_event_) {
    TextInputClient* client = GetTextInputClient();
    if (client && client->HasCompositionText()) {
      if (!SendFakeProcessKeyEvent(true)) {
        client->ClearCompositionText();
      }
      SendFakeProcessKeyEvent(false);
    }
    composition_changed_ = false;
  }
}

TextInputMethod::InputContext InputMethodAsh::GetInputContext() const {
  TextInputClient* client = GetTextInputClient();
  if (!client) {
    return TextInputMethod::InputContext(ui::TEXT_INPUT_TYPE_NONE);
  }

  const int flags = client->GetTextInputFlags();
  TextInputMethod::InputContext input_context(
      flags & ui::TEXT_INPUT_FLAG_HAS_BEEN_PASSWORD
          ? ui::TEXT_INPUT_TYPE_PASSWORD
          : client->GetTextInputType());
  input_context.mode = client->GetTextInputMode();
  input_context.autocompletion_mode =
      ConvertTextInputFlagToEnum<AutocompletionMode>(
          flags, ui::TEXT_INPUT_FLAG_AUTOCOMPLETE_ON,
          ui::TEXT_INPUT_FLAG_AUTOCOMPLETE_OFF);
  input_context.autocorrection_mode =
      ConvertTextInputFlagToEnum<AutocorrectionMode>(
          flags, ui::TEXT_INPUT_FLAG_AUTOCORRECT_ON,
          ui::TEXT_INPUT_FLAG_AUTOCORRECT_OFF);
  input_context.autocapitalization_mode = ConvertAutocapitalizationMode(flags);
  input_context.spellcheck_mode = ConvertTextInputFlagToEnum<SpellcheckMode>(
      flags, ui::TEXT_INPUT_FLAG_SPELLCHECK_ON,
      ui::TEXT_INPUT_FLAG_SPELLCHECK_OFF);
  input_context.focus_reason = client->GetFocusReason();
  input_context.personalization_mode = client->ShouldDoLearning()
                                           ? PersonalizationMode::kEnabled
                                           : PersonalizationMode::kDisabled;
  return input_context;
}

void InputMethodAsh::SendKeyEvent(ui::KeyEvent* event) {
  ui::EventDispatchDetails details = DispatchKeyEvent(event);
  DCHECK(!details.dispatcher_destroyed);
}

SurroundingTextInfo InputMethodAsh::GetSurroundingTextInfo() {
  gfx::Range text_range;
  SurroundingTextInfo info;
  TextInputClient* client = GetTextInputClient();
  if (!client || !client->GetTextRange(&text_range) ||
      !client->GetTextFromRange(text_range, &info.surrounding_text) ||
      !client->GetEditableSelectionRange(&info.selection_range)) {
    return SurroundingTextInfo();
  }
  // Makes the |selection_range| be relative to the |surrounding_text|.
  info.selection_range.set_start(info.selection_range.start() -
                                 text_range.start());
  info.selection_range.set_end(info.selection_range.end() - text_range.start());
  info.offset = text_range.start();
  return info;
}

void InputMethodAsh::DeleteSurroundingText(uint32_t num_char16s_before_cursor,
                                           uint32_t num_char16s_after_cursor) {
  if (!GetTextInputClient()) {
    return;
  }

  if (GetTextInputClient()->HasCompositionText()) {
    return;
  }

  GetTextInputClient()->ExtendSelectionAndDelete(num_char16s_before_cursor,
                                                 num_char16s_after_cursor);
}

void InputMethodAsh::ReplaceSurroundingText(
    uint32_t length_before_selection,
    uint32_t length_after_selection,
    std::u16string_view replacement_text) {
  if (!GetTextInputClient()) {
    return;
  }

  GetTextInputClient()->ExtendSelectionAndReplace(
      length_before_selection, length_after_selection, replacement_text);
}

bool InputMethodAsh::ExecuteCharacterComposer(const ui::KeyEvent& event) {
  if (!character_composer_.FilterKeyPress(event)) {
    return false;
  }

  // `ui::CharacterComposer` consumed the key event. Update the composition
  // text.
  CompositionText preedit;
  preedit.text = character_composer_.preedit_string();
  UpdateCompositionText(preedit, preedit.text.size(), !preedit.text.empty());
  const std::u16string& commit_text = character_composer_.composed_character();
  if (!commit_text.empty()) {
    CommitText(commit_text,
               TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  }
  return true;
}

CompositionText InputMethodAsh::ExtractCompositionText(
    const CompositionText& text,
    uint32_t cursor_position) const {
  CompositionText composition;
  composition.text = text.text;

  if (composition.text.empty()) {
    return composition;
  }

  // ibus uses character index for cursor position and attribute range, but we
  // use char16 offset for them. So we need to do conversion here.
  std::vector<size_t> char16_offsets;
  size_t length = composition.text.length();
  for (base::i18n::UTF16CharIterator char_iterator(composition.text);
       !char_iterator.end(); char_iterator.Advance()) {
    char16_offsets.push_back(char_iterator.array_pos());
  }

  // The text length in Unicode characters.
  auto char_length = static_cast<uint32_t>(char16_offsets.size());
  // Make sure we can convert the value of |char_length| as well.
  char16_offsets.push_back(length);

  size_t cursor_offset = char16_offsets[std::min(char_length, cursor_position)];

  composition.selection = gfx::Range(cursor_offset);

  const ui::ImeTextSpans text_ime_text_spans = text.ime_text_spans;
  if (!text_ime_text_spans.empty()) {
    for (const auto& text_ime_text_span : text_ime_text_spans) {
      const uint32_t start = text_ime_text_span.start_offset;
      const uint32_t end = text_ime_text_span.end_offset;
      if (start >= end || end >= char16_offsets.size()) {
        LOG(ERROR) << "IME composition invalid bounds.";
        continue;
      }
      ui::ImeTextSpan ime_text_span(ui::ImeTextSpan::Type::kComposition,
                                    char16_offsets[start], char16_offsets[end],
                                    text_ime_text_span.thickness,
                                    ui::ImeTextSpan::UnderlineStyle::kSolid,
                                    text_ime_text_span.background_color);
      ime_text_span.underline_color = text_ime_text_span.underline_color;
      composition.ime_text_spans.push_back(ime_text_span);
    }
  }

  DCHECK(text.selection.start() <= text.selection.end());
  DCHECK(text.selection.end() <= char_length);
  if (text.selection.start() < text.selection.end()) {
    const size_t start =
        std::min(text.selection.start(), static_cast<size_t>(char_length));
    const size_t end =
        std::min(text.selection.end(), static_cast<size_t>(char_length));
    ui::ImeTextSpan ime_text_span(
        ui::ImeTextSpan::Type::kComposition, char16_offsets[start],
        char16_offsets[end], ui::ImeTextSpan::Thickness::kThick,
        ui::ImeTextSpan::UnderlineStyle::kSolid, SK_ColorTRANSPARENT);
    composition.ime_text_spans.push_back(ime_text_span);

    // If the cursor is at start or end of this ime_text_span, then we treat
    // it as the selection range as well, but make sure to set the cursor
    // position to the selection end.
    if (ime_text_span.start_offset == cursor_offset) {
      composition.selection.set_start(ime_text_span.end_offset);
      composition.selection.set_end(cursor_offset);
    } else if (ime_text_span.end_offset == cursor_offset) {
      composition.selection.set_start(ime_text_span.start_offset);
      composition.selection.set_end(cursor_offset);
    }
  }

  // Use a thin underline with text color by default.
  if (composition.ime_text_spans.empty()) {
    composition.ime_text_spans.push_back(ui::ImeTextSpan(
        ui::ImeTextSpan::Type::kComposition, 0, length,
        ui::ImeTextSpan::Thickness::kThin,
        ui::ImeTextSpan::UnderlineStyle::kSolid, SK_ColorTRANSPARENT));
  }

  return composition;
}

bool InputMethodAsh::IsPasswordOrNoneInputFieldFocused() {
  ui::TextInputType type = GetTextInputType();
  return type == ui::TEXT_INPUT_TYPE_NONE ||
         type == ui::TEXT_INPUT_TYPE_PASSWORD;
}

bool InputMethodAsh::HasCompositionText() {
  TextInputClient* client = GetTextInputClient();
  return client && client->HasCompositionText();
}

ukm::SourceId InputMethodAsh::GetClientSourceForMetrics() {
  TextInputClient* client = GetTextInputClient();
  return client ? client->GetClientSourceForMetrics() : ukm::kInvalidSourceId;
}

ui::InputMethod* InputMethodAsh::GetInputMethod() {
  return this;
}

bool InputMethodAsh::SendFakeProcessKeyEvent(bool pressed) const {
  ui::KeyEvent evt(
      pressed ? ui::EventType::kKeyPressed : ui::EventType::kKeyReleased,
      pressed ? ui::VKEY_PROCESSKEY : ui::VKEY_UNKNOWN,
      ui::EF_IME_FABRICATED_KEY);
  ui::SetKeyboardImeFlags(&evt, ui::kPropertyKeyboardImeHandledFlag);

  std::ignore = DispatchKeyEventPostIME(&evt);
  return evt.stopped_propagation();
}

}  // namespace ash