| // Copyright 2018 The Chromium Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "ui/ozone/platform/wayland/host/wayland_input_method_context.h" |
| |
| #include "base/command_line.h" |
| #include "base/containers/contains.h" |
| #include "base/feature_list.h" |
| #include "base/functional/bind.h" |
| #include "base/i18n/char_iterator.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/memory/weak_ptr.h" |
| #include "base/notreached.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_offset_string_conversions.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "build/chromeos_buildflags.h" |
| #include "third_party/abseil-cpp/absl/types/optional.h" |
| #include "ui/base/ime/composition_text.h" |
| #include "ui/base/ime/ime_text_span.h" |
| #include "ui/base/ime/text_input_client.h" |
| #include "ui/base/ime/text_input_flags.h" |
| #include "ui/base/ime/text_input_type.h" |
| #include "ui/base/ui_base_features.h" |
| #include "ui/events/base_event_utils.h" |
| #include "ui/events/event.h" |
| #include "ui/events/keycodes/dom/dom_code.h" |
| #include "ui/events/ozone/events_ozone.h" |
| #include "ui/events/types/event_type.h" |
| #include "ui/gfx/range/range.h" |
| #include "ui/ozone/platform/wayland/host/wayland_connection.h" |
| #include "ui/ozone/platform/wayland/host/wayland_seat.h" |
| #include "ui/ozone/platform/wayland/host/wayland_window.h" |
| #include "ui/ozone/platform/wayland/host/zwp_text_input_wrapper_v1.h" |
| #include "ui/ozone/public/ozone_switches.h" |
| |
| #if BUILDFLAG(USE_XKBCOMMON) |
| #include "ui/events/ozone/layout/keyboard_layout_engine_manager.h" |
| #include "ui/events/ozone/layout/xkb/xkb_keyboard_layout_engine.h" |
| #endif |
| |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| #include "base/check.h" |
| #include "chromeos/crosapi/mojom/crosapi.mojom.h" |
| #include "chromeos/startup/browser_params_proxy.h" |
| #endif |
| |
| namespace ui { |
| namespace { |
| |
| absl::optional<size_t> OffsetFromUTF8Offset(const base::StringPiece& text, |
| uint32_t offset) { |
| if (offset > text.length()) |
| return absl::nullopt; |
| |
| std::u16string converted; |
| if (!base::UTF8ToUTF16(text.data(), offset, &converted)) |
| return absl::nullopt; |
| |
| return converted.size(); |
| } |
| |
| bool IsImeEnabled() { |
| base::CommandLine* cmd_line = base::CommandLine::ForCurrentProcess(); |
| // We do not expect both switches are set at the same time. |
| DCHECK(!cmd_line->HasSwitch(switches::kEnableWaylandIme) || |
| !cmd_line->HasSwitch(switches::kDisableWaylandIme)); |
| // Force enable/disable wayland IMEs, when explictly specified via commandline |
| // arguments. |
| if (cmd_line->HasSwitch(switches::kEnableWaylandIme)) |
| return true; |
| if (cmd_line->HasSwitch(switches::kDisableWaylandIme)) |
| return false; |
| |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| // On Lacros chrome, we check whether ash-chrome supports IME, then |
| // enable IME if so. This allows us to control IME enabling state in |
| // Lacros-chrome side, which helps us on releasing. |
| // TODO(crbug.com/1159237): In the future, we may want to unify the behavior |
| // of ozone/wayland across platforms. |
| const chromeos::BrowserParamsProxy* init_params = |
| chromeos::BrowserParamsProxy::Get(); |
| if (init_params->ExoImeSupport() != |
| crosapi::mojom::ExoImeSupport::kUnsupported) { |
| return true; |
| } |
| #endif |
| |
| // Do not enable wayland IME by default. |
| return false; |
| } |
| |
| // Returns ImeTextSpan style to be assigned. Maybe nullopt if it is not |
| // supported. |
| absl::optional<std::pair<ImeTextSpan::Type, ImeTextSpan::Thickness>> |
| ConvertStyle(uint32_t style) { |
| switch (style) { |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_DEFAULT: |
| return absl::make_optional(std::make_pair(ImeTextSpan::Type::kComposition, |
| ImeTextSpan::Thickness::kNone)); |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_HIGHLIGHT: |
| return absl::make_optional(std::make_pair( |
| ImeTextSpan::Type::kComposition, ImeTextSpan::Thickness::kThick)); |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_UNDERLINE: |
| return absl::make_optional(std::make_pair(ImeTextSpan::Type::kComposition, |
| ImeTextSpan::Thickness::kThin)); |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_SELECTION: |
| return absl::make_optional(std::make_pair(ImeTextSpan::Type::kSuggestion, |
| ImeTextSpan::Thickness::kNone)); |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_INCORRECT: |
| return absl::make_optional( |
| std::make_pair(ImeTextSpan::Type::kMisspellingSuggestion, |
| ImeTextSpan::Thickness::kNone)); |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_NONE: |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_ACTIVE: |
| case ZWP_TEXT_INPUT_V1_PREEDIT_STYLE_INACTIVE: |
| default: |
| VLOG(1) << "Unsupported style. Skipped: " << style; |
| } |
| return absl::nullopt; |
| } |
| |
| struct OffsetText { |
| std::string text; |
| size_t offset; |
| }; |
| |
| // Trims surrounding text for standard text_input. There is the limit of length |
| // of the surrounding text, which is 4000 bytes. This gives it a try to keep |
| // the surrounding text around the selection with respecting UTF-8 boundary. |
| // Returns the trimmed string and UTF-8 offset. |
| absl::optional<OffsetText> TrimSurroundingTextForStandard( |
| base::StringPiece text_utf8, |
| gfx::Range selection_utf8) { |
| // The text length for set_surrounding_text can not be longer than the maximum |
| // length of wayland messages. The maximum length of the text is explicitly |
| // specified as 4000 in the protocol spec of text-input-unstable-v3. |
| static constexpr size_t kWaylandMessageDataMaxLength = 4000; |
| |
| // If the selection range in UTF8 form is longer than the maximum length of |
| // wayland messages, skip sending set_surrounding_text requests. |
| if (selection_utf8.length() > kWaylandMessageDataMaxLength) { |
| return absl::nullopt; |
| } |
| |
| if (text_utf8.size() <= kWaylandMessageDataMaxLength) { |
| // We separate this case to run the function simpler and faster since this |
| // condition is satisfied in most cases. |
| return OffsetText{std::string(text_utf8), 0u}; |
| } |
| |
| // If the text in UTF8 form is longer than the maximum length of wayland |
| // messages while the selection range in UTF8 form is not, truncate the text |
| // into the limitation and adjust indices of |selection_range|. |
| |
| // Decide where to start. The truncated text should be around the selection |
| // range. We choose a text whose center point is same to the center of the |
| // selection range unless this chosen text is shorter than the maximum |
| // length of wayland messages because of the original text position. |
| uint32_t selection_range_utf8_center = |
| selection_utf8.start() + selection_utf8.length() / 2; |
| // The substring starting with |start_index| might be invalid as UTF8. |
| size_t start_index; |
| if (selection_range_utf8_center <= kWaylandMessageDataMaxLength / 2) { |
| // The selection range is near enough to the start point of original text. |
| start_index = 0; |
| } else if (text_utf8.size() - selection_range_utf8_center < |
| kWaylandMessageDataMaxLength / 2) { |
| // The selection range is near enough to the end point of original text. |
| start_index = text_utf8.size() - kWaylandMessageDataMaxLength; |
| } else { |
| // Choose a text whose center point is same to the center of the selection |
| // range. |
| start_index = |
| selection_range_utf8_center - kWaylandMessageDataMaxLength / 2; |
| } |
| |
| // Truncate the text to fit into the wayland message size and adjust indices |
| // of |selection_range|. Since the text is in UTF8 form, we need to adjust |
| // the text and selection range positions where all characters are valid. |
| // |
| // TODO(crbug.com/1214957): We should use base::i18n::BreakIterator |
| // to get the offsets and convert it into UTF8 form instead of using |
| // UTF8CharIterator. |
| base::i18n::UTF8CharIterator iter(text_utf8); |
| while (iter.array_pos() < start_index) { |
| iter.Advance(); |
| } |
| size_t truncated_text_start = iter.array_pos(); |
| size_t truncated_text_end; |
| while (iter.array_pos() <= start_index + kWaylandMessageDataMaxLength) { |
| truncated_text_end = iter.array_pos(); |
| if (!iter.Advance()) { |
| break; |
| } |
| } |
| |
| return OffsetText{ |
| std::string(text_utf8.substr(truncated_text_start, |
| truncated_text_end - truncated_text_start)), |
| truncated_text_start}; |
| } |
| |
| // TODO(crbug.com/1402906): Add TrimSurroundingTextForExtension. |
| |
| } // namespace |
| |
| WaylandInputMethodContext::WaylandInputMethodContext( |
| WaylandConnection* connection, |
| WaylandKeyboard::Delegate* key_delegate, |
| LinuxInputMethodContextDelegate* ime_delegate) |
| : connection_(connection), |
| key_delegate_(key_delegate), |
| ime_delegate_(ime_delegate), |
| text_input_(nullptr) { |
| connection_->window_manager()->AddObserver(this); |
| Init(); |
| } |
| |
| WaylandInputMethodContext::~WaylandInputMethodContext() { |
| if (text_input_) { |
| DismissVirtualKeyboard(); |
| text_input_->Deactivate(); |
| } |
| connection_->window_manager()->RemoveObserver(this); |
| } |
| |
| void WaylandInputMethodContext::Init(bool initialize_for_testing) { |
| bool use_ozone_wayland_vkb = initialize_for_testing || IsImeEnabled(); |
| |
| // If text input instance is not created then all ime context operations |
| // are noop. This option is because in some environments someone might not |
| // want to enable ime/virtual keyboard even if it's available. |
| if (use_ozone_wayland_vkb && !text_input_ && |
| connection_->text_input_manager_v1()) { |
| text_input_ = std::make_unique<ZWPTextInputWrapperV1>( |
| connection_, this, connection_->text_input_manager_v1(), |
| connection_->text_input_extension_v1()); |
| } |
| } |
| |
| bool WaylandInputMethodContext::DispatchKeyEvent(const KeyEvent& key_event) { |
| if (key_event.type() != ET_KEY_PRESSED) |
| return false; |
| |
| // Consume all peek key event. |
| if (IsPeekKeyEvent(key_event)) |
| return true; |
| |
| // This is the fallback key event which was not consumed by IME. |
| // So, process it inside Chrome. |
| if (!character_composer_.FilterKeyPress(key_event)) |
| return false; |
| |
| // CharacterComposer consumed the key event. Update the composition text. |
| UpdatePreeditText(character_composer_.preedit_string()); |
| auto composed = character_composer_.composed_character(); |
| if (!composed.empty()) |
| ime_delegate_->OnCommit(composed); |
| return true; |
| } |
| |
| bool WaylandInputMethodContext::IsPeekKeyEvent(const KeyEvent& key_event) { |
| return !(GetKeyboardImeFlags(key_event) & kPropertyKeyboardImeIgnoredFlag); |
| } |
| |
| void WaylandInputMethodContext::UpdatePreeditText( |
| const std::u16string& preedit_text) { |
| CompositionText preedit; |
| preedit.text = preedit_text; |
| auto length = preedit.text.size(); |
| |
| preedit.selection = gfx::Range(length); |
| preedit.ime_text_spans.push_back(ImeTextSpan( |
| ImeTextSpan::Type::kComposition, 0, length, ImeTextSpan::Thickness::kThin, |
| ImeTextSpan::UnderlineStyle::kSolid, SK_ColorTRANSPARENT)); |
| surrounding_text_tracker_.OnSetCompositionText(preedit); |
| ime_delegate_->OnPreeditChanged(preedit); |
| } |
| |
| void WaylandInputMethodContext::Reset() { |
| character_composer_.Reset(); |
| if (base::FeatureList::IsEnabled(features::kWaylandCancelComposition)) { |
| // TODO(b/269964109): In ChromeOS, 'reset' means to reset the composition |
| // only, excluding surrounding text etc. In Wayland, text-input-v1 doesn't |
| // define what state is reset in a 'reset' call. However, based on the |
| // description in text-input-v3, the state likely includes the surrounding |
| // text. Therefore, the call below is likely not compliant with Wayland's |
| // intentions. Introduce a dedicated extended Wayland API for resetting only |
| // the composition. |
| surrounding_text_tracker_.CancelComposition(); |
| } else { |
| surrounding_text_tracker_.Reset(); |
| } |
| if (text_input_) |
| text_input_->Reset(); |
| } |
| |
| void WaylandInputMethodContext::WillUpdateFocus(TextInputClient* old_client, |
| TextInputClient* new_client) { |
| if (old_client) |
| past_clients_.try_emplace(old_client, base::AsWeakPtr(old_client)); |
| } |
| |
| void WaylandInputMethodContext::UpdateFocus( |
| bool has_client, |
| TextInputType old_type, |
| TextInputType new_type, |
| TextInputClient::FocusReason reason) { |
| // This prevents unnecessarily hiding/showing the virtual keyboard. |
| bool skip_vk_update = |
| old_type != TEXT_INPUT_TYPE_NONE && new_type != TEXT_INPUT_TYPE_NONE; |
| |
| if (old_type != TEXT_INPUT_TYPE_NONE) |
| Blur(skip_vk_update); |
| if (new_type != TEXT_INPUT_TYPE_NONE) |
| Focus(skip_vk_update, reason); |
| } |
| |
| void WaylandInputMethodContext::Focus(bool skip_virtual_keyboard_update, |
| TextInputClient::FocusReason reason) { |
| focused_ = true; |
| MaybeUpdateActivated(skip_virtual_keyboard_update, reason); |
| } |
| |
| void WaylandInputMethodContext::Blur(bool skip_virtual_keyboard_update) { |
| focused_ = false; |
| MaybeUpdateActivated(skip_virtual_keyboard_update, |
| TextInputClient::FOCUS_REASON_NONE); |
| } |
| |
| void WaylandInputMethodContext::SetCursorLocation(const gfx::Rect& rect) { |
| if (!text_input_) { |
| return; |
| } |
| WaylandWindow* focused_window = |
| connection_->window_manager()->GetCurrentKeyboardFocusedWindow(); |
| if (!focused_window) { |
| return; |
| } |
| text_input_->SetCursorRect( |
| rect - focused_window->GetBoundsInDIP().OffsetFromOrigin()); |
| } |
| |
| void WaylandInputMethodContext::SetSurroundingText( |
| const std::u16string& text, |
| const gfx::Range& text_range, |
| const gfx::Range& selection_range, |
| const absl::optional<GrammarFragment>& fragment, |
| const absl::optional<AutocorrectInfo>& autocorrect) { |
| // TODO(crbug.com/1402906): Text range is not currently handled correctly. |
| surrounding_text_tracker_.Update(text, 0u, selection_range); |
| |
| if (!text_input_) |
| return; |
| |
| // Convert |text| and |selection_range| into UTF8 form. |
| std::vector<size_t> offsets_for_adjustment = {selection_range.start(), |
| selection_range.end()}; |
| if (fragment.has_value()) { |
| offsets_for_adjustment.push_back(fragment->range.start()); |
| offsets_for_adjustment.push_back(fragment->range.end()); |
| } |
| |
| std::string text_utf8 = |
| base::UTF16ToUTF8AndAdjustOffsets(text, &offsets_for_adjustment); |
| if (offsets_for_adjustment[0] == std::u16string::npos || |
| offsets_for_adjustment[1] == std::u16string::npos) { |
| LOG(DFATAL) << "The selection range is invalid."; |
| return; |
| } |
| gfx::Range selection_range_utf8 = { |
| static_cast<uint32_t>(offsets_for_adjustment[0]), |
| static_cast<uint32_t>(offsets_for_adjustment[1])}; |
| |
| auto trimmed = |
| TrimSurroundingTextForStandard(text_utf8, selection_range_utf8); |
| if (!trimmed.has_value()) { |
| surrounding_text_tracker_.Reset(); |
| return; |
| } |
| text_utf8 = std::move(trimmed->text); |
| surrounding_text_offset_ = trimmed->offset; |
| |
| if (fragment.has_value()) { |
| // SetGrammarFragmentAtCursor must happen before SetSurroundingText to make |
| // sure it is properly updated before IME needs it. |
| DCHECK_EQ(offsets_for_adjustment.size(), 4u); |
| text_input_->SetGrammarFragmentAtCursor(GrammarFragment( |
| gfx::Range(static_cast<uint32_t>(offsets_for_adjustment[2] - |
| surrounding_text_offset_), |
| static_cast<uint32_t>(offsets_for_adjustment[3] - |
| surrounding_text_offset_)), |
| fragment->suggestion)); |
| } else { |
| // Invalidate the grammar fragment. |
| text_input_->SetGrammarFragmentAtCursor(GrammarFragment(gfx::Range(), "")); |
| } |
| |
| if (autocorrect.has_value()) { |
| // Send the updated autocorrect information before surrounding text, |
| // as surrounding text changes may trigger the IME to ask for the |
| // autocorrect information. |
| text_input_->SetAutocorrectInfo(autocorrect->range, autocorrect->bounds); |
| } |
| |
| gfx::Range relocated_selection_range( |
| selection_range_utf8.start() - surrounding_text_offset_, |
| selection_range_utf8.end() - surrounding_text_offset_); |
| text_input_->SetSurroundingText(text_utf8, relocated_selection_range); |
| } |
| |
| void WaylandInputMethodContext::SetContentType(TextInputType type, |
| TextInputMode mode, |
| uint32_t flags, |
| bool should_do_learning, |
| bool can_compose_inline) { |
| if (!text_input_) |
| return; |
| text_input_->SetContentType(type, mode, flags, should_do_learning, |
| can_compose_inline); |
| } |
| |
| VirtualKeyboardController* |
| WaylandInputMethodContext::GetVirtualKeyboardController() { |
| if (!text_input_) |
| return nullptr; |
| return this; |
| } |
| |
| bool WaylandInputMethodContext::DisplayVirtualKeyboard() { |
| if (!text_input_) |
| return false; |
| |
| text_input_->ShowInputPanel(); |
| return true; |
| } |
| |
| void WaylandInputMethodContext::DismissVirtualKeyboard() { |
| if (!text_input_) |
| return; |
| |
| text_input_->HideInputPanel(); |
| } |
| |
| void WaylandInputMethodContext::AddObserver( |
| VirtualKeyboardControllerObserver* observer) { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| void WaylandInputMethodContext::RemoveObserver( |
| VirtualKeyboardControllerObserver* observer) { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| |
| bool WaylandInputMethodContext::IsKeyboardVisible() { |
| return virtual_keyboard_visible_; |
| } |
| |
| void WaylandInputMethodContext::OnPreeditString( |
| base::StringPiece text, |
| const std::vector<SpanStyle>& spans, |
| int32_t preedit_cursor) { |
| CompositionText composition_text; |
| composition_text.text = base::UTF8ToUTF16(text); |
| for (const auto& span : spans) { |
| auto start_offset = OffsetFromUTF8Offset(text, span.index); |
| if (!start_offset) |
| continue; |
| auto end_offset = OffsetFromUTF8Offset(text, span.index + span.length); |
| if (!end_offset) |
| continue; |
| auto style = ConvertStyle(span.style); |
| if (!style.has_value()) |
| continue; |
| composition_text.ime_text_spans.emplace_back( |
| /* type= */ style->first, *start_offset, *end_offset, |
| /* thickness = */ style->second); |
| } |
| |
| if (preedit_cursor < 0) { |
| composition_text.selection = gfx::Range::InvalidRange(); |
| } else { |
| auto cursor = |
| OffsetFromUTF8Offset(text, static_cast<uint32_t>(preedit_cursor)); |
| if (!cursor) { |
| // Invalid cursor position. Do nothing. |
| return; |
| } |
| composition_text.selection = gfx::Range(*cursor); |
| } |
| |
| surrounding_text_tracker_.OnSetCompositionText(composition_text); |
| ime_delegate_->OnPreeditChanged(composition_text); |
| } |
| |
| void WaylandInputMethodContext::OnCommitString(base::StringPiece text) { |
| if (pending_keep_selection_) { |
| surrounding_text_tracker_.OnConfirmCompositionText(true); |
| ime_delegate_->OnConfirmCompositionText(true); |
| pending_keep_selection_ = false; |
| return; |
| } |
| std::u16string text_utf16 = base::UTF8ToUTF16(text); |
| surrounding_text_tracker_.OnInsertText( |
| text_utf16, |
| TextInputClient::InsertTextCursorBehavior::kMoveCursorAfterText); |
| ime_delegate_->OnCommit(text_utf16); |
| } |
| |
| void WaylandInputMethodContext::OnCursorPosition(int32_t index, |
| int32_t anchor) { |
| const auto& [surrounding_text, utf16_offset, selection, |
| unused_composition_text] = |
| surrounding_text_tracker_.predicted_state(); |
| |
| if (surrounding_text.empty()) { |
| LOG(ERROR) << "SetSurroundingText should run before OnCursorPosition."; |
| return; |
| } |
| |
| // Adjust index and anchor to the position in `surrounding_text_`. |
| // `index` and `anchor` sent from Exo is for the surrounding text sent to Exo |
| // which could be trimmed when the actual surrounding text is longer than 4000 |
| // bytes. |
| // Note that `index` and `anchor` is guaranteed to be under 4000 bytes, |
| // adjusted index and anchor below won't overflow. |
| std::vector<size_t> offsets = {index + surrounding_text_offset_, |
| anchor + surrounding_text_offset_}; |
| base::UTF8ToUTF16AndAdjustOffsets(base::UTF16ToUTF8(surrounding_text), |
| &offsets); |
| if (offsets[0] == std::u16string::npos || |
| offsets[0] > surrounding_text.size()) { |
| LOG(ERROR) << "Invalid index is specified."; |
| return; |
| } |
| if (offsets[1] == std::u16string::npos || |
| offsets[1] > surrounding_text.size()) { |
| LOG(ERROR) << "Invalid anchor is specified."; |
| return; |
| } |
| |
| const gfx::Range new_selection_range = |
| base::FeatureList::IsEnabled(features::kWaylandKeepSelectionFix) |
| ? gfx::Range(offsets[1], offsets[0]) |
| : gfx::Range(offsets[0], offsets[1]); |
| #if BUILDFLAG(IS_CHROMEOS_LACROS) |
| // Cursor position may be wrong on Lacros due to timing issue for some |
| // scenario when surrounding text is longer than wayland message size |
| // limitation (4000 bytes) such as: |
| // 1. Set surrounding text with 8000 bytes and send the selection adjusted to |
| // 4000 bytes (wayland message size maximum). |
| // 2. Exo requests to delete surrounding text sent from 1. |
| // 3. Before receiving OnDeleteSurrounding, move the selection to 4000 bytes |
| // (this implies that surrounding text sent to Exo is changed) on wayland and |
| // set surrounding text. |
| // In this case, Exo can only know the relative position to the offset trimmed |
| // on Wayland, so the position is mismatched to Wayland. |
| // |
| // This timing issue will be fixed by sending whole surrounding text instead |
| // of trimmed text. |
| if (selection == new_selection_range) { |
| pending_keep_selection_ = true; |
| } else { |
| NOTIMPLEMENTED_LOG_ONCE(); |
| } |
| #endif |
| |
| surrounding_text_tracker_.OnSetEditableSelectionRange(new_selection_range); |
| } |
| |
| void WaylandInputMethodContext::OnDeleteSurroundingText(int32_t index, |
| uint32_t length) { |
| const auto& [surrounding_text, utf16_offset, selection, unsused_composition] = |
| surrounding_text_tracker_.predicted_state(); |
| DCHECK(selection.IsValid()); |
| |
| // TODO(crbug.com/1227590): Currently data sent from delete surrounding text |
| // from exo is broken. Currently this broken behavior is supported to prevent |
| // visible regressions, but should be fixed in the future, specifically the |
| // compatibility with non-exo wayland compositors. |
| std::vector<size_t> offsets_for_adjustment = { |
| surrounding_text_offset_ + index, |
| surrounding_text_offset_ + index + length, |
| }; |
| base::UTF8ToUTF16AndAdjustOffsets(base::UTF16ToUTF8(surrounding_text), |
| &offsets_for_adjustment); |
| if (base::Contains(offsets_for_adjustment, std::u16string::npos)) { |
| LOG(DFATAL) << "The selection range for surrounding text is invalid."; |
| return; |
| } |
| |
| if (selection.GetMin() < offsets_for_adjustment[0] || |
| selection.GetMax() > offsets_for_adjustment[1]) { |
| // The range is started after the selection, or ended before the selection, |
| // which is not supported. |
| LOG(DFATAL) << "The deletion range needs to cover whole selection range."; |
| return; |
| } |
| |
| // Move by offset calculated in SetSurroundingText to adjust to the original |
| // text place. |
| size_t before = selection.GetMin() - offsets_for_adjustment[0]; |
| size_t after = offsets_for_adjustment[1] - selection.GetMax(); |
| |
| surrounding_text_tracker_.OnExtendSelectionAndDelete(before, after); |
| ime_delegate_->OnDeleteSurroundingText(before, after); |
| } |
| |
| void WaylandInputMethodContext::OnKeysym(uint32_t keysym, |
| uint32_t state, |
| uint32_t modifiers_bits) { |
| #if BUILDFLAG(USE_XKBCOMMON) |
| auto* layout_engine = KeyboardLayoutEngineManager::GetKeyboardLayoutEngine(); |
| if (!layout_engine) |
| return; |
| |
| // TODO(crbug.com/1289236): This is for the backward compatibility with older |
| // ash-chrome (M101 and earlier). In that version of ash-chrome didn't send |
| // CapsLock so that we hit an issue on using it. |
| // Because newer ash-chrome always sends CapsLock modifier map, as short term |
| // workaround, check the condition to identify whether Lacros is running |
| // on top of enough newer ash-chrome. |
| // To avoid accident, we also check text_input_extension, which is available |
| // only on ash-chrome. |
| // We can remove this workaround check in M104 or later. |
| absl::optional<std::vector<base::StringPiece>> modifiers; |
| if (!connection_->text_input_extension_v1() || |
| base::Contains(modifiers_map_, XKB_MOD_NAME_CAPS)) { |
| std::vector<base::StringPiece> modifier_content; |
| for (size_t i = 0; i < modifiers_map_.size(); ++i) { |
| if (modifiers_bits & (1 << i)) |
| modifier_content.emplace_back(modifiers_map_[i]); |
| } |
| modifiers = std::move(modifier_content); |
| } |
| |
| DomCode dom_code = static_cast<XkbKeyboardLayoutEngine*>(layout_engine) |
| ->GetDomCodeByKeysym(keysym, modifiers); |
| if (dom_code == DomCode::NONE) |
| return; |
| |
| // Keyboard might not exist. |
| int device_id = connection_->seat()->keyboard() |
| ? connection_->seat()->keyboard()->device_id() |
| : 0; |
| |
| EventType type = |
| state == WL_KEYBOARD_KEY_STATE_PRESSED ? ET_KEY_PRESSED : ET_KEY_RELEASED; |
| key_delegate_->OnKeyboardKeyEvent(type, dom_code, /*repeat=*/false, |
| absl::nullopt, EventTimeForNow(), device_id, |
| WaylandKeyboard::KeyEventKind::kKey); |
| #else |
| NOTIMPLEMENTED(); |
| #endif |
| } |
| |
| void WaylandInputMethodContext::OnSetPreeditRegion( |
| int32_t index, |
| uint32_t length, |
| const std::vector<SpanStyle>& spans) { |
| const auto& [surrounding_text, utf16_offset, selection, |
| unused_composition_text] = |
| surrounding_text_tracker_.predicted_state(); |
| |
| std::vector<size_t> selection_utf8_offsets = {selection.start(), |
| selection.end()}; |
| std::string surrounding_text_utf8 = base::UTF16ToUTF8AndAdjustOffsets( |
| surrounding_text, &selection_utf8_offsets); |
| |
| if (surrounding_text.empty() || !selection.IsValid()) { |
| LOG(ERROR) << "SetSurroundingText should run before OnSetPreeditRegion."; |
| return; |
| } |
| |
| // |index| and |length| are expected to be in UTF8. |index| is relative to the |
| // current cursor position. |
| |
| // Validation of index and length. |
| if (index < 0 && selection_utf8_offsets[1] < static_cast<uint32_t>(-index)) { |
| LOG(ERROR) << "Invalid starting point is specified"; |
| return; |
| } |
| size_t begin_utf8 = static_cast<size_t>( |
| static_cast<ssize_t>(selection_utf8_offsets[1]) + index); |
| size_t end_utf8 = begin_utf8 + length; |
| if (end_utf8 > surrounding_text_utf8.size()) { |
| LOG(ERROR) << "Too long preedit range is specified"; |
| return; |
| } |
| |
| std::vector<size_t> offsets = {begin_utf8, end_utf8}; |
| for (const auto& span : spans) { |
| offsets.push_back(begin_utf8 + span.index); |
| offsets.push_back(begin_utf8 + span.index + span.length); |
| } |
| base::UTF8ToUTF16AndAdjustOffsets(surrounding_text_utf8, &offsets); |
| if (offsets[0] == std::u16string::npos || |
| offsets[1] == std::u16string::npos) { |
| LOG(ERROR) << "Invalid range is specified"; |
| return; |
| } |
| |
| std::vector<ImeTextSpan> ime_text_spans; |
| for (size_t i = 0; i < spans.size(); ++i) { |
| size_t begin_span = offsets[i * 2 + 2]; |
| size_t end_span = offsets[i * 2 + 3]; |
| if (begin_span == std::u16string::npos || end_span == std::u16string::npos) |
| continue; |
| if (begin_span < offsets[0] || end_span < offsets[0] || |
| begin_span > offsets[1] || end_span > offsets[1]) { |
| // Out of composition range. |
| continue; |
| } |
| |
| auto style = ConvertStyle(spans[i].style); |
| if (!style.has_value()) |
| continue; |
| ime_text_spans.emplace_back(/* type= */ style->first, |
| begin_span - offsets[0], end_span - offsets[0], |
| /* thickness = */ style->second); |
| } |
| |
| surrounding_text_tracker_.OnSetCompositionFromExistingText( |
| gfx::Range(offsets[0], offsets[1])); |
| ime_delegate_->OnSetPreeditRegion(gfx::Range(offsets[0], offsets[1]), |
| ime_text_spans); |
| } |
| |
| void WaylandInputMethodContext::OnClearGrammarFragments( |
| const gfx::Range& range) { |
| std::u16string surrounding_text = |
| surrounding_text_tracker_.predicted_state().surrounding_text; |
| |
| std::vector<size_t> offsets = {range.start() + surrounding_text_offset_, |
| range.end() + surrounding_text_offset_}; |
| base::UTF8ToUTF16AndAdjustOffsets(base::UTF16ToUTF8(surrounding_text), |
| &offsets); |
| ime_delegate_->OnClearGrammarFragments(gfx::Range( |
| static_cast<uint32_t>(offsets[0]), static_cast<uint32_t>(offsets[1]))); |
| } |
| |
| void WaylandInputMethodContext::OnAddGrammarFragment( |
| const GrammarFragment& fragment) { |
| std::u16string surrounding_text = |
| surrounding_text_tracker_.predicted_state().surrounding_text; |
| |
| std::vector<size_t> offsets = { |
| fragment.range.start() + surrounding_text_offset_, |
| fragment.range.end() + surrounding_text_offset_}; |
| base::UTF8ToUTF16AndAdjustOffsets(base::UTF16ToUTF8(surrounding_text), |
| &offsets); |
| ime_delegate_->OnAddGrammarFragment( |
| {GrammarFragment(gfx::Range(static_cast<uint32_t>(offsets[0]), |
| static_cast<uint32_t>(offsets[1])), |
| fragment.suggestion)}); |
| } |
| |
| void WaylandInputMethodContext::OnSetAutocorrectRange(const gfx::Range& range) { |
| ime_delegate_->OnSetAutocorrectRange( |
| gfx::Range(range.start() + surrounding_text_offset_, |
| range.end() + surrounding_text_offset_)); |
| } |
| |
| void WaylandInputMethodContext::OnSetVirtualKeyboardOccludedBounds( |
| const gfx::Rect& screen_bounds) { |
| ime_delegate_->OnSetVirtualKeyboardOccludedBounds(screen_bounds); |
| |
| for (auto& client : past_clients_) { |
| if (client.second) |
| client.second->EnsureCaretNotInRect(screen_bounds); |
| } |
| if (screen_bounds.IsEmpty()) |
| past_clients_.clear(); |
| } |
| |
| void WaylandInputMethodContext::OnInputPanelState(uint32_t state) { |
| virtual_keyboard_visible_ = (state & 1) != 0; |
| // Note: Currently there's no support of VirtualKeyboardControllerObserver. |
| // In the future, we may need to support it. Specifically, |
| // RenderWidgetHostViewAura would like to know the VirtualKeyboard's |
| // region somehow. |
| } |
| |
| void WaylandInputMethodContext::OnModifiersMap( |
| std::vector<std::string> modifiers_map) { |
| modifiers_map_ = std::move(modifiers_map); |
| } |
| |
| void WaylandInputMethodContext::OnKeyboardFocusedWindowChanged() { |
| MaybeUpdateActivated(false, TextInputClient::FOCUS_REASON_OTHER); |
| } |
| |
| void WaylandInputMethodContext::MaybeUpdateActivated( |
| bool skip_virtual_keyboard_update, |
| TextInputClient::FocusReason reason) { |
| if (!text_input_) |
| return; |
| |
| WaylandWindow* window = |
| connection_->window_manager()->GetCurrentKeyboardFocusedWindow(); |
| if (!window && !connection_->seat()->keyboard()) |
| window = connection_->window_manager()->GetCurrentActiveWindow(); |
| // Activate Wayland IME only if 1) InputMethod in Chrome has some |
| // TextInputClient connected, and 2) the actual keyboard focus of Wayland |
| // is given to Chrome, which is notified via wl_keyboard::enter. |
| // If no keyboard is connected, the current active window is used for 2) |
| // instead (https://crbug.com/1168411). |
| bool activated = focused_ && window; |
| if (activated_ == activated) |
| return; |
| |
| activated_ = activated; |
| if (activated) { |
| text_input_->Activate(window, reason); |
| if (!skip_virtual_keyboard_update) |
| DisplayVirtualKeyboard(); |
| } else { |
| if (!skip_virtual_keyboard_update) |
| DismissVirtualKeyboard(); |
| text_input_->Deactivate(); |
| } |
| } |
| |
| } // namespace ui |