#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 "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,
gfx::AcceleratedWidget widget)
: InputMethodBase(ime_key_event_dispatcher),
widget_(widget),
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() == EventType::kKeyPressed ||
event->type() == EventType::kKeyReleased);
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() == EventType::kKeyPressed &&
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::EventType::kKeyPressed) {
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_ = std::nullopt;
base::AutoReset<bool> flipper(&is_sync_mode_, true);
filtered = context_->DispatchKeyEvent(*event);
}
if (filtered && !HasInputMethodResult() && !IsTextInputTypeNone()) {
ime_filtered_key_event_ = std::move(*event);
return ui::EventDispatchDetails();
}
ui::EventDispatchDetails details;
if (event->type() == ui::EventType::kKeyPressed && 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 |=
UpdateCompositionIfChanged(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::EventType::kKeyPressed) {
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_ = std::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::UpdateCompositionIfTextSelected() {
TextInputClient* client = GetTextInputClient();
if (!client || IsTextInputTypeNone()) {
return false;
}
if (!client->HasCompositionText() && composition_.text.empty() &&
selection_range_.IsValid() && !selection_range_.is_empty()) {
client->SetCompositionText(composition_);
return true;
}
return false;
}
bool InputMethodAuraLinux::UpdateCompositionIfChanged(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;
LinuxInputMethodContext::TextInputClientAttributes attributes;
attributes.input_type = text_input_type_;
if (client) {
reason = client->GetFocusReason();
attributes.input_mode = client->GetTextInputMode();
attributes.flags = client->GetTextInputFlags();
attributes.should_do_learning = client->ShouldDoLearning();
attributes.can_compose_inline = client->CanComposeInline();
} 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, attributes, reason);
}
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, composition_range, selection_range;
std::u16string text;
if (client->GetTextRange(&text_range) &&
client->GetTextFromRange(text_range, &text) &&
client->GetEditableSelectionRange(&selection_range)) {
if (!client->GetCompositionTextRange(&composition_range)) {
composition_range = gfx::Range::InvalidRange();
}
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, composition_range,
selection_range);
}
}
}
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_ = std::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();
}
gfx::AcceleratedWidget InputMethodAuraLinux::GetClientWindowKey() const {
return widget_;
}
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::EventType::kKeyPressed, 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::OnInsertImage(const GURL& src) {
if (auto* text_input_client = GetTextInputClient()) {
text_input_client->InsertImage(src);
}
}
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;
text_input_client->SetCompositionFromExistingText(range, spans);
std::u16string text;
if (text_input_client->GetTextFromRange(range, &text)) {
composition_changed_ |= composition_.text != text;
composition_.text = text;
}
last_commit_result_.reset();
}
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;
}
}
if (!UpdateCompositionIfTextSelected()) {
UpdateCompositionIfChanged(last_commit_result_ == CommitResult::kSuccess);
}
last_commit_result_.reset();
}
bool InputMethodAuraLinux::HasInputMethodResult() {
return result_text_ || composition_changed_;
}
bool InputMethodAuraLinux::NeedInsertChar(
const std::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::EventType::kKeyPressed, 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();
}
}