#include "ui/base/ime/linux/input_method_auralinux.h"
#include "base/auto_reset.h"
#include "base/environment.h"
#include "base/functional/bind.h"
#include "base/strings/utf_offset_string_conversions.h"
#include "base/strings/utf_string_conversions.h"
#include "build/chromeos_buildflags.h"
#include "ui/base/ime/constants.h"
#include "ui/base/ime/linux/linux_input_method_context_factory.h"
#include "ui/base/ime/text_input_client.h"
#include "ui/base/ime/text_input_flags.h"
#include "ui/events/event.h"
namespace {
constexpr base::TimeDelta kIgnoreCommitsDuration = base::Milliseconds(100);
bool IsEventFromVK(const ui::KeyEvent& event) {
if (event.HasNativeEvent())
return false;
const auto* properties = event.properties();
return properties &&
properties->find(ui::kPropertyFromVK) != properties->end();
}
bool IsSameKeyEvent(const ui::KeyEvent& lhs, const ui::KeyEvent& rhs) {
return lhs.type() == rhs.type() && lhs.code() == rhs.code() &&
(lhs.flags() & ~ui::EF_IS_REPEAT) == (rhs.flags() & ~ui::EF_IS_REPEAT);
}
}
namespace ui {
InputMethodAuraLinux::InputMethodAuraLinux(
ImeKeyEventDispatcher* ime_key_event_dispatcher)
: InputMethodBase(ime_key_event_dispatcher),
text_input_type_(TEXT_INPUT_TYPE_NONE),
is_sync_mode_(false),
composition_changed_(false) {
context_ = CreateLinuxInputMethodContext(this);
}
InputMethodAuraLinux::~InputMethodAuraLinux() = default;
LinuxInputMethodContext* InputMethodAuraLinux::GetContextForTesting() {
return context_.get();
}
ui::EventDispatchDetails InputMethodAuraLinux::DispatchKeyEvent(
ui::KeyEvent* event) {
DCHECK(event->type() == ET_KEY_PRESSED || event->type() == ET_KEY_RELEASED);
if (ime_filtered_key_event_.has_value() &&
!IsSameKeyEvent(*ime_filtered_key_event_, *event) &&
ime_filtered_key_event_->GetDomKey().IsDeadKey()) {
std::ignore = DispatchKeyEventPostIME(&*ime_filtered_key_event_);
}
ime_filtered_key_event_.reset();
if (!GetTextInputClient()) {
if (event->type() == ET_KEY_PRESSED && context_->IsPeekKeyEvent(*event)) {
ime_filtered_key_event_ = std::move(*event);
return ui::EventDispatchDetails();
}
return DispatchKeyEventPostIME(event);
}
if (IsEventFromVK(*event)) {
ui::EventDispatchDetails details = DispatchKeyEventPostIME(event);
if (details.dispatcher_destroyed || details.target_destroyed ||
event->stopped_propagation()) {
return details;
}
if ((event->is_char() || event->GetDomKey().IsCharacter()) &&
event->type() == ui::ET_KEY_PRESSED) {
GetTextInputClient()->InsertChar(*event);
}
return details;
}
bool filtered = false;
{
suppress_non_key_input_until_ = base::TimeTicks::UnixEpoch();
composition_changed_ = false;
last_commit_result_.reset();
result_text_ = absl::nullopt;
base::AutoReset<bool> flipper(&is_sync_mode_, true);
filtered = context_->DispatchKeyEvent(*event);
}
if (filtered && !HasInputMethodResult()
#if !BUILDFLAG(IS_CHROMEOS_LACROS)
&& !IsTextInputTypeNone()
#endif
) {
ime_filtered_key_event_ = std::move(*event);
return ui::EventDispatchDetails();
}
ui::EventDispatchDetails details;
if (event->type() == ui::ET_KEY_PRESSED && filtered) {
details = DispatchImeFilteredKeyPressEvent(event);
if (details.target_destroyed || details.dispatcher_destroyed ||
event->stopped_propagation()) {
return details;
}
}
const auto commit_result = MaybeCommitResult(filtered, *event);
if (commit_result == CommitResult::kTargetDestroyed) {
details.target_destroyed = true;
event->StopPropagation();
return details;
}
bool should_stop_propagation = commit_result == CommitResult::kSuccess;
should_stop_propagation |=
MaybeUpdateComposition(commit_result == CommitResult::kSuccess);
if (!filtered) {
details = DispatchKeyEventPostIME(event);
if (details.dispatcher_destroyed) {
if (should_stop_propagation)
event->StopPropagation();
return details;
}
if (event->stopped_propagation() || details.target_destroyed) {
ResetContext();
} else if (event->type() == ui::ET_KEY_PRESSED) {
char16_t ch = event->GetCharacter();
if (ch && GetTextInputClient())
GetTextInputClient()->InsertChar(*event);
should_stop_propagation = true;
}
}
if (should_stop_propagation)
event->StopPropagation();
return details;
}
ui::EventDispatchDetails InputMethodAuraLinux::DispatchImeFilteredKeyPressEvent(
ui::KeyEvent* event) {
ui::EventDispatchDetails details;
if (event->key_code() == VKEY_RETURN)
details = SendFakeProcessKeyEvent(event);
else {
details = NeedInsertChar(result_text_) ? DispatchKeyEventPostIME(event)
: SendFakeProcessKeyEvent(event);
}
if (details.dispatcher_destroyed)
return details;
if (event->stopped_propagation() || details.target_destroyed)
ResetContext();
return details;
}
InputMethodAuraLinux::CommitResult InputMethodAuraLinux::MaybeCommitResult(
bool filtered,
const KeyEvent& event) {
TextInputClient* client = GetTextInputClient();
if (!client || !result_text_)
return CommitResult::kNoCommitString;
std::u16string result_text = std::move(*result_text_);
result_text_ = absl::nullopt;
if (filtered && NeedInsertChar(result_text)) {
for (const auto ch : result_text) {
ui::KeyEvent ch_event(event);
ch_event.set_character(ch);
client->InsertChar(ch_event);
if (client != GetTextInputClient())
return CommitResult::kTargetDestroyed;
}
} else {
client->InsertText(
result_text,
ui::TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText);
if (client != GetTextInputClient())
return CommitResult::kTargetDestroyed;
}
return CommitResult::kSuccess;
}
bool InputMethodAuraLinux::MaybeUpdateComposition(bool text_committed) {
TextInputClient* client = GetTextInputClient();
bool update_composition =
client && composition_changed_ && !IsTextInputTypeNone();
if (update_composition) {
if (!composition_.text.empty())
client->SetCompositionText(composition_);
else if (!text_committed)
client->ClearCompositionText();
}
if (client && !client->HasCompositionText())
composition_ = CompositionText();
return update_composition;
}
void InputMethodAuraLinux::UpdateContextFocusState() {
surrounding_text_.reset();
text_range_ = gfx::Range::InvalidRange();
selection_range_ = gfx::Range::InvalidRange();
auto old_text_input_type = text_input_type_;
text_input_type_ = GetTextInputType();
auto* client = GetTextInputClient();
bool has_client = client != nullptr;
TextInputClient::FocusReason reason;
if (client) {
reason = client->GetFocusReason();
} else {
reason = text_input_type_ == TEXT_INPUT_TYPE_NONE
? TextInputClient::FocusReason::FOCUS_REASON_NONE
: TextInputClient::FocusReason::FOCUS_REASON_OTHER;
}
context_->UpdateFocus(has_client, old_text_input_type, text_input_type_,
reason);
TextInputMode mode = TEXT_INPUT_MODE_DEFAULT;
int flags = TEXT_INPUT_FLAG_NONE;
bool should_do_learning = false;
bool can_compose_inline = true;
if (client) {
mode = client->GetTextInputMode();
flags = client->GetTextInputFlags();
should_do_learning = client->ShouldDoLearning();
can_compose_inline = client->CanComposeInline();
}
context_->SetContentType(text_input_type_, mode, flags, should_do_learning,
can_compose_inline);
}
void InputMethodAuraLinux::OnTextInputTypeChanged(TextInputClient* client) {
UpdateContextFocusState();
InputMethodBase::OnTextInputTypeChanged(client);
}
void InputMethodAuraLinux::OnCaretBoundsChanged(const TextInputClient* client) {
if (!IsTextInputClientFocused(client))
return;
NotifyTextInputCaretBoundsChanged(client);
context_->SetCursorLocation(GetTextInputClient()->GetCaretBounds());
gfx::Range text_range, selection_range;
std::u16string text;
if (client->GetTextRange(&text_range) &&
client->GetTextFromRange(text_range, &text) &&
client->GetEditableSelectionRange(&selection_range)) {
absl::optional<GrammarFragment> fragment;
absl::optional<AutocorrectInfo> autocorrect;
#if BUILDFLAG(IS_CHROMEOS_LACROS)
fragment = client->GetGrammarFragmentAtCursor();
autocorrect = AutocorrectInfo{
client->GetAutocorrectRange(),
client->GetAutocorrectCharacterBounds(),
};
#endif
if (surrounding_text_ != text || text_range_ != text_range ||
selection_range_ != selection_range) {
surrounding_text_ = text;
text_range_ = text_range;
selection_range_ = selection_range;
context_->SetSurroundingText(text, text_range, selection_range, fragment,
autocorrect);
}
}
}
void InputMethodAuraLinux::CancelComposition(const TextInputClient* client) {
if (!IsTextInputClientFocused(client))
return;
ResetContext();
}
void InputMethodAuraLinux::ResetContext() {
if (!GetTextInputClient())
return;
is_sync_mode_ = true;
if (!composition_.text.empty()) {
suppress_non_key_input_until_ =
base::TimeTicks::Now() + kIgnoreCommitsDuration;
}
context_->Reset();
composition_ = CompositionText();
result_text_ = absl::nullopt;
is_sync_mode_ = false;
composition_changed_ = false;
}
bool InputMethodAuraLinux::IgnoringNonKeyInput() const {
return !is_sync_mode_ &&
base::TimeTicks::Now() < suppress_non_key_input_until_;
}
bool InputMethodAuraLinux::IsCandidatePopupOpen() const {
return false;
}
VirtualKeyboardController*
InputMethodAuraLinux::GetVirtualKeyboardController() {
if (auto* controller = InputMethodBase::GetVirtualKeyboardController())
return controller;
return context_->GetVirtualKeyboardController();
}
void InputMethodAuraLinux::OnCommit(const std::u16string& text) {
if (IgnoringNonKeyInput() || !GetTextInputClient())
return;
if (is_sync_mode_ || !IsTextInputTypeNone()) {
if (result_text_) {
result_text_->append(text);
} else {
result_text_ = text;
}
}
if (!is_sync_mode_ && !IsTextInputTypeNone()) {
ui::KeyEvent event =
ui::KeyEvent(ui::ET_KEY_PRESSED, ui::VKEY_PROCESSKEY, 0);
if (ime_filtered_key_event_.has_value()) {
event = std::move(*ime_filtered_key_event_);
ime_filtered_key_event_.reset();
ui::EventDispatchDetails details =
DispatchImeFilteredKeyPressEvent(&event);
if (details.target_destroyed || details.dispatcher_destroyed ||
event.stopped_propagation()) {
return;
}
}
last_commit_result_ = MaybeCommitResult(true, event);
composition_ = CompositionText();
}
}
void InputMethodAuraLinux::OnConfirmCompositionText(bool keep_selection) {
ConfirmCompositionText(keep_selection);
}
void InputMethodAuraLinux::OnDeleteSurroundingText(size_t before,
size_t after) {
auto* client = GetTextInputClient();
if (client && composition_.text.empty())
client->ExtendSelectionAndDelete(before, after);
}
void InputMethodAuraLinux::OnPreeditChanged(
const CompositionText& composition_text) {
OnPreeditUpdate(composition_text, !is_sync_mode_);
}
void InputMethodAuraLinux::OnPreeditEnd() {
TextInputClient* client = GetTextInputClient();
OnPreeditUpdate(CompositionText(),
!is_sync_mode_ && client && client->HasCompositionText());
}
void InputMethodAuraLinux::OnSetPreeditRegion(
const gfx::Range& range,
const std::vector<ImeTextSpan>& spans) {
auto* text_input_client = GetTextInputClient();
if (!text_input_client)
return;
#if !BUILDFLAG(IS_OHOS)
text_input_client->SetCompositionFromExistingText(range, spans);
#endif
std::u16string text;
if (text_input_client->GetTextFromRange(range, &text)) {
composition_changed_ |= composition_.text != text;
composition_.text = text;
}
last_commit_result_.reset();
}
void InputMethodAuraLinux::OnClearGrammarFragments(const gfx::Range& range) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
auto* text_input_client = GetTextInputClient();
if (!text_input_client)
return;
text_input_client->ClearGrammarFragments(range);
#endif
}
void InputMethodAuraLinux::OnAddGrammarFragment(
const ui::GrammarFragment& fragment) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
auto* text_input_client = GetTextInputClient();
if (!text_input_client)
return;
text_input_client->AddGrammarFragments({fragment});
#endif
}
void InputMethodAuraLinux::OnSetAutocorrectRange(const gfx::Range& range) {
#if BUILDFLAG(IS_CHROMEOS_LACROS)
auto* text_input_client = GetTextInputClient();
if (!text_input_client)
return;
text_input_client->SetAutocorrectRange(range);
#endif
}
void InputMethodAuraLinux::OnSetVirtualKeyboardOccludedBounds(
const gfx::Rect& screen_bounds) {
SetVirtualKeyboardBounds(screen_bounds);
}
void InputMethodAuraLinux::OnWillChangeFocusedClient(
TextInputClient* focused_before,
TextInputClient* focused) {
ResetContext();
context_->WillUpdateFocus(focused_before, focused);
}
void InputMethodAuraLinux::OnDidChangeFocusedClient(
TextInputClient* focused_before,
TextInputClient* focused) {
UpdateContextFocusState();
if (text_input_type_ != TEXT_INPUT_TYPE_NONE)
OnCaretBoundsChanged(GetTextInputClient());
InputMethodBase::OnDidChangeFocusedClient(focused_before, focused);
}
void InputMethodAuraLinux::OnPreeditUpdate(
const ui::CompositionText& composition_text,
bool force_update_client) {
if (IgnoringNonKeyInput() || IsTextInputTypeNone())
return;
composition_changed_ |= composition_ != composition_text;
composition_ = composition_text;
if (!force_update_client)
return;
if (ime_filtered_key_event_.has_value()) {
ui::KeyEvent event = std::move(*ime_filtered_key_event_);
ime_filtered_key_event_.reset();
ui::EventDispatchDetails details = DispatchImeFilteredKeyPressEvent(&event);
if (details.target_destroyed || details.dispatcher_destroyed ||
event.stopped_propagation()) {
return;
}
}
MaybeUpdateComposition(last_commit_result_ == CommitResult::kSuccess);
last_commit_result_.reset();
}
bool InputMethodAuraLinux::HasInputMethodResult() {
return result_text_ || composition_changed_;
}
bool InputMethodAuraLinux::NeedInsertChar(
const absl::optional<std::u16string>& result_text) const {
return IsTextInputTypeNone() ||
(!composition_changed_ && composition_.text.empty() && result_text &&
result_text->length() == 1);
}
ui::EventDispatchDetails InputMethodAuraLinux::SendFakeProcessKeyEvent(
ui::KeyEvent* event) const {
ui::KeyEvent key_event(ui::ET_KEY_PRESSED, ui::VKEY_PROCESSKEY, event->code(),
event->flags(), ui::DomKey::PROCESS,
event->time_stamp());
ui::EventDispatchDetails details = DispatchKeyEventPostIME(&key_event);
if (key_event.stopped_propagation())
event->StopPropagation();
return details;
}
void InputMethodAuraLinux::ConfirmCompositionText(bool keep_selection) {
auto* client = GetTextInputClient();
if (client)
client->ConfirmCompositionText(keep_selection);
composition_ = CompositionText();
composition_changed_ = false;
result_text_.reset();
}
}