chromium/ui/views/cocoa/text_input_host.mm

// 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 "ui/views/cocoa/text_input_host.h"

#include "components/remote_cocoa/app_shim/native_widget_ns_window_bridge.h"
#include "ui/accelerated_widget_mac/window_resize_helper_mac.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/views/cocoa/native_widget_mac_ns_window_host.h"

namespace {

// Returns the boundary rectangle for composition characters in the
// |requested_range|. Sets |actual_range| corresponding to the returned
// rectangle. For cases, where there is no composition text or the
// |requested_range| lies outside the composition range, a zero width rectangle
// corresponding to the caret bounds is returned. Logic used is similar to
// RenderWidgetHostViewMac::GetCachedFirstRectForCharacterRange(...).
gfx::Rect GetFirstRectForRangeHelper(const ui::TextInputClient* client,
                                     const gfx::Range& requested_range,
                                     gfx::Range* actual_range) {
  // NSRange doesn't support reversed ranges.
  DCHECK(!requested_range.is_reversed());
  DCHECK(actual_range);

  // Set up default return values, to be returned in case of unusual cases.
  gfx::Rect default_rect;
  *actual_range = gfx::Range::InvalidRange();
  if (!client)
    return default_rect;

  default_rect = client->GetCaretBounds();
  default_rect.set_width(0);

  // If possible, modify actual_range to correspond to caret position.
  gfx::Range selection_range;
  if (client->GetEditableSelectionRange(&selection_range)) {
    // Caret bounds correspond to end index of selection_range.
    *actual_range = gfx::Range(selection_range.end());
  }

  gfx::Range composition_range;
  if (!client->HasCompositionText() ||
      !client->GetCompositionTextRange(&composition_range) ||
      !requested_range.IsBoundedBy(composition_range)) {
    return default_rect;
  }

  DCHECK(!composition_range.is_reversed());

  const size_t from = requested_range.start() - composition_range.start();
  const size_t to = requested_range.end() - composition_range.start();

  // Pick the first character's bounds as the initial rectangle, then grow it to
  // the full |requested_range| if possible.
  const bool request_is_composition_end = from == composition_range.length();
  const size_t first_index = request_is_composition_end ? from - 1 : from;
  gfx::Rect union_rect;
  if (!client->GetCompositionCharacterBounds(first_index, &union_rect))
    return default_rect;

  // If requested_range is empty, return a zero width rectangle corresponding to
  // it.
  if (from == to) {
    if (request_is_composition_end &&
        client->GetTextDirection() != base::i18n::RIGHT_TO_LEFT) {
      // In case of an empty requested range at end of composition, return the
      // rectangle to the right of the last compositioned character.
      union_rect.set_origin(union_rect.top_right());
    }
    union_rect.set_width(0);
    *actual_range = requested_range;
    return union_rect;
  }

  // Toolkit-views textfields are always single-line, so no need to check for
  // line breaks.
  for (size_t i = from + 1; i < to; i++) {
    gfx::Rect current_rect;
    if (client->GetCompositionCharacterBounds(i, &current_rect)) {
      union_rect.Union(current_rect);
    } else {
      *actual_range =
          gfx::Range(requested_range.start(), i + composition_range.start());
      return union_rect;
    }
  }
  *actual_range = requested_range;
  return union_rect;
}

// Returns the string corresponding to |requested_range| for the given |client|.
// If a gfx::Range::InvalidRange() is passed, the full string stored by |client|
// is returned. Sets |actual_range| corresponding to the returned string.
std::u16string AttributedSubstringForRangeHelper(
    const ui::TextInputClient* client,
    const gfx::Range& requested_range,
    gfx::Range* actual_range) {
  // NSRange doesn't support reversed ranges.
  DCHECK(!requested_range.is_reversed());
  DCHECK(actual_range);

  std::u16string substring;
  gfx::Range text_range;
  *actual_range = gfx::Range::InvalidRange();
  if (!client || !client->GetTextRange(&text_range))
    return substring;

  // gfx::Range::Intersect() behaves a bit weirdly. If B is an empty range
  // contained inside a non-empty range A, B intersection A returns
  // gfx::Range::InvalidRange(), instead of returning B.
  *actual_range = text_range.Contains(requested_range)
                      ? requested_range
                      : text_range.Intersect(requested_range);

  // This is a special case for which the complete string should should be
  // returned. NSTextView also follows this, though the same is not mentioned in
  // NSTextInputClient documentation.
  if (!requested_range.IsValid())
    *actual_range = text_range;

  client->GetTextFromRange(*actual_range, &substring);
  return substring;
}

}  // namespace

namespace views {

////////////////////////////////////////////////////////////////////////////////
// TextInputHost, public:

TextInputHost::TextInputHost(NativeWidgetMacNSWindowHost* host_impl)
    : host_impl_(host_impl) {}

TextInputHost::~TextInputHost() = default;

void TextInputHost::BindReceiver(
    mojo::PendingAssociatedReceiver<remote_cocoa::mojom::TextInputHost>
        receiver) {
  mojo_receiver_.Bind(std::move(receiver),
                      ui::WindowResizeHelperMac::Get()->task_runner());
}

ui::TextInputClient* TextInputHost::GetTextInputClient() const {
  return text_input_client_;
}

void TextInputHost::SetTextInputClient(
    ui::TextInputClient* new_text_input_client) {
  if (pending_text_input_client_ == new_text_input_client)
    return;

  // This method may cause the IME window to dismiss, which may cause it to
  // insert text (e.g. to replace marked text with "real" text). That should
  // happen in the old -inputContext (which AppKit stores a reference to).
  // Unfortunately, the only way to invalidate the the old -inputContext is to
  // invoke -[NSApp updateWindows], which also wants a reference to the _new_
  // -inputContext. So put the new inputContext in |pendingTextInputClient_| and
  // only use it for -inputContext.
  ui::TextInputClient* old_text_input_client = text_input_client_;

  // Since dismissing an IME may insert text, a misbehaving IME or a
  // ui::TextInputClient that acts on InsertChar() to change focus a second time
  // may invoke -setTextInputClient: recursively; with [NSApp updateWindows]
  // still on the stack. Calling [NSApp updateWindows] recursively may upset
  // an IME. Since the rest of this method is only to decide whether to call
  // updateWindows, and we're already calling it, just bail out.
  if (text_input_client_ != pending_text_input_client_) {
    pending_text_input_client_ = new_text_input_client;
    return;
  }

  // Start by assuming no need to invoke -updateWindows.
  text_input_client_ = new_text_input_client;
  pending_text_input_client_ = new_text_input_client;

  if (host_impl_->in_process_ns_window_bridge_ &&
      host_impl_->in_process_ns_window_bridge_->NeedsUpdateWindows()) {
    text_input_client_ = old_text_input_client;
    [NSApp updateWindows];
    // Note: |pending_text_input_client_| (and therefore +[NSTextInputContext
    // currentInputContext] may have changed if called recursively.
    text_input_client_ = pending_text_input_client_;
  }
}

////////////////////////////////////////////////////////////////////////////////
// TextInputHost, remote_cocoa::mojom::TextInputHost:

bool TextInputHost::HasClient(bool* out_has_client) {
  *out_has_client = text_input_client_ != nullptr;
  return true;
}

bool TextInputHost::HasInputContext(bool* out_has_input_context) {
  *out_has_input_context = false;

  // If the textInputClient_ does not exist, return nil since this view does not
  // conform to NSTextInputClient protocol.
  if (!pending_text_input_client_)
    return true;

  // If a menu is active, and -[NSView interpretKeyEvents:] asks for the
  // input context, return nil. This ensures the action message is sent to
  // the view, rather than any NSTextInputClient a subview has installed.
  bool has_menu_controller = false;
  host_impl_->GetHasMenuController(&has_menu_controller);
  if (has_menu_controller)
    return true;

  // When not in an editable mode, or while entering passwords
  // (http://crbug.com/23219), we don't want to show IME candidate windows.
  // Returning nil prevents this view from getting messages defined as part of
  // the NSTextInputClient protocol.
  switch (pending_text_input_client_->GetTextInputType()) {
    case ui::TEXT_INPUT_TYPE_NONE:
    case ui::TEXT_INPUT_TYPE_PASSWORD:
      return true;
    default:
      *out_has_input_context = true;
  }
  return true;
}

bool TextInputHost::IsRTL(bool* out_is_rtl) {
  *out_is_rtl = text_input_client_ && text_input_client_->GetTextDirection() ==
                                          base::i18n::RIGHT_TO_LEFT;
  return true;
}

bool TextInputHost::GetSelectionRange(gfx::Range* out_range) {
  if (!text_input_client_ ||
      !text_input_client_->GetEditableSelectionRange(out_range)) {
    *out_range = gfx::Range::InvalidRange();
  }
  return true;
}

bool TextInputHost::GetSelectionText(bool* out_result,
                                     std::u16string* out_text) {
  *out_result = false;
  if (!text_input_client_)
    return true;
  gfx::Range selection_range;
  if (!text_input_client_->GetEditableSelectionRange(&selection_range))
    return true;
  *out_result = text_input_client_->GetTextFromRange(selection_range, out_text);
  return true;
}

void TextInputHost::InsertText(const std::u16string& text, bool as_character) {
  if (!text_input_client_)
    return;
  if (as_character) {
    // If a single character is inserted by keyDown's call to
    // interpretKeyEvents: then use InsertChar() to allow editing events to be
    // merged. We use ui::VKEY_UNKNOWN as the key code since it's not feasible
    // to determine the correct key code for each unicode character. Also a
    // correct keycode is not needed in the current context. Send ui::EF_NONE as
    // the key modifier since |text| already accounts for the pressed key
    // modifiers.
    text_input_client_->InsertChar(ui::KeyEvent::FromCharacter(
        text[0], ui::VKEY_UNKNOWN, ui::DomCode::NONE, ui::EF_NONE));
  } else {
    text_input_client_->InsertText(
        text,
        ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
  }
}

void TextInputHost::DeleteRange(const gfx::Range& range) {
  if (!text_input_client_)
    return;
  text_input_client_->DeleteRange(range);
}

void TextInputHost::SetCompositionText(const std::u16string& text,
                                       const gfx::Range& selected_range,
                                       const gfx::Range& replacement_range) {
  if (!text_input_client_)
    return;

  text_input_client_->DeleteRange(replacement_range);
  ui::CompositionText composition;
  composition.text = text;
  composition.selection = selected_range;

  // Add an underline with text color and a transparent background to the
  // composition text. TODO(karandeepb): On Cocoa textfields, the target clause
  // of the composition has a thick underlines. The composition text also has
  // discontinuous underlines for different clauses. This is also supported in
  // the Chrome renderer. Add code to extract underlines from |text| once our
  // render text implementation supports thick underlines and discontinuous
  // underlines for consecutive characters. See http://crbug.com/612675.
  composition.ime_text_spans.emplace_back(
      ui::ImeTextSpan::Type::kComposition, 0, text.length(),
      ui::ImeTextSpan::Thickness::kThin,
      ui::ImeTextSpan::UnderlineStyle::kSolid, SK_ColorTRANSPARENT);
  text_input_client_->SetCompositionText(composition);
}

void TextInputHost::ConfirmCompositionText() {
  if (!text_input_client_)
    return;
  text_input_client_->ConfirmCompositionText(/* keep_selection */ false);
}

bool TextInputHost::HasCompositionText(bool* out_has_composition_text) {
  *out_has_composition_text = false;
  if (!text_input_client_)
    return true;
  *out_has_composition_text = text_input_client_->HasCompositionText();
  return true;
}

bool TextInputHost::GetCompositionTextRange(gfx::Range* out_composition_range) {
  *out_composition_range = gfx::Range::InvalidRange();
  if (!text_input_client_)
    return true;
  if (!text_input_client_->HasCompositionText())
    return true;
  text_input_client_->GetCompositionTextRange(out_composition_range);
  return true;
}

bool TextInputHost::GetAttributedSubstringForRange(
    const gfx::Range& requested_range,
    std::u16string* out_text,
    gfx::Range* out_actual_range) {
  *out_text = AttributedSubstringForRangeHelper(
      text_input_client_, requested_range, out_actual_range);
  return true;
}

bool TextInputHost::GetFirstRectForRange(const gfx::Range& requested_range,
                                         gfx::Rect* out_rect,
                                         gfx::Range* out_actual_range) {
  *out_rect = GetFirstRectForRangeHelper(text_input_client_, requested_range,
                                         out_actual_range);
  return true;
}

bool TextInputHost::IsTextEditCommandEnabled(ui::TextEditCommand command,
                                             bool* out_enabled) {
  if (text_input_client_) {
    *out_enabled = text_input_client_->IsTextEditCommandEnabled(command);
  } else {
    *out_enabled = false;
  }
  return true;
}

void TextInputHost::SetTextEditCommandForNextKeyEvent(
    ui::TextEditCommand command) {
  if (text_input_client_) {
    text_input_client_->SetTextEditCommandForNextKeyEvent(command);
  }
}

////////////////////////////////////////////////////////////////////////////////
// TextInputHost, remote_cocoa::mojom::TextInputHost synchronous methods:

void TextInputHost::HasClient(HasClientCallback callback) {
  bool has_client = false;
  HasClient(&has_client);
  std::move(callback).Run(has_client);
}

void TextInputHost::HasInputContext(HasInputContextCallback callback) {
  bool has_input_context = false;
  HasClient(&has_input_context);
  std::move(callback).Run(has_input_context);
}

void TextInputHost::IsRTL(IsRTLCallback callback) {
  bool is_rtl = false;
  IsRTL(&is_rtl);
  std::move(callback).Run(is_rtl);
}

void TextInputHost::GetSelectionRange(GetSelectionRangeCallback callback) {
  gfx::Range range = gfx::Range::InvalidRange();
  GetSelectionRange(&range);
  std::move(callback).Run(range);
}

void TextInputHost::GetSelectionText(GetSelectionTextCallback callback) {
  bool result = false;
  std::u16string text;
  GetSelectionText(&result, &text);
  std::move(callback).Run(result, text);
}

void TextInputHost::HasCompositionText(HasCompositionTextCallback callback) {
  bool has_composition_text = false;
  HasCompositionText(&has_composition_text);
  std::move(callback).Run(has_composition_text);
}

void TextInputHost::GetCompositionTextRange(
    GetCompositionTextRangeCallback callback) {
  gfx::Range range = gfx::Range::InvalidRange();
  GetCompositionTextRange(&range);
  std::move(callback).Run(range);
}

void TextInputHost::GetAttributedSubstringForRange(
    const gfx::Range& requested_range,
    GetAttributedSubstringForRangeCallback callback) {
  std::u16string text;
  gfx::Range actual_range = gfx::Range::InvalidRange();
  GetAttributedSubstringForRange(requested_range, &text, &actual_range);
  std::move(callback).Run(text, actual_range);
}

void TextInputHost::GetFirstRectForRange(
    const gfx::Range& requested_range,
    GetFirstRectForRangeCallback callback) {
  gfx::Rect rect;
  gfx::Range actual_range;
  GetFirstRectForRange(requested_range, &rect, &actual_range);
  std::move(callback).Run(rect, actual_range);
}

void TextInputHost::IsTextEditCommandEnabled(
    ui::TextEditCommand command,
    IsTextEditCommandEnabledCallback callback) {
  bool enable;
  IsTextEditCommandEnabled(command, &enable);
  std::move(callback).Run(enable);
}

}  // namespace views