chromium/components/exo/text_input.cc

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

#include "components/exo/text_input.h"

#include <algorithm>
#include <string_view>
#include <utility>

#include "base/check.h"
#include "base/logging.h"
#include "base/strings/utf_offset_string_conversions.h"
#include "components/exo/seat.h"
#include "components/exo/shell_surface_util.h"
#include "components/exo/surface.h"
#include "components/exo/wm_helper.h"
#include "third_party/icu/source/common/unicode/uchar.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/input_method.h"
#include "ui/base/ime/utf_offset.h"
#include "ui/base/ime/virtual_keyboard_controller.h"
#include "ui/events/event.h"

namespace exo {

namespace {

constexpr int kTextInputSeatObserverPriority = 1;
static_assert(Seat::IsValidObserverPriority(kTextInputSeatObserverPriority),
              "kTextInputSeatObserverPriority is not in the valid range.");

ui::InputMethod* GetInputMethod(aura::Window* window) {
  if (!window || !window->GetHost())
    return nullptr;
  return window->GetHost()->GetInputMethod();
}

bool ShouldUseNullInputType(bool surrounding_text_supported) {
  // TODO(b/273674108): We should be able to tell the IME that the client does
  // not support surrounding text. Instead, we currently disable all IME
  // features by setting input type to null in cases where the IME will not
  // function correctly without surrounding text.
  // Some basic IMEs (incl. EN, DE, FR) are known to be buggy when auto-correct
  // is on and surrounding text is not provided.
  // Complex IMEs (e.g. JA, KO) are not known to be buggy when surrounding text
  // is not provided.

  if (surrounding_text_supported) {
    return false;
  }

  auto* manager = ash::input_method::InputMethodManager::Get();
  scoped_refptr<ash::input_method::InputMethodManager::State> state =
      manager->GetActiveIMEState();
  if (!state) {
    return false;
  }

  return state->GetCurrentInputMethod().id().find("xkb:") != std::string::npos;
}

gfx::Range RemoveOffset(gfx::Range range, size_t offset) {
  return {range.start() - offset, range.end() - offset};
}

}  // namespace

TextInput::TextInput(std::unique_ptr<Delegate> delegate)
    : delegate_(std::move(delegate)) {
  input_method_manager_observation_.Observe(
      ash::input_method::InputMethodManager::Get());
}

TextInput::~TextInput() {
  Deactivate();
}

void TextInput::Activate(Seat* seat,
                         Surface* surface,
                         ui::TextInputClient::FocusReason reason) {
  DCHECK(surface);
  DCHECK(seat);

  focus_reason_ = reason;
  if (surface_ == surface)
    return;
  DetachInputMethod();
  surface_ = surface;
  seat_ = seat;
  seat_->AddObserver(this, kTextInputSeatObserverPriority);
  if (seat_->GetFocusedSurface() == surface_)
    AttachInputMethod();
}

void TextInput::Deactivate() {
  focus_reason_ = ui::TextInputClient::FOCUS_REASON_NONE;
  if (!surface_)
    return;
  DetachInputMethod();
  seat_->RemoveObserver(this);
  surface_ = nullptr;
  seat_ = nullptr;
}

void TextInput::ShowVirtualKeyboardIfEnabled() {
  pending_vk_finalize_ = true;

  // Some clients may ask showing virtual keyboard before sending activation.
  if (!input_method_) {
    pending_vk_visible_ = true;
    return;
  }
  input_method_->SetVirtualKeyboardVisibilityIfEnabled(true);
}

void TextInput::HideVirtualKeyboard() {
  pending_vk_finalize_ = true;

  if (input_method_)
    input_method_->SetVirtualKeyboardVisibilityIfEnabled(false);
  pending_vk_visible_ = false;
}

void TextInput::Resync() {
  if (input_method_)
    input_method_->OnCaretBoundsChanged(this);
}

void TextInput::Reset() {
  surrounding_text_tracker_.CancelComposition();
  if (input_method_)
    input_method_->CancelComposition(this);
}

void TextInput::SetSurroundingText(
    std::u16string_view text,
    uint32_t offset,
    const gfx::Range& cursor_pos,
    const std::optional<ui::GrammarFragment>& grammar_fragment,
    const std::optional<ui::AutocorrectInfo>& autocorrect_info) {
  surrounding_text_tracker_.Update(text, offset, cursor_pos);

  grammar_fragment_at_cursor_ = grammar_fragment;
  if (autocorrect_info.has_value()) {
    autocorrect_info_ = autocorrect_info.value();
  }

  // TODO(b/206068262): Consider introducing an API to notify surrounding text
  // update explicitly.
  if (input_method_)
    input_method_->OnCaretBoundsChanged(this);
}

void TextInput::SetTypeModeFlags(ui::TextInputType type,
                                 ui::TextInputMode mode,
                                 int flags,
                                 bool should_do_learning,
                                 bool can_compose_inline,
                                 bool surrounding_text_supported) {
  if (!input_method_) {
    return;
  }

  bool changed = (input_type_ != type) || (input_mode_ != mode) ||
                 (flags_ != flags) ||
                 (should_do_learning_ != should_do_learning) ||
                 (can_compose_inline_ != can_compose_inline) ||
                 (surrounding_text_supported_ != surrounding_text_supported);
  input_type_ = type;
  input_mode_ = mode;
  flags_ = flags;
  should_do_learning_ = should_do_learning;
  can_compose_inline_ = can_compose_inline;
  surrounding_text_supported_ = surrounding_text_supported;
  use_null_input_type_ = ShouldUseNullInputType(surrounding_text_supported_);
  if (changed)
    input_method_->OnTextInputTypeChanged(this);
}

void TextInput::SetCaretBounds(const gfx::Rect& bounds) {
  if (caret_bounds_ == bounds)
    return;
  caret_bounds_ = bounds;
  if (!input_method_)
    return;
  input_method_->OnCaretBoundsChanged(this);
}

void TextInput::FinalizeVirtualKeyboardChanges() {
  if (staged_vk_visible_) {
    // Order the events so vk bounds is sent while vk is visible.
    if (*staged_vk_visible_) {
      SendStagedVKVisibility();
      SendStagedVKOccludedBounds();
    } else {
      SendStagedVKOccludedBounds();
      SendStagedVKVisibility();
    }
  }

  if (staged_vk_occluded_bounds_) {
    SendStagedVKOccludedBounds();
  }

  pending_vk_finalize_ = false;
}

base::WeakPtr<ui::TextInputClient> TextInput::AsWeakPtr() {
  return weak_ptr_factory_.GetWeakPtr();
}

void TextInput::SetCompositionText(const ui::CompositionText& composition) {
  delegate_->SetCompositionText(composition);
  surrounding_text_tracker_.OnSetCompositionText(composition);
}

size_t TextInput::ConfirmCompositionText(bool keep_selection) {
  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  const auto& [surrounding_text, utf16_offset, cursor_pos, composition] =
      predicted_state;

  if (!delegate_->ConfirmComposition(keep_selection)) {
    // Fallback to SetCursor and Commit if ConfirmComposition is not supported.
    // TODO(b/265853952): Remove once all versions of Lacros supports
    // ConfirmComposition.
    if (keep_selection && cursor_pos.IsValid() &&
        cursor_pos.IsBoundedBy(predicted_state.GetSurroundingTextRange())) {
      delegate_->SetCursor(surrounding_text,
                           RemoveOffset(cursor_pos, utf16_offset));
    }

    delegate_->Commit(
        predicted_state.GetCompositionText().value_or(std::u16string_view()));
  }

  // Preserve the result value before updating the tracker's state.
  const size_t composition_text_length = composition.length();
  surrounding_text_tracker_.OnConfirmCompositionText(keep_selection);
  return composition_text_length;
}

void TextInput::ClearCompositionText() {
  const auto composition =
      surrounding_text_tracker_.predicted_state().composition;
  if (composition.is_empty())
    return;
  delegate_->SetCompositionText(ui::CompositionText{});
  surrounding_text_tracker_.OnClearCompositionText();
}

void TextInput::InsertText(const std::u16string& text,
                           InsertTextCursorBehavior cursor_behavior) {
  // TODO(crbug.com/1155331): Handle |cursor_behavior| correctly.
  delegate_->Commit(text);
  surrounding_text_tracker_.OnInsertText(
      text, InsertTextCursorBehavior::kMoveCursorAfterText);
}

void TextInput::InsertChar(const ui::KeyEvent& event) {
  if (ConsumedByIme(event)) {
    // TODO(b/240618514): Short term workaround to accept temporary fix in IME
    // for urgent production breakage.
    // We should come up with the proper solution of what to be done.
    if (event.code() == ui::DomCode::NONE) {
      // On some specific cases, IME use InsertChar, even if there's no clear
      // key mapping from key_code. Then, use InsertText().
      InsertText(std::u16string(1u, event.GetCharacter()),
                 InsertTextCursorBehavior::kMoveCursorAfterText);
    } else {
      delegate_->SendKey(event);
    }
  }
}

bool TextInput::CanInsertImage() {
  return delegate_->HasImageInsertSupport() &&
         input_type_ == ui::TEXT_INPUT_TYPE_CONTENT_EDITABLE;
}

void TextInput::InsertImage(const GURL& src) {
  if (CanInsertImage()) {
    delegate_->InsertImage(src);
  }
}

ui::TextInputType TextInput::GetTextInputType() const {
  return use_null_input_type_ ? ui::TEXT_INPUT_TYPE_NULL : input_type_;
}

ui::TextInputMode TextInput::GetTextInputMode() const {
  return input_mode_;
}

base::i18n::TextDirection TextInput::GetTextDirection() const {
  return direction_;
}

int TextInput::GetTextInputFlags() const {
  return flags_;
}

bool TextInput::CanComposeInline() const {
  return can_compose_inline_;
}

gfx::Rect TextInput::GetCaretBounds() const {
  return caret_bounds_ +
         surface_->window()->GetBoundsInScreen().OffsetFromOrigin();
}

gfx::Rect TextInput::GetSelectionBoundingBox() const {
  NOTIMPLEMENTED();
  return gfx::Rect();
}

bool TextInput::GetCompositionCharacterBounds(size_t index,
                                              gfx::Rect* rect) const {
  return false;
}

bool TextInput::HasCompositionText() const {
  return !surrounding_text_tracker_.predicted_state().composition.is_empty();
}

ui::TextInputClient::FocusReason TextInput::GetFocusReason() const {
  return focus_reason_;
}

bool TextInput::GetTextRange(gfx::Range* range) const {
  DCHECK(range);
  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  DCHECK(predicted_state.selection.IsValid());

  *range = predicted_state.GetSurroundingTextRange();
  return true;
}

bool TextInput::GetCompositionTextRange(gfx::Range* range) const {
  DCHECK(range);
  const auto& composition =
      surrounding_text_tracker_.predicted_state().composition;
  if (composition.is_empty())
    return false;

  *range = composition;
  return true;
}

bool TextInput::GetEditableSelectionRange(gfx::Range* range) const {
  DCHECK(range);
  const auto& selection = surrounding_text_tracker_.predicted_state().selection;
  DCHECK(selection.IsValid());

  *range = selection;
  return true;
}

bool TextInput::SetEditableSelectionRange(const gfx::Range& range) {
  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  std::optional<std::u16string_view> composition_text =
      predicted_state.GetCompositionText();
  if (!range.IsBoundedBy(predicted_state.GetSurroundingTextRange()) ||
      !composition_text.has_value()) {
    return false;
  }

  // Send a SetCursor followed by a Commit of the current composition text, or
  // empty string if there is no composition text. This is necessary since
  // SetCursor only takes effect on the following Commit.
  delegate_->SetCursor(predicted_state.surrounding_text,
                       RemoveOffset(range, predicted_state.utf16_offset));
  delegate_->Commit(*composition_text);
  surrounding_text_tracker_.OnSetEditableSelectionRange(range);
  return true;
}

bool TextInput::GetTextFromRange(const gfx::Range& range,
                                 std::u16string* text) const {
  DCHECK(text);
  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  if (!range.IsBoundedBy(predicted_state.GetSurroundingTextRange())) {
    return false;
  }

  text->assign(predicted_state.surrounding_text,
               range.GetMin() - predicted_state.utf16_offset, range.length());
  return true;
}

void TextInput::OnInputMethodChanged() {
  // This observer method does not signify anything meaningful. When the user
  // switches input method, |InputMethodChanged()| is triggered instead of
  // this, and the ui::InputMethod we are attached to is a singleton which does
  // not change.
}

bool TextInput::ChangeTextDirectionAndLayoutAlignment(
    base::i18n::TextDirection direction) {
  if (direction == direction_)
    return true;
  direction_ = direction;
  delegate_->OnTextDirectionChanged(direction_);
  return true;
}

void TextInput::ExtendSelectionAndDelete(size_t before, size_t after) {
  const auto& [surrounding_text, utf16_offset, selection, unused_composition] =
      surrounding_text_tracker_.predicted_state();

  DCHECK(selection.IsValid());

  size_t utf16_start =
      selection.GetMin() - std::min(before, selection.GetMin());
  size_t utf16_end = std::min(selection.GetMax() + after,
                              surrounding_text.length() + utf16_offset);

  delegate_->DeleteSurroundingText(
      surrounding_text,
      gfx::Range(utf16_start - utf16_offset, utf16_end - utf16_offset));
  surrounding_text_tracker_.OnExtendSelectionAndDelete(before, after);
}

void TextInput::ExtendSelectionAndReplace(
    size_t before,
    size_t after,
    const std::u16string_view replacement_text) {
  // TODO(crbug.com/40267455): Implement this using an extended Wayland API.
  NOTIMPLEMENTED_LOG_ONCE();
}

void TextInput::EnsureCaretNotInRect(const gfx::Rect& rect) {
  if (ShouldStageVKState()) {
    staged_vk_occluded_bounds_ = rect;
    return;
  }
  delegate_->OnVirtualKeyboardOccludedBoundsChanged(rect);
}

bool TextInput::IsTextEditCommandEnabled(ui::TextEditCommand command) const {
  return false;
}

void TextInput::SetTextEditCommandForNextKeyEvent(ui::TextEditCommand command) {
}

ukm::SourceId TextInput::GetClientSourceForMetrics() const {
  NOTIMPLEMENTED_LOG_ONCE();
  return ukm::kInvalidSourceId;
}

bool TextInput::ShouldDoLearning() {
  return should_do_learning_;
}

bool TextInput::SetCompositionFromExistingText(
    const gfx::Range& range,
    const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) {
  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  const gfx::Range surrounding_text_range =
      predicted_state.GetSurroundingTextRange();
  DCHECK(predicted_state.selection.IsValid());
  if (!range.IsBoundedBy(surrounding_text_range) ||
      !predicted_state.selection.IsBoundedBy(surrounding_text_range)) {
    return false;
  }

  const auto composition_length = range.length();
  for (const auto& span : ui_ime_text_spans) {
    if (composition_length < std::max(span.start_offset, span.end_offset)) {
      return false;
    }
  }

  const size_t utf16_offset = predicted_state.utf16_offset;
  delegate_->SetCompositionFromExistingText(
      predicted_state.surrounding_text,
      RemoveOffset(predicted_state.selection, utf16_offset),
      RemoveOffset(range, utf16_offset), ui_ime_text_spans);
  surrounding_text_tracker_.OnSetCompositionFromExistingText(range);
  return true;
}

gfx::Range TextInput::GetAutocorrectRange() const {
  return autocorrect_info_.range;
}

gfx::Rect TextInput::GetAutocorrectCharacterBounds() const {
  return autocorrect_info_.bounds;
}

bool TextInput::SetAutocorrectRange(const gfx::Range& range) {
  if (range.is_empty()) {
    delegate_->SetAutocorrectRange(u"", range);
    return true;
  }

  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  if (!range.IsBoundedBy(predicted_state.GetSurroundingTextRange())) {
    return false;
  }

  delegate_->SetAutocorrectRange(
      predicted_state.surrounding_text,
      RemoveOffset(range, predicted_state.utf16_offset));
  return true;
}

std::optional<ui::GrammarFragment> TextInput::GetGrammarFragmentAtCursor()
    const {
  return grammar_fragment_at_cursor_;
}

bool TextInput::ClearGrammarFragments(const gfx::Range& range) {
  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  if (!range.IsBoundedBy(predicted_state.GetSurroundingTextRange())) {
    return false;
  }

  delegate_->ClearGrammarFragments(
      predicted_state.surrounding_text,
      RemoveOffset(range, predicted_state.utf16_offset));
  return true;
}

bool TextInput::AddGrammarFragments(
    const std::vector<ui::GrammarFragment>& fragments) {
  const auto& predicted_state = surrounding_text_tracker_.predicted_state();
  const gfx::Range surrounding_text_range =
      predicted_state.GetSurroundingTextRange();

  for (const auto& fragment : fragments) {
    if (!fragment.range.IsBoundedBy(surrounding_text_range)) {
      continue;
    }

    delegate_->AddGrammarFragment(
        predicted_state.surrounding_text,
        ui::GrammarFragment(
            RemoveOffset(fragment.range, predicted_state.utf16_offset),
            fragment.suggestion));
  }
  return true;
}

bool TextInput::SupportsAlwaysConfirmComposition() {
  return delegate_->SupportsConfirmPreedit();
}

void GetActiveTextInputControlLayoutBounds(
    std::optional<gfx::Rect>* control_bounds,
    std::optional<gfx::Rect>* selection_bounds) {
  NOTIMPLEMENTED_LOG_ONCE();
}

void TextInput::OnKeyboardVisible(const gfx::Rect& keyboard_rect) {
  if (ShouldStageVKState()) {
    staged_vk_visible_ = true;
    // Bounds are now stale, so clear it.
    staged_vk_occluded_bounds_.reset();
    return;
  }
  delegate_->OnVirtualKeyboardVisibilityChanged(true);
}

void TextInput::OnKeyboardHidden() {
  if (ShouldStageVKState()) {
    staged_vk_occluded_bounds_ = gfx::Rect();
    staged_vk_visible_ = false;
    return;
  }
  delegate_->OnVirtualKeyboardOccludedBoundsChanged({});
  delegate_->OnVirtualKeyboardVisibilityChanged(false);
}

// This is called when the user switches input method.
void TextInput::InputMethodChanged(
    ash::input_method::InputMethodManager* manager,
    Profile* profile,
    bool show_message) {
  ui::TextInputType old_input_type = GetTextInputType();
  use_null_input_type_ = ShouldUseNullInputType(surrounding_text_supported_);
  if (input_method_ && GetTextInputType() != old_input_type) {
    input_method_->OnTextInputTypeChanged(this);
  }
}

void TextInput::OnSurfaceFocused(Surface* gained_focus,
                                 Surface* lost_focus,
                                 bool has_focused_surface) {
  DCHECK(surface_);
  if (gained_focus == lost_focus)
    return;

  if (gained_focus == surface_) {
    AttachInputMethod();
  } else if (lost_focus == surface_) {
    Deactivate();
  }
}

void TextInput::AttachInputMethod() {
  DCHECK(!input_method_);
  DCHECK(surface_);
  input_method_ = GetInputMethod(surface_->window());
  if (!input_method_) {
    LOG(ERROR) << "input method not found";
    return;
  }

  input_mode_ = ui::TEXT_INPUT_MODE_TEXT;
  input_type_ = ui::TEXT_INPUT_TYPE_TEXT;
  if (auto* controller = input_method_->GetVirtualKeyboardController())
    virtual_keyboard_observation_.Observe(controller);
  input_method_->SetFocusedTextInputClient(this);
  delegate_->Activated();

  if (pending_vk_visible_) {
    input_method_->SetVirtualKeyboardVisibilityIfEnabled(true);
    pending_vk_visible_ = false;
  }
}

void TextInput::DetachInputMethod() {
  if (!input_method_)
    return;
  input_mode_ = ui::TEXT_INPUT_MODE_DEFAULT;
  input_type_ = ui::TEXT_INPUT_TYPE_NONE;
  input_method_->DetachTextInputClient(this);
  virtual_keyboard_observation_.Reset();
  input_method_ = nullptr;
  delegate_->Deactivated();
}

bool TextInput::ShouldStageVKState() {
  return delegate_->SupportsFinalizeVirtualKeyboardChanges() &&
         pending_vk_finalize_;
}

void TextInput::SendStagedVKVisibility() {
  if (staged_vk_visible_) {
    delegate_->OnVirtualKeyboardVisibilityChanged(*staged_vk_visible_);
    staged_vk_visible_.reset();
  }
}

void TextInput::SendStagedVKOccludedBounds() {
  if (staged_vk_occluded_bounds_) {
    delegate_->OnVirtualKeyboardOccludedBoundsChanged(
        *staged_vk_occluded_bounds_);
    staged_vk_occluded_bounds_.reset();
  }
}

}  // namespace exo