| // 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 "components/exo/text_input.h" |
| |
| #include <algorithm> |
| |
| #include "ash/keyboard/ui/keyboard_ui_controller.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "components/exo/surface.h" |
| #include "components/exo/wm_helper.h" |
| #include "third_party/icu/source/common/unicode/uchar.h" |
| #include "ui/aura/window.h" |
| #include "ui/aura/window_tree_host.h" |
| #include "ui/base/ime/input_method.h" |
| #include "ui/events/event.h" |
| |
| namespace exo { |
| |
| namespace { |
| |
| ui::InputMethod* GetInputMethod(aura::Window* window) { |
| if (!window || !window->GetHost()) |
| return nullptr; |
| return window->GetHost()->GetInputMethod(); |
| } |
| |
| } // namespace |
| |
| size_t OffsetFromUTF8Offset(const base::StringPiece& text, uint32_t offset) { |
| return base::UTF8ToUTF16(text.substr(0, offset)).size(); |
| } |
| |
| size_t OffsetFromUTF16Offset(const base::StringPiece16& text, uint32_t offset) { |
| return base::UTF16ToUTF8(text.substr(0, offset)).size(); |
| } |
| |
| TextInput::TextInput(std::unique_ptr<Delegate> delegate) |
| : delegate_(std::move(delegate)) {} |
| |
| TextInput::~TextInput() { |
| if (keyboard_ui_controller_) |
| keyboard_ui_controller_->RemoveObserver(this); |
| if (input_method_) |
| Deactivate(); |
| } |
| |
| void TextInput::Activate(Surface* surface) { |
| DLOG_IF(ERROR, window_) << "Already activated with " << window_; |
| DCHECK(surface); |
| |
| window_ = surface->window(); |
| AttachInputMethod(); |
| } |
| |
| void TextInput::Deactivate() { |
| DetachInputMethod(); |
| window_ = nullptr; |
| } |
| |
| void TextInput::ShowVirtualKeyboardIfEnabled() { |
| // Some clients may ask showing virtual keyboard before sending activation. |
| if (!input_method_) { |
| pending_vk_visible_ = true; |
| return; |
| } |
| input_method_->ShowVirtualKeyboardIfEnabled(); |
| } |
| |
| void TextInput::HideVirtualKeyboard() { |
| if (keyboard_ui_controller_) |
| keyboard_ui_controller_->HideKeyboardByUser(); |
| pending_vk_visible_ = false; |
| } |
| |
| void TextInput::Resync() { |
| if (input_method_) |
| input_method_->OnCaretBoundsChanged(this); |
| } |
| |
| void TextInput::SetSurroundingText(const base::string16& text, |
| uint32_t cursor_pos, |
| uint32_t anchor) { |
| surrounding_text_ = text; |
| cursor_pos_ = gfx::Range(cursor_pos); |
| if (anchor < cursor_pos) |
| cursor_pos_->set_start(anchor); |
| else |
| cursor_pos_->set_end(anchor); |
| } |
| |
| void TextInput::SetTypeModeFlags(ui::TextInputType type, |
| ui::TextInputMode mode, |
| int flags, |
| bool should_do_learning) { |
| if (!input_method_) |
| return; |
| bool changed = (input_type_ != type); |
| input_type_ = type; |
| input_mode_ = mode; |
| flags_ = flags; |
| should_do_learning_ = should_do_learning; |
| if (changed) |
| input_method_->OnTextInputTypeChanged(this); |
| } |
| |
| void TextInput::SetCaretBounds(const gfx::Rect& bounds) { |
| if (caret_bounds_ == bounds) |
| return; |
| caret_bounds_ = bounds; |
| if (!input_method_) |
| return; |
| input_method_->OnCaretBoundsChanged(this); |
| } |
| |
| void TextInput::SetCompositionText(const ui::CompositionText& composition) { |
| composition_ = composition; |
| delegate_->SetCompositionText(composition); |
| } |
| |
| uint32_t TextInput::ConfirmCompositionText(bool keep_selection) { |
| // TODO(b/134473433) Modify this function so that when keep_selection is |
| // true, the selection is not changed when text committed |
| if (keep_selection) { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| const uint32_t composition_text_length = |
| static_cast<uint32_t>(composition_.text.length()); |
| delegate_->Commit(composition_.text); |
| return composition_text_length; |
| } |
| |
| void TextInput::ClearCompositionText() { |
| composition_ = ui::CompositionText(); |
| delegate_->SetCompositionText(composition_); |
| } |
| |
| void TextInput::InsertText(const base::string16& text) { |
| delegate_->Commit(text); |
| } |
| |
| void TextInput::InsertChar(const ui::KeyEvent& event) { |
| base::char16 ch = event.GetCharacter(); |
| if (u_isprint(ch)) { |
| InsertText(base::string16(1, ch)); |
| return; |
| } |
| delegate_->SendKey(event); |
| } |
| |
| ui::TextInputType TextInput::GetTextInputType() const { |
| return input_type_; |
| } |
| |
| ui::TextInputMode TextInput::GetTextInputMode() const { |
| return input_mode_; |
| } |
| |
| base::i18n::TextDirection TextInput::GetTextDirection() const { |
| return direction_; |
| } |
| |
| int TextInput::GetTextInputFlags() const { |
| return flags_; |
| } |
| |
| bool TextInput::CanComposeInline() const { |
| return true; |
| } |
| |
| gfx::Rect TextInput::GetCaretBounds() const { |
| return caret_bounds_ + window_->GetBoundsInScreen().OffsetFromOrigin(); |
| } |
| |
| bool TextInput::GetCompositionCharacterBounds(uint32_t index, |
| gfx::Rect* rect) const { |
| return false; |
| } |
| |
| bool TextInput::HasCompositionText() const { |
| return !composition_.text.empty(); |
| } |
| |
| ui::TextInputClient::FocusReason TextInput::GetFocusReason() const { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| return ui::TextInputClient::FOCUS_REASON_OTHER; |
| } |
| |
| bool TextInput::GetTextRange(gfx::Range* range) const { |
| if (!cursor_pos_) |
| return false; |
| range->set_start(0); |
| if (composition_.text.empty()) { |
| range->set_end(surrounding_text_.size()); |
| } else { |
| range->set_end(surrounding_text_.size() - cursor_pos_->length() + |
| composition_.text.size()); |
| } |
| return true; |
| } |
| |
| bool TextInput::GetCompositionTextRange(gfx::Range* range) const { |
| if (!cursor_pos_ || composition_.text.empty()) |
| return false; |
| |
| range->set_start(cursor_pos_->start()); |
| range->set_end(cursor_pos_->start() + composition_.text.size()); |
| return true; |
| } |
| |
| bool TextInput::GetEditableSelectionRange(gfx::Range* range) const { |
| if (!cursor_pos_) |
| return false; |
| range->set_start(cursor_pos_->start()); |
| range->set_end(cursor_pos_->end()); |
| return true; |
| } |
| |
| bool TextInput::SetEditableSelectionRange(const gfx::Range& range) { |
| if (surrounding_text_.size() < range.GetMax()) |
| return false; |
| delegate_->SetCursor( |
| gfx::Range(OffsetFromUTF16Offset(surrounding_text_, range.start()), |
| OffsetFromUTF16Offset(surrounding_text_, range.end()))); |
| return true; |
| } |
| |
| bool TextInput::DeleteRange(const gfx::Range& range) { |
| if (surrounding_text_.size() < range.GetMax()) |
| return false; |
| delegate_->DeleteSurroundingText( |
| gfx::Range(OffsetFromUTF16Offset(surrounding_text_, range.start()), |
| OffsetFromUTF16Offset(surrounding_text_, range.end()))); |
| return true; |
| } |
| |
| bool TextInput::GetTextFromRange(const gfx::Range& range, |
| base::string16* text) const { |
| gfx::Range text_range; |
| if (!GetTextRange(&text_range) || !text_range.Contains(range)) |
| return false; |
| if (composition_.text.empty() || range.GetMax() <= cursor_pos_->GetMin()) { |
| text->assign(surrounding_text_, range.GetMin(), range.length()); |
| return true; |
| } |
| size_t composition_end = cursor_pos_->GetMin() + composition_.text.size(); |
| if (range.GetMin() >= composition_end) { |
| size_t start = |
| range.GetMin() - composition_.text.size() + cursor_pos_->length(); |
| text->assign(surrounding_text_, start, range.length()); |
| return true; |
| } |
| |
| size_t start_in_composition = 0; |
| if (range.GetMin() <= cursor_pos_->GetMin()) { |
| text->assign(surrounding_text_, range.GetMin(), |
| cursor_pos_->GetMin() - range.GetMin()); |
| } else { |
| start_in_composition = range.GetMin() - cursor_pos_->GetMin(); |
| } |
| if (range.GetMax() <= composition_end) { |
| text->append(composition_.text, start_in_composition, |
| range.GetMax() - cursor_pos_->GetMin() - start_in_composition); |
| } else { |
| text->append(composition_.text, start_in_composition, |
| composition_.text.size() - start_in_composition); |
| text->append(surrounding_text_, cursor_pos_->GetMax(), |
| range.GetMax() - composition_end); |
| } |
| return true; |
| } |
| |
| void TextInput::OnInputMethodChanged() { |
| ui::InputMethod* input_method = GetInputMethod(window_); |
| if (input_method == input_method_) |
| return; |
| input_method_->DetachTextInputClient(this); |
| input_method_ = input_method; |
| input_method_->SetFocusedTextInputClient(this); |
| } |
| |
| bool TextInput::ChangeTextDirectionAndLayoutAlignment( |
| base::i18n::TextDirection direction) { |
| if (direction == direction_) |
| return true; |
| direction_ = direction; |
| delegate_->OnTextDirectionChanged(direction_); |
| return true; |
| } |
| |
| void TextInput::ExtendSelectionAndDelete(size_t before, size_t after) { |
| if (!cursor_pos_) |
| return; |
| uint32_t start = |
| (cursor_pos_->GetMin() < before) ? 0 : (cursor_pos_->GetMin() - before); |
| uint32_t end = |
| std::min(cursor_pos_->GetMax() + after, surrounding_text_.size()); |
| delegate_->DeleteSurroundingText( |
| gfx::Range(OffsetFromUTF16Offset(surrounding_text_, start), |
| OffsetFromUTF16Offset(surrounding_text_, end))); |
| } |
| |
| void TextInput::EnsureCaretNotInRect(const gfx::Rect& rect) {} |
| |
| bool TextInput::IsTextEditCommandEnabled(ui::TextEditCommand command) const { |
| return false; |
| } |
| |
| void TextInput::SetTextEditCommandForNextKeyEvent(ui::TextEditCommand command) { |
| } |
| |
| ukm::SourceId TextInput::GetClientSourceForMetrics() const { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| return ukm::kInvalidSourceId; |
| } |
| |
| bool TextInput::ShouldDoLearning() { |
| return should_do_learning_; |
| } |
| |
| bool TextInput::SetCompositionFromExistingText( |
| const gfx::Range& range, |
| const std::vector<ui::ImeTextSpan>& ui_ime_text_spans) { |
| // TODO(https://crbug.com/952757): Implement this method. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| return false; |
| } |
| |
| gfx::Range TextInput::GetAutocorrectRange() const { |
| // TODO(https://crbug.com/952757): Implement this method. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| return gfx::Range(); |
| } |
| |
| gfx::Rect TextInput::GetAutocorrectCharacterBounds() const { |
| // TODO(https://crbug.com/952757): Implement this method. |
| NOTIMPLEMENTED_LOG_ONCE(); |
| return gfx::Rect(); |
| } |
| |
| // TODO(crbug.com/1091088) Implement setAutocorrectRange |
| bool TextInput::SetAutocorrectRange(const base::string16& autocorrect_text, |
| const gfx::Range& range) { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| return false; |
| } |
| |
| // TODO(crbug.com/1091088) Implement ClearAutocorrectRange |
| void TextInput::ClearAutocorrectRange() { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| void TextInput::OnKeyboardVisibilityChanged(bool is_visible) { |
| delegate_->OnVirtualKeyboardVisibilityChanged(is_visible); |
| } |
| |
| void TextInput::AttachInputMethod() { |
| DCHECK(!input_method_); |
| |
| ui::InputMethod* input_method = GetInputMethod(window_); |
| if (!input_method) { |
| LOG(ERROR) << "input method not found"; |
| return; |
| } |
| |
| input_mode_ = ui::TEXT_INPUT_MODE_TEXT; |
| input_type_ = ui::TEXT_INPUT_TYPE_TEXT; |
| input_method_ = input_method; |
| input_method_->SetFocusedTextInputClient(this); |
| delegate_->Activated(); |
| |
| if (!keyboard_ui_controller_ && |
| keyboard::KeyboardUIController::HasInstance()) { |
| auto* keyboard_ui_controller = keyboard::KeyboardUIController::Get(); |
| if (keyboard_ui_controller->IsEnabled()) { |
| keyboard_ui_controller_ = keyboard_ui_controller; |
| keyboard_ui_controller_->AddObserver(this); |
| } |
| } |
| |
| if (pending_vk_visible_) { |
| input_method_->ShowVirtualKeyboardIfEnabled(); |
| pending_vk_visible_ = false; |
| } |
| } |
| |
| void TextInput::DetachInputMethod() { |
| if (!input_method_) { |
| DLOG(ERROR) << "input method already detached"; |
| return; |
| } |
| input_mode_ = ui::TEXT_INPUT_MODE_DEFAULT; |
| input_type_ = ui::TEXT_INPUT_TYPE_NONE; |
| input_method_->DetachTextInputClient(this); |
| input_method_ = nullptr; |
| delegate_->Deactivated(); |
| } |
| |
| } // namespace exo |