// Copyright 2013 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "chrome/browser/ash/input_method/input_method_engine.h"
#include <algorithm>
#include <memory>
#include <string_view>
#include <utility>
#include "ash/constants/ash_pref_names.h"
#include "ash/constants/notifier_catalogs.h"
#include "ash/keyboard/ui/keyboard_ui_controller.h"
#include "ash/public/cpp/system/anchored_nudge_manager.h"
#include "base/check.h"
#include "base/i18n/char_iterator.h"
#include "base/logging.h"
#include "base/metrics/histogram_macros.h"
#include "base/notreached.h"
#include "base/strings/string_number_conversions.h"
#include "base/strings/string_util.h"
#include "base/strings/stringprintf.h"
#include "base/strings/utf_string_conversions.h"
#include "chrome/browser/ui/ash/input_method/input_method_menu_item.h"
#include "chrome/browser/ui/ash/input_method/input_method_menu_manager.h"
#include "chrome/browser/ui/ash/keyboard/chrome_keyboard_controller_client.h"
#include "chrome/common/pref_names.h"
#include "components/prefs/pref_service.h"
#include "ui/aura/window.h"
#include "ui/aura/window_tree_host.h"
#include "ui/base/ime/ash/component_extension_ime_manager.h"
#include "ui/base/ime/ash/extension_ime_util.h"
#include "ui/base/ime/ash/ime_bridge.h"
#include "ui/base/ime/ash/ime_keymap.h"
#include "ui/base/ime/constants.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/base/ui_base_features.h"
#include "ui/events/event_sink.h"
#include "ui/events/event_utils.h"
#include "ui/events/keycodes/dom/dom_code.h"
#include "ui/events/keycodes/dom/keycode_converter.h"
namespace ash {
namespace input_method {
namespace {
const char kErrorNotActive[] = "IME is not active.";
const char kErrorWrongContext[] = "Context is not active.";
const char kErrorInvalidValue[] = "Argument '%s' with value '%d' is not valid.";
const char kCandidateNotFound[] = "Candidate not found.";
const char kSuggestionNotFound[] = "Suggestion not found.";
// The default entry number of a page in CandidateWindowProperty.
const int kDefaultPageSize = 9;
bool IsUint32Value(int i) {
return 0 <= i && i <= std::numeric_limits<uint32_t>::max();
}
int GetUtf16Size(std::u16string& text) {
int utf16_size = 0;
for (base::i18n::UTF16CharIterator char_iterator(text); !char_iterator.end();
char_iterator.Advance()) {
++utf16_size;
}
return utf16_size;
}
} // namespace
InputMethodEngine::Candidate::Candidate() = default;
InputMethodEngine::Candidate::Candidate(const Candidate& other) = default;
InputMethodEngine::Candidate::~Candidate() = default;
// When the default values are changed, please modify
// CandidateWindow::CandidateWindowProperty defined in chromeos/ime/ too.
InputMethodEngine::CandidateWindowProperty::CandidateWindowProperty()
: page_size(kDefaultPageSize),
is_cursor_visible(true),
is_vertical(false),
show_window_at_composition(false),
is_auxiliary_text_visible(false) {}
InputMethodEngine::CandidateWindowProperty::~CandidateWindowProperty() =
default;
InputMethodEngine::CandidateWindowProperty::CandidateWindowProperty(
const CandidateWindowProperty& other) = default;
InputMethodEngine::InputMethodEngine()
: current_input_type_(ui::TEXT_INPUT_TYPE_NONE),
context_id_(0),
next_context_id_(1),
profile_(nullptr),
composition_changed_(false),
commit_text_changed_(false),
pref_change_registrar_(nullptr),
// This is safe because the callback does not outlive
// screen_projection_change_monitor_, which is a member of this class.
screen_projection_change_monitor_(
base::BindRepeating(&InputMethodEngine::OnScreenProjectionChanged,
base::Unretained(this))) {}
InputMethodEngine::~InputMethodEngine() = default;
void InputMethodEngine::Initialize(
std::unique_ptr<InputMethodEngineObserver> observer,
const char* extension_id,
Profile* profile) {
DCHECK(observer) << "Observer must not be null.";
// TODO(komatsu): It is probably better to set observer out of Initialize.
observer_ = std::move(observer);
extension_id_ = extension_id;
profile_ = profile;
if (profile_ && profile->GetPrefs()) {
profile_observation_.Observe(profile);
input_method_settings_snapshot_ =
profile->GetPrefs()
->GetDict(::prefs::kLanguageInputMethodSpecificSettings)
.Clone();
pref_change_registrar_ = std::make_unique<PrefChangeRegistrar>();
pref_change_registrar_->Init(profile->GetPrefs());
pref_change_registrar_->Add(
::prefs::kLanguageInputMethodSpecificSettings,
base::BindRepeating(&InputMethodEngine::OnInputMethodOptionsChanged,
base::Unretained(this)));
pref_change_registrar_->Add(
ash::prefs::kLongPressDiacriticsEnabled,
base::BindRepeating(&InputMethodEngine::DiacriticsSettingsChanged,
base::Unretained(this)));
}
}
void InputMethodEngine::DiacriticsSettingsChanged() {
const bool new_value =
profile_->GetPrefs()->GetBoolean(ash::prefs::kLongPressDiacriticsEnabled);
if (!new_value) {
AnchoredNudgeManager::Get()->MaybeRecordNudgeAction(
NudgeCatalogName::kDisableDiacritics);
}
}
const std::string& InputMethodEngine::GetActiveComponentId() const {
return active_component_id_;
}
bool InputMethodEngine::ClearComposition(int context_id, std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
UpdateComposition(ui::CompositionText(), 0, false);
return true;
}
bool InputMethodEngine::CommitText(int context_id,
const std::u16string& text,
std::string* error) {
if (!IsActive()) {
// TODO: Commit the text anyways.
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
CommitTextToInputContext(context_id, text);
return true;
}
void InputMethodEngine::ConfirmComposition(bool reset_engine) {
TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler();
if (input_context) {
input_context->ConfirmComposition(reset_engine);
}
}
bool InputMethodEngine::DeleteSurroundingText(int context_id,
int offset,
size_t number_of_chars,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
// TODO(nona): Return false if there is ongoing composition.
TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler();
if (input_context) {
const uint32_t before =
offset >= 0 ? 0U : static_cast<uint32_t>(-1 * offset);
input_context->DeleteSurroundingText(before, number_of_chars - before);
}
return true;
}
base::expected<void, InputMethodEngine::Error>
InputMethodEngine::ReplaceSurroundingText(
int context_id,
int length_before_selection,
int length_after_selection,
const std::u16string_view replacement_text) {
if (!IsActive()) {
return base::unexpected(Error::kInputMethodNotActive);
}
if (context_id != context_id_ || context_id_ == -1) {
return base::unexpected(Error::kIncorrectContextId);
}
if (TextInputTarget* input_context =
IMEBridge::Get()->GetInputContextHandler()) {
input_context->ReplaceSurroundingText(
length_before_selection, length_after_selection, replacement_text);
}
return base::ok();
}
bool InputMethodEngine::FinishComposingText(int context_id,
std::string* error) {
if (context_id != context_id_ || context_id_ == -1) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
ConfirmComposition(/* reset_engine */ false);
return true;
}
bool InputMethodEngine::SendKeyEvents(int context_id,
const std::vector<ui::KeyEvent>& events,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
// context_id == 0, means sending key events to non-input field.
// context_id_ == -1, means the focus is not in an input field.
if ((context_id != 0 && (context_id != context_id_ || context_id_ == -1))) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
for (const auto& event : events) {
ui::KeyEvent event_copy = event;
// Marks the simulated key event is from the Virtual Keyboard.
ui::Event::Properties properties;
properties[ui::kPropertyFromVK] =
std::vector<uint8_t>(ui::kPropertyFromVKSize);
properties[ui::kPropertyFromVK][ui::kPropertyFromVKIsMirroringIndex] =
static_cast<uint8_t>(screen_projection_change_monitor_.is_mirroring());
event_copy.SetProperties(properties);
TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler();
if (input_context) {
input_context->SendKeyEvent(&event_copy);
continue;
}
*error = kErrorWrongContext;
return false;
}
return true;
}
bool InputMethodEngine::SetComposition(int context_id,
const char* text,
int selection_start,
int selection_end,
int cursor,
const std::vector<SegmentInfo>& segments,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
if (selection_start < 0 || selection_end < 0 || cursor < 0) {
*error = base::StringPrintf(
"%s request selection start = %d, selection end = %d, cursor = %d",
"At least 1 arg is negative, which is invalid.", selection_start,
selection_end, cursor);
return false;
}
ui::CompositionText composition_text;
composition_text.text = base::UTF8ToUTF16(text);
// Check the length of the text.
int utf16_length = GetUtf16Size(composition_text.text);
if (selection_start > utf16_length || selection_end > utf16_length) {
*error = base::StringPrintf(
"%s request selection start = %d, selection end = %d, cursor = %d",
"At least 1 length is above the length of the text, which is invalid.",
selection_start, selection_end, cursor);
return false;
}
composition_text.selection.set_start(selection_start);
composition_text.selection.set_end(selection_end);
// TODO: Add support for displaying selected text in the composition string.
for (auto segment : segments) {
ui::ImeTextSpan ime_text_span;
ime_text_span.underline_color = SK_ColorTRANSPARENT;
switch (segment.style) {
case SEGMENT_STYLE_UNDERLINE:
ime_text_span.thickness = ui::ImeTextSpan::Thickness::kThin;
break;
case SEGMENT_STYLE_DOUBLE_UNDERLINE:
ime_text_span.thickness = ui::ImeTextSpan::Thickness::kThick;
break;
case SEGMENT_STYLE_NO_UNDERLINE:
ime_text_span.thickness = ui::ImeTextSpan::Thickness::kNone;
break;
default:
continue;
}
ime_text_span.start_offset = segment.start;
ime_text_span.end_offset = segment.end;
composition_text.ime_text_spans.push_back(ime_text_span);
}
// TODO(nona): Makes focus out mode configuable, if necessary.
UpdateComposition(composition_text, cursor, true);
return true;
}
bool InputMethodEngine::SetCompositionRange(
int context_id,
int selection_before,
int selection_after,
const std::vector<SegmentInfo>& segments,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
// When there is composition text, commit it to the text field first before
// changing the composition range.
ConfirmComposition(/* reset_engine */ false);
std::vector<ui::ImeTextSpan> text_spans;
for (const auto& segment : segments) {
ui::ImeTextSpan text_span;
text_span.underline_color = SK_ColorTRANSPARENT;
switch (segment.style) {
case SEGMENT_STYLE_UNDERLINE:
text_span.thickness = ui::ImeTextSpan::Thickness::kThin;
break;
case SEGMENT_STYLE_DOUBLE_UNDERLINE:
text_span.thickness = ui::ImeTextSpan::Thickness::kThick;
break;
case SEGMENT_STYLE_NO_UNDERLINE:
text_span.thickness = ui::ImeTextSpan::Thickness::kNone;
break;
}
text_span.start_offset = segment.start;
text_span.end_offset = segment.end;
text_spans.push_back(text_span);
}
if (!IsUint32Value(selection_before)) {
*error = base::StringPrintf(kErrorInvalidValue, "selection_before",
selection_before);
return false;
}
if (!IsUint32Value(selection_after)) {
*error = base::StringPrintf(kErrorInvalidValue, "selection_after",
selection_after);
return false;
}
TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler();
if (!input_context) {
return false;
}
return input_context->SetCompositionRange(
static_cast<uint32_t>(selection_before),
static_cast<uint32_t>(selection_after), text_spans);
}
bool InputMethodEngine::SetComposingRange(
int context_id,
int start,
int end,
const std::vector<SegmentInfo>& segments,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
// When there is composition text, commit it to the text field first before
// changing the composition range.
ConfirmComposition(/* reset_engine */ false);
std::vector<ui::ImeTextSpan> text_spans;
for (const auto& segment : segments) {
ui::ImeTextSpan text_span;
text_span.underline_color = SK_ColorTRANSPARENT;
switch (segment.style) {
case SEGMENT_STYLE_UNDERLINE:
text_span.thickness = ui::ImeTextSpan::Thickness::kThin;
break;
case SEGMENT_STYLE_DOUBLE_UNDERLINE:
text_span.thickness = ui::ImeTextSpan::Thickness::kThick;
break;
case SEGMENT_STYLE_NO_UNDERLINE:
text_span.thickness = ui::ImeTextSpan::Thickness::kNone;
break;
}
text_span.start_offset = segment.start;
text_span.end_offset = segment.end;
text_spans.push_back(text_span);
}
if (!IsUint32Value(start)) {
*error = base::StringPrintf(kErrorInvalidValue, "start", start);
return false;
}
if (!IsUint32Value(end)) {
*error = base::StringPrintf(kErrorInvalidValue, "end", end);
return false;
}
TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler();
if (!input_context) {
return false;
}
return input_context->SetComposingRange(
static_cast<uint32_t>(start), static_cast<uint32_t>(end), text_spans);
}
void InputMethodEngine::KeyEventHandled(const std::string& extension_id,
const std::string& request_id,
bool handled) {
// When finish handling key event, take care of the unprocessed commitText
// and setComposition calls.
if (commit_text_changed_) {
CommitTextToInputContext(context_id_, text_);
text_.clear();
commit_text_changed_ = false;
}
if (composition_changed_) {
UpdateComposition(composition_, composition_.selection.start(), true);
composition_ = ui::CompositionText();
composition_changed_ = false;
}
const auto it = pending_key_events_.find(request_id);
if (it == pending_key_events_.end()) {
LOG(ERROR) << "Request ID not found: " << request_id;
return;
}
std::move(it->second.callback)
.Run(handled ? ui::ime::KeyEventHandledState::kHandledByIME
: ui::ime::KeyEventHandledState::kNotHandled);
pending_key_events_.erase(it);
}
std::string InputMethodEngine::AddPendingKeyEvent(
const std::string& component_id,
TextInputMethod::KeyEventDoneCallback callback) {
std::string request_id = base::NumberToString(next_request_id_);
++next_request_id_;
pending_key_events_.emplace(
request_id, PendingKeyEvent(component_id, std::move(callback)));
return request_id;
}
void InputMethodEngine::CancelPendingKeyEvents() {
for (auto& event : pending_key_events_) {
std::move(event.second.callback)
.Run(ui::ime::KeyEventHandledState::kNotHandled);
}
pending_key_events_.clear();
}
void InputMethodEngine::Focus(
const TextInputMethod::InputContext& input_context) {
current_input_type_ = input_context.type;
if (!IsActive() || current_input_type_ == ui::TEXT_INPUT_TYPE_NONE) {
return;
}
context_id_ = next_context_id_;
++next_context_id_;
observer_->OnFocus(active_component_id_, context_id_, input_context);
}
void InputMethodEngine::Blur() {
if (!IsActive() || current_input_type_ == ui::TEXT_INPUT_TYPE_NONE) {
return;
}
current_input_type_ = ui::TEXT_INPUT_TYPE_NONE;
int context_id = context_id_;
context_id_ = -1;
observer_->OnBlur(active_component_id_, context_id);
}
void InputMethodEngine::Enable(const std::string& component_id) {
active_component_id_ = component_id;
observer_->OnActivate(component_id);
const TextInputMethod::InputContext& input_context =
IMEBridge::Get()->GetCurrentInputContext();
current_input_type_ = input_context.type;
Focus(input_context);
InputMethodManager::Get()->GetActiveIMEState()->EnableInputView();
auto* keyboard_client = ChromeKeyboardControllerClient::Get();
if (keyboard_client->is_keyboard_enabled()) {
keyboard_client->ReloadKeyboardIfNeeded();
}
// Resets candidate_window_property_ whenever a new component_id (aka
// engine_id) is enabled.
candidate_window_property_ = {component_id,
InputMethodEngine::CandidateWindowProperty()};
is_ready_for_testing_ = false;
}
bool InputMethodEngine::IsActive() const {
return !active_component_id_.empty();
}
void InputMethodEngine::Disable() {
std::string last_component_id{active_component_id_};
active_component_id_.clear();
ConfirmComposition(/* reset_engine */ true);
observer_->OnDeactivated(last_component_id);
}
void InputMethodEngine::Reset() {
observer_->OnReset(active_component_id_);
}
void InputMethodEngine::ProcessKeyEvent(const ui::KeyEvent& key_event,
KeyEventDoneCallback callback) {
if (key_event.IsCommandDown()) {
std::move(callback).Run(ui::ime::KeyEventHandledState::kNotHandled);
return;
}
// Should not pass key event in password field.
if (current_input_type_ != ui::TEXT_INPUT_TYPE_PASSWORD) {
// Bind the start time to the callback so that we can calculate the latency
// when the callback is called.
observer_->OnKeyEvent(
active_component_id_, key_event,
base::BindOnce(
[](base::Time start, int context_id, int* context_id_ptr,
KeyEventDoneCallback callback,
ui::ime::KeyEventHandledState handled_state) {
// If the input_context has changed, assume the key event is
// invalid as a precaution.
if (context_id == *context_id_ptr) {
std::move(callback).Run(handled_state);
}
UMA_HISTOGRAM_TIMES("InputMethod.KeyEventLatency",
base::Time::Now() - start);
},
base::Time::Now(), context_id_, &context_id_, std::move(callback)));
}
}
void InputMethodEngine::SetSurroundingText(const std::u16string& text,
const gfx::Range selection_range,
uint32_t offset_pos) {
observer_->OnSurroundingTextChanged(active_component_id_, text,
selection_range,
static_cast<int>(offset_pos));
}
void InputMethodEngine::SetCaretBounds(const gfx::Rect& caret_bounds) {
observer_->OnCaretBoundsChanged(caret_bounds);
}
void InputMethodEngine::PropertyActivate(const std::string& property_name) {
observer_->OnMenuItemActivated(active_component_id_, property_name);
}
void InputMethodEngine::CandidateClicked(uint32_t index) {
if (index > candidate_ids_.size()) {
return;
}
// Only left button click is supported at this moment.
observer_->OnCandidateClicked(active_component_id_, candidate_ids_.at(index),
MOUSE_BUTTON_LEFT);
}
void InputMethodEngine::AssistiveWindowButtonClicked(
const ui::ime::AssistiveWindowButton& button) {
observer_->OnAssistiveWindowButtonClicked(button);
}
void InputMethodEngine::AssistiveWindowChanged(
const ash::ime::AssistiveWindow& window) {
observer_->OnAssistiveWindowChanged(window);
}
ui::VirtualKeyboardController* InputMethodEngine::GetVirtualKeyboardController()
const {
// Callers expect a nullptr when the keyboard is disabled. See
// https://crbug.com/850020.
if (!keyboard::KeyboardUIController::HasInstance() ||
!keyboard::KeyboardUIController::Get()->IsEnabled()) {
return nullptr;
}
return keyboard::KeyboardUIController::Get()->virtual_keyboard_controller();
}
bool InputMethodEngine::IsReadyForTesting() {
return is_ready_for_testing_;
}
void InputMethodEngine::OnSuggestionsChanged(
const std::vector<std::string>& suggestions) {
observer_->OnSuggestionsChanged(suggestions);
}
bool InputMethodEngine::SetButtonHighlighted(
int context_id,
const ui::ime::AssistiveWindowButton& button,
bool highlighted,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
IMEAssistiveWindowHandlerInterface* aw_handler =
IMEBridge::Get()->GetAssistiveWindowHandler();
if (aw_handler) {
aw_handler->SetButtonHighlighted(button, highlighted);
}
return true;
}
void InputMethodEngine::ClickButton(
const ui::ime::AssistiveWindowButton& button) {
observer_->OnAssistiveWindowButtonClicked(button);
}
bool InputMethodEngine::AcceptSuggestionCandidate(
int context_id,
const std::u16string& suggestion,
size_t delete_previous_utf16_len,
bool use_replace_surrounding_text,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
if (use_replace_surrounding_text) {
if (delete_previous_utf16_len) {
if (base::expected<void, Error> result = ReplaceSurroundingText(
context_id_, delete_previous_utf16_len, 0, suggestion);
!result.has_value()) {
switch (result.error()) {
case Error::kInputMethodNotActive:
*error = kErrorNotActive;
return false;
case Error::kIncorrectContextId:
*error = base::StringPrintf(
"%s request context id = %d, current context id = %d",
kErrorWrongContext, context_id, context_id_);
return false;
}
}
} else {
CommitText(context_id, suggestion, error);
}
} else {
if (delete_previous_utf16_len) {
DeleteSurroundingText(context_id_,
-static_cast<int>(delete_previous_utf16_len),
delete_previous_utf16_len, error);
}
CommitText(context_id, suggestion, error);
}
IMEAssistiveWindowHandlerInterface* aw_handler =
IMEBridge::Get()->GetAssistiveWindowHandler();
if (aw_handler) {
aw_handler->AcceptSuggestion(suggestion);
}
return true;
}
const InputMethodEngine::CandidateWindowProperty&
InputMethodEngine::GetCandidateWindowProperty(const std::string& engine_id) {
if (candidate_window_property_.first != engine_id) {
candidate_window_property_ = {engine_id,
InputMethodEngine::CandidateWindowProperty()};
}
return candidate_window_property_.second;
}
void InputMethodEngine::SetCandidateWindowProperty(
const std::string& engine_id,
const CandidateWindowProperty& property) {
// Type conversion from InputMethodEngine::CandidateWindowProperty to
// CandidateWindow::CandidateWindowProperty defined in chromeos/ime/.
ui::CandidateWindow::CandidateWindowProperty dest_property;
dest_property.page_size = property.page_size;
dest_property.is_cursor_visible = property.is_cursor_visible;
dest_property.is_vertical = property.is_vertical;
dest_property.show_window_at_composition =
property.show_window_at_composition;
dest_property.cursor_position =
candidate_window_.GetProperty().cursor_position;
dest_property.auxiliary_text = property.auxiliary_text;
dest_property.is_auxiliary_text_visible = property.is_auxiliary_text_visible;
dest_property.current_candidate_index = property.current_candidate_index;
dest_property.total_candidates = property.total_candidates;
dest_property.is_user_selecting = candidate_window_.is_user_selecting();
candidate_window_.SetProperty(dest_property);
candidate_window_property_ = {engine_id, property};
if (IsActive()) {
IMECandidateWindowHandlerInterface* cw_handler =
IMEBridge::Get()->GetCandidateWindowHandler();
if (cw_handler) {
if (window_visible_) {
cw_handler->UpdateLookupTable(candidate_window_);
} else {
cw_handler->HideLookupTable();
}
}
}
}
bool InputMethodEngine::SetCandidateWindowVisible(bool visible,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
window_visible_ = visible;
IMECandidateWindowHandlerInterface* cw_handler =
IMEBridge::Get()->GetCandidateWindowHandler();
if (cw_handler) {
if (window_visible_) {
cw_handler->UpdateLookupTable(candidate_window_);
} else {
cw_handler->HideLookupTable();
}
}
return true;
}
bool InputMethodEngine::SetCandidates(int context_id,
const std::vector<Candidate>& candidates,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
// TODO: Nested candidates
candidate_ids_.clear();
candidate_indexes_.clear();
candidate_window_.mutable_candidates()->clear();
for (const auto& candidate : candidates) {
ui::CandidateWindow::Entry entry;
entry.value = base::UTF8ToUTF16(candidate.value);
entry.label = base::UTF8ToUTF16(candidate.label);
entry.annotation = base::UTF8ToUTF16(candidate.annotation);
entry.description_title = base::UTF8ToUTF16(candidate.usage.title);
entry.description_body = base::UTF8ToUTF16(candidate.usage.body);
// Store a mapping from the user defined ID to the candidate index.
candidate_indexes_[candidate.id] = candidate_ids_.size();
candidate_ids_.push_back(candidate.id);
candidate_window_.mutable_candidates()->push_back(entry);
}
candidate_window_.set_is_user_selecting(InferIsUserSelecting(candidates));
if (IsActive()) {
IMECandidateWindowHandlerInterface* cw_handler =
IMEBridge::Get()->GetCandidateWindowHandler();
if (cw_handler) {
if (window_visible_) {
cw_handler->UpdateLookupTable(candidate_window_);
} else {
cw_handler->HideLookupTable();
}
}
}
return true;
}
bool InputMethodEngine::SetCursorPosition(int context_id,
int candidate_id,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
std::map<int, int>::const_iterator position =
candidate_indexes_.find(candidate_id);
if (position == candidate_indexes_.end()) {
*error = base::StringPrintf("%s candidate id = %d", kCandidateNotFound,
candidate_id);
return false;
}
candidate_window_.set_cursor_position(position->second);
IMECandidateWindowHandlerInterface* cw_handler =
IMEBridge::Get()->GetCandidateWindowHandler();
if (cw_handler) {
if (window_visible_) {
cw_handler->UpdateLookupTable(candidate_window_);
} else {
cw_handler->HideLookupTable();
}
}
return true;
}
bool InputMethodEngine::SetSuggestion(int context_id,
const ui::ime::SuggestionDetails& details,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
IMEAssistiveWindowHandlerInterface* aw_handler =
IMEBridge::Get()->GetAssistiveWindowHandler();
if (aw_handler) {
aw_handler->ShowSuggestion(details);
}
return true;
}
bool InputMethodEngine::DismissSuggestion(int context_id, std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
IMEAssistiveWindowHandlerInterface* aw_handler =
IMEBridge::Get()->GetAssistiveWindowHandler();
if (aw_handler) {
aw_handler->HideSuggestion();
}
return true;
}
bool InputMethodEngine::AcceptSuggestion(int context_id, std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
FinishComposingText(context_id_, error);
if (!error->empty()) {
return false;
}
IMEAssistiveWindowHandlerInterface* aw_handler =
IMEBridge::Get()->GetAssistiveWindowHandler();
if (aw_handler) {
std::u16string suggestion_text = aw_handler->GetSuggestionText();
if (suggestion_text.empty()) {
*error = kSuggestionNotFound;
return false;
}
size_t confirmed_length = aw_handler->GetConfirmedLength();
if (confirmed_length > 0) {
DeleteSurroundingText(context_id_, -static_cast<int>(confirmed_length),
confirmed_length, error);
}
CommitText(context_id_, suggestion_text, error);
aw_handler->HideSuggestion();
}
return true;
}
bool InputMethodEngine::SetAssistiveWindowProperties(
int context_id,
const AssistiveWindowProperties& assistive_window,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
if (context_id != context_id_ || context_id_ == -1) {
*error = kErrorWrongContext;
return false;
}
IMEAssistiveWindowHandlerInterface* aw_handler =
IMEBridge::Get()->GetAssistiveWindowHandler();
if (aw_handler) {
aw_handler->SetAssistiveWindowProperties(assistive_window);
}
return true;
}
void InputMethodEngine::Announce(const std::u16string& message) {
IMEAssistiveWindowHandlerInterface* aw_handler =
IMEBridge::Get()->GetAssistiveWindowHandler();
if (aw_handler) {
aw_handler->Announce(message);
}
}
void InputMethodEngine::OnProfileWillBeDestroyed(Profile* profile) {
if (profile == profile_) {
pref_change_registrar_.reset();
DCHECK(profile_observation_.IsObservingSource(profile_));
profile_observation_.Reset();
profile_ = nullptr;
}
}
bool InputMethodEngine::UpdateMenuItems(
const std::vector<InputMethodManager::MenuItem>& items,
std::string* error) {
if (!IsActive()) {
*error = kErrorNotActive;
return false;
}
ui::ime::InputMethodMenuItemList menu_item_list;
for (const auto& item : items) {
ui::ime::InputMethodMenuItem property;
MenuItemToProperty(item, &property);
menu_item_list.push_back(property);
}
ui::ime::InputMethodMenuManager::GetInstance()
->SetCurrentInputMethodMenuItemList(menu_item_list);
InputMethodManager::Get()->NotifyImeMenuItemsChanged(active_component_id_,
items);
return true;
}
void InputMethodEngine::HideInputView() {
auto* keyboard_client = ChromeKeyboardControllerClient::Get();
if (keyboard_client->is_keyboard_enabled()) {
keyboard_client->HideKeyboard(ash::HideReason::kUser);
}
}
void InputMethodEngine::OnInputMethodOptionsChanged() {
const base::Value::Dict& new_settings = profile_->GetPrefs()->GetDict(
::prefs::kLanguageInputMethodSpecificSettings);
const base::Value::Dict& old_settings = input_method_settings_snapshot_;
for (const auto&& [path, value] : new_settings) {
if (const base::Value* old_value = old_settings.Find(path)) {
if (*old_value != value) {
observer_->OnInputMethodOptionsChanged(path);
}
} else {
observer_->OnInputMethodOptionsChanged(path);
}
}
input_method_settings_snapshot_ = new_settings.Clone();
}
void InputMethodEngine::UpdateComposition(
const ui::CompositionText& composition_text,
uint32_t cursor_pos,
bool is_visible) {
TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler();
if (input_context) {
input_context->UpdateCompositionText(composition_text, cursor_pos,
is_visible);
}
}
void InputMethodEngine::CommitTextToInputContext(int context_id,
const std::u16string& text) {
TextInputTarget* input_context = IMEBridge::Get()->GetInputContextHandler();
if (!input_context) {
return;
}
const bool had_composition_text = input_context->HasCompositionText();
input_context->CommitText(
text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
if (had_composition_text) {
// Records histograms for committed characters with composition text.
UMA_HISTOGRAM_CUSTOM_COUNTS("InputMethod.CommitLength", text.length(), 1,
25, 25);
}
}
// TODO(uekawa): rename this method to a more reasonable name.
void InputMethodEngine::MenuItemToProperty(
const InputMethodManager::MenuItem& item,
ui::ime::InputMethodMenuItem* property) {
property->key = item.id;
if (item.modified & MENU_ITEM_MODIFIED_LABEL) {
property->label = item.label;
}
if (item.modified & MENU_ITEM_MODIFIED_CHECKED) {
property->is_selection_item_checked = item.checked;
}
// TODO(nona): Support item.children.
}
void InputMethodEngine::OnScreenProjectionChanged(bool is_projected) {
if (observer_) {
observer_->OnScreenProjectionChanged(is_projected);
}
}
bool InputMethodEngine::InferIsUserSelecting(
base::span<const Candidate> candidates) {
if (candidates.empty()) {
return false;
}
// Only infer for Japanese IME.
if (!active_component_id_.starts_with("nacl_mozc_")) {
return true;
}
const bool any_non_empty_label = base::ranges::any_of(
candidates,
[](const Candidate& candidate) { return !candidate.label.empty(); });
return any_non_empty_label;
}
void InputMethodEngine::NotifyInputMethodExtensionReadyForTesting() {
is_ready_for_testing_ = true;
}
InputMethodEngine::PendingKeyEvent::PendingKeyEvent(
const std::string& component_id,
TextInputMethod::KeyEventDoneCallback callback)
: component_id(component_id), callback(std::move(callback)) {}
InputMethodEngine::PendingKeyEvent::PendingKeyEvent(PendingKeyEvent&& other) =
default;
InputMethodEngine::PendingKeyEvent::~PendingKeyEvent() = default;
} // namespace input_method
} // namespace ash