blob: ce72993c8fe64ef1efa4d3b8e59ffb81eb9a0ca5 [file] [log] [blame]
// Copyright 2018 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/chromeos/arc/input_method_manager/input_connection_impl.h"
#include <tuple>
#include "base/strings/utf_string_conversions.h"
#include "base/threading/thread_task_runner_handle.h"
#include "ui/base/ime/ime_bridge.h"
#include "ui/events/keycodes/keyboard_codes.h"
namespace arc {
namespace {
// Timeout threshold after the IME operation is sent to TextInputClient.
// If no text input state observer methods in below ArcProxyInputMethodObserver
// is called during this time period, the current text input state is sent to
// Android.
// TODO(yhanada): Implement a way to observe an IME operation completion and
// send the current text input state right after the IME operation completion.
constexpr base::TimeDelta kStateUpdateTimeout = base::TimeDelta::FromSeconds(1);
// Characters which should be sent as a KeyEvent and attributes of generated
// KeyEvent.
constexpr std::tuple<char, ui::KeyboardCode, const char*>
kControlCharToKeyEvent[] = {{'\n', ui::VKEY_RETURN, "Enter"}};
bool IsControlChar(const base::string16& text) {
const std::string str = base::UTF16ToUTF8(text);
if (str.length() != 1)
return false;
for (const auto& t : kControlCharToKeyEvent) {
if (str[0] == std::get<0>(t))
return true;
}
return false;
}
ui::TextInputClient* GetTextInputClient() {
ui::IMEBridge* bridge = ui::IMEBridge::Get();
DCHECK(bridge);
ui::IMEInputContextHandlerInterface* handler =
bridge->GetInputContextHandler();
DCHECK(handler);
ui::TextInputClient* client = handler->GetInputMethod()->GetTextInputClient();
DCHECK(client);
return client;
}
} // namespace
InputConnectionImpl::InputConnectionImpl(
chromeos::InputMethodEngine* ime_engine,
ArcInputMethodManagerBridge* imm_bridge,
int input_context_id)
: ime_engine_(ime_engine),
imm_bridge_(imm_bridge),
input_context_id_(input_context_id),
binding_(this),
composing_text_(),
state_update_timer_() {}
InputConnectionImpl::~InputConnectionImpl() = default;
void InputConnectionImpl::Bind(mojom::InputConnectionPtr* interface_ptr) {
binding_.Bind(mojo::MakeRequest(interface_ptr));
}
void InputConnectionImpl::UpdateTextInputState(
bool is_input_state_update_requested) {
if (state_update_timer_.IsRunning()) {
// There is a pending request.
is_input_state_update_requested = true;
}
state_update_timer_.Stop();
imm_bridge_->SendUpdateTextInputState(
GetTextInputState(is_input_state_update_requested));
}
mojom::TextInputStatePtr InputConnectionImpl::GetTextInputState(
bool is_input_state_update_requested) const {
ui::TextInputClient* client = GetTextInputClient();
gfx::Range text_range, selection_range;
base::string16 text;
client->GetTextRange(&text_range);
client->GetEditableSelectionRange(&selection_range);
client->GetTextFromRange(text_range, &text);
return mojom::TextInputStatePtr(
base::in_place, selection_range.start(), text, text_range,
selection_range, client->GetTextInputType(), client->ShouldDoLearning(),
client->GetTextInputFlags(), is_input_state_update_requested);
}
void InputConnectionImpl::CommitText(const base::string16& text,
int new_cursor_pos) {
StartStateUpdateTimer();
std::string error;
// Clear the current composing text at first.
if (!ime_engine_->ClearComposition(input_context_id_, &error))
LOG(ERROR) << "ClearComposition failed: error=\"" << error << "\"";
if (IsControlChar(text)) {
SendControlKeyEvent(text);
return;
}
if (!ime_engine_->CommitText(input_context_id_,
base::UTF16ToUTF8(text).c_str(), &error))
LOG(ERROR) << "CommitText failed: error=\"" << error << "\"";
composing_text_.clear();
}
void InputConnectionImpl::DeleteSurroundingText(int before, int after) {
if (before == 0 && after == 0) {
// This should be no-op.
// Return the current state immediately.
UpdateTextInputState(true);
return;
}
std::string error;
// DeleteSurroundingText takes a start position relative to the current cursor
// position and a length of the text is going to be deleted.
// |before| is a number of characters is going to be deleted before the cursor
// and |after| is a number of characters is going to be deleted after the
// cursor.
if (!ime_engine_->DeleteSurroundingText(input_context_id_, -before,
before + after, &error)) {
LOG(ERROR) << "DeleteSurroundingText failed: before = " << before
<< ", after = " << after << ", error = \"" << error << "\"";
}
}
void InputConnectionImpl::FinishComposingText() {
StartStateUpdateTimer();
if (composing_text_.empty()) {
// There is no ongoing composing. Do nothing.
UpdateTextInputState(true);
return;
}
ui::TextInputClient* client = GetTextInputClient();
gfx::Range selection_range, composition_range;
client->GetEditableSelectionRange(&selection_range);
client->GetCompositionTextRange(&composition_range);
std::string error;
if (!ime_engine_->CommitText(input_context_id_,
base::UTF16ToUTF8(composing_text_).c_str(),
&error)) {
LOG(ERROR) << "FinishComposingText: CommitText() failed, error=\"" << error
<< "\"";
}
composing_text_.clear();
if (selection_range.start() == selection_range.end() &&
selection_range.start() == composition_range.end()) {
// The call of CommitText won't update the state.
// Return the current state immediately.
UpdateTextInputState(true);
}
}
void InputConnectionImpl::SetComposingText(
const base::string16& text,
int new_cursor_pos,
const base::Optional<gfx::Range>& new_selection_range) {
// It's relative to the last character of the composing text,
// so 0 means the cursor should be just before the last character of the text.
new_cursor_pos += text.length() - 1;
const int selection_start = new_selection_range
? new_selection_range.value().start()
: new_cursor_pos;
const int selection_end =
new_selection_range ? new_selection_range.value().end() : new_cursor_pos;
ui::TextInputClient* client = GetTextInputClient();
gfx::Range selection_range;
client->GetEditableSelectionRange(&selection_range);
if (text.empty() &&
selection_range.start() == static_cast<uint32_t>(selection_start) &&
selection_range.end() == static_cast<uint32_t>(selection_end)) {
// This SetComposingText call is no-op.
// Return the current state immediately.
UpdateTextInputState(true);
}
std::string error;
if (!ime_engine_->SetComposition(
input_context_id_, base::UTF16ToUTF8(text).c_str(), selection_start,
selection_end, new_cursor_pos,
std::vector<input_method::InputMethodEngineBase::SegmentInfo>(),
&error)) {
LOG(ERROR) << "SetComposingText failed: pos=" << new_cursor_pos
<< ", error=\"" << error << "\"";
return;
}
composing_text_ = text;
}
void InputConnectionImpl::RequestTextInputState(
mojom::InputConnection::RequestTextInputStateCallback callback) {
std::move(callback).Run(GetTextInputState(false));
}
void InputConnectionImpl::SetSelection(const gfx::Range& new_selection_range) {
ui::TextInputClient* client = GetTextInputClient();
gfx::Range selection_range;
client->GetEditableSelectionRange(&selection_range);
if (new_selection_range == selection_range) {
// This SetSelection call is no-op.
// Return the current state immediately.
UpdateTextInputState(true);
}
StartStateUpdateTimer();
client->SetEditableSelectionRange(new_selection_range);
}
void InputConnectionImpl::StartStateUpdateTimer() {
// It's safe to use Unretained() here because the timer is automatically
// canceled when it go out of scope.
state_update_timer_.Start(
FROM_HERE, kStateUpdateTimeout,
base::BindOnce(&InputConnectionImpl::UpdateTextInputState,
base::Unretained(this),
true /* is_input_state_update_requested */));
}
void InputConnectionImpl::SendControlKeyEvent(const base::string16& text) {
DCHECK(IsControlChar(text));
const std::string str = base::UTF16ToUTF8(text);
DCHECK_EQ(1u, str.length());
for (const auto& t : kControlCharToKeyEvent) {
if (std::get<0>(t) == str[0]) {
chromeos::InputMethodEngine::KeyboardEvent press;
press.type = "keydown";
press.key_code = std::get<1>(t);
press.key = press.code = std::get<2>(t);
chromeos::InputMethodEngine::KeyboardEvent release(press);
release.type = "keyup";
ime_engine_->SendKeyEvents(input_context_id_, {press, release});
break;
}
}
return;
}
} // namespace arc