From 53ae034b557440bac532e0d0bbe3a48fc23490a6 Mon Sep 17 00:00:00 2001 From: JunsuChoi Date: Mon, 8 Jun 2026 18:11:33 +0900 Subject: [PATCH] Filter hardware keyboard keys through IMF for Hangul input When a hardware keyboard is connected, the input panel is usually not shown, and key events were never routed to the IMF. As a result, the Hangul/English toggle key was ignored and Hangul composition was impossible. Now key events are filtered through the IMF while a text field is being edited, even when the input panel is hidden. Behavior while the panel is shown is unchanged. Navigation and system keys (direction keys, Return, Select, XF86 keys) always bypass the IMF when the panel is hidden so that app navigation and TV remote control keep working. A TextInputType.none client keeps the IMF context focused with the panel disabled, so it also accepts hardware keyboard composition without showing the on-screen keyboard. Also hardens the surrounding IMF handling: - TextInput.setClient resets the IMF context so that a composition pending from the previous client cannot leak into the new client. - TextInput.setEditingState resets the IMF context only when the new state differs from the current model, so a state echoed back by the framework no longer cancels an ongoing composition. - All TizenInputMethodContext methods are guarded against a null imf_context_ (e.g. on images without an ISE). - The input panel is treated as shown during the WILL_SHOW transition so that keys arriving mid-transition are not misjudged as hidden-path input. - Ecore_Event_Key in HandleNuiKeyEvent is zero-initialized; its compose and dev members were read uninitialized when converting to an Ecore_IMF event. --- .../tizen/channels/text_input_channel.cc | 23 +++- .../tizen/tizen_input_method_context.cc | 104 +++++++++++++++--- .../tizen/tizen_input_method_context.h | 7 ++ .../shell/platform/tizen/tizen_view_nui.cc | 2 +- .../platform/tizen/tizen_window_ecore_wl2.cc | 4 +- 5 files changed, 123 insertions(+), 17 deletions(-) diff --git a/flutter/shell/platform/tizen/channels/text_input_channel.cc b/flutter/shell/platform/tizen/channels/text_input_channel.cc index 7e2b3435..605a5441 100644 --- a/flutter/shell/platform/tizen/channels/text_input_channel.cc +++ b/flutter/shell/platform/tizen/channels/text_input_channel.cc @@ -136,13 +136,18 @@ void TextInputChannel::HandleMethodCall( if (input_type_ != kNoneInputType) { input_method_context_->ShowInputPanel(); } + if (active_model_) { + input_method_context_->SetEditingActive(true); + } } else if (method.compare(kHideMethod) == 0) { + input_method_context_->SetEditingActive(false); input_method_context_->HideInputPanel(); input_method_context_->ResetInputMethodContext(); } else if (method.compare(kSetPlatformViewClient) == 0) { result->NotImplemented(); return; } else if (method.compare(kClearClientMethod) == 0) { + input_method_context_->SetEditingActive(false); active_model_ = nullptr; } else if (method.compare(kSetClientMethod) == 0) { if (!method_call.arguments() || method_call.arguments()->IsNull()) { @@ -212,14 +217,18 @@ void TextInputChannel::HandleMethodCall( // change. See https://github.com/flutter-tizen/engine/pull/194. input_method_context_->HideInputPanel(); if (input_type_ != kNoneInputType) { + input_method_context_->SetInputPanelEnabled(true); input_method_context_->ShowInputPanel(); + } else { + input_method_context_->SetInputPanelEnabled(false); } } } + input_method_context_->ResetInputMethodContext(); active_model_ = std::make_unique(); + input_method_context_->SetEditingActive(true); } else if (method.compare(kSetEditingStateMethod) == 0) { - input_method_context_->ResetInputMethodContext(); if (!method_call.arguments() || method_call.arguments()->IsNull()) { result->Error(kBadArgumentError, "Method invoked without args."); return; @@ -250,8 +259,14 @@ void TextInputChannel::HandleMethodCall( "Selection base/extent values invalid."); return; } + int selection_base_value = selection_base->value.GetInt(); int selection_extent_value = selection_extent->value.GetInt(); + if (active_model_->GetText() != text->value.GetString() || + !(active_model_->selection() == + TextRange(selection_base_value, selection_extent_value))) { + input_method_context_->ResetInputMethodContext(); + } active_model_->SetText(text->value.GetString()); active_model_->SetSelection( @@ -327,10 +342,16 @@ bool TextInputChannel::HandleKey(const char* key, active_model_->AddCodePoint(string[0]); needs_update = true; } else if (key_str == "Return") { + if (active_model_->composing()) { + input_method_context_->ResetInputMethodContext(); + } EnterPressed(); return true; #ifdef TV_PROFILE } else if (key_str == "Select") { + if (active_model_->composing()) { + input_method_context_->ResetInputMethodContext(); + } SelectPressed(); return true; #endif diff --git a/flutter/shell/platform/tizen/tizen_input_method_context.cc b/flutter/shell/platform/tizen/tizen_input_method_context.cc index 869d2c03..76e46d60 100644 --- a/flutter/shell/platform/tizen/tizen_input_method_context.cc +++ b/flutter/shell/platform/tizen/tizen_input_method_context.cc @@ -4,6 +4,8 @@ #include "tizen_input_method_context.h" +#include + #include "flutter/shell/platform/tizen/logger.h" namespace { @@ -115,6 +117,27 @@ T EcoreEventKeyToEcoreImfEvent(Ecore_Event_Key* event) { return imf_event; } +bool IsNavigationOrSystemKey(const char* key) { + if (!key) { + return false; + } + // Multimedia / system / TV remote keys. + if (strncmp(key, "XF86", 4) == 0) { + return true; + } + // Directional and action keys used for app/remote navigation. + static const char* kNavigationKeys[] = { + "Up", "Down", "Left", "Right", "KP_Up", "KP_Down", + "KP_Left", "KP_Right", "Return", "KP_Enter", "Select", + }; + for (const char* nav_key : kNavigationKeys) { + if (strcmp(key, nav_key) == 0) { + return true; + } + } + return false; +} + } // namespace namespace flutter { @@ -165,8 +188,10 @@ TizenInputMethodContext::~TizenInputMethodContext() { bool TizenInputMethodContext::HandleEcoreEventKey(Ecore_Event_Key* event, bool is_down) { - FT_ASSERT(imf_context_); FT_ASSERT(event); + if (!imf_context_) { + return false; + } Ecore_IMF_Event imf_event; if (is_down) { @@ -192,7 +217,7 @@ bool TizenInputMethodContext::HandleNuiKeyEvent(const char* device_name, uint32_t scan_code, size_t timestamp, bool is_down) { - Ecore_Event_Key event; + Ecore_Event_Key event = {}; event.keyname = event.key = key ? key : ""; event.string = string ? string : ""; event.modifiers = modifiers; @@ -227,39 +252,80 @@ bool TizenInputMethodContext::HandleNuiKeyEvent(const char* device_name, #endif InputPanelGeometry TizenInputMethodContext::GetInputPanelGeometry() { - FT_ASSERT(imf_context_); InputPanelGeometry geometry; + if (!imf_context_) { + return geometry; + } ecore_imf_context_input_panel_geometry_get( imf_context_, &geometry.x, &geometry.y, &geometry.w, &geometry.h); return geometry; } void TizenInputMethodContext::ResetInputMethodContext() { - FT_ASSERT(imf_context_); + if (!imf_context_) { + return; + } ecore_imf_context_reset(imf_context_); } void TizenInputMethodContext::ShowInputPanel() { - FT_ASSERT(imf_context_); + if (!imf_context_) { + return; + } ecore_imf_context_input_panel_show(imf_context_); - ecore_imf_context_focus_in(imf_context_); } void TizenInputMethodContext::HideInputPanel() { - FT_ASSERT(imf_context_); - ecore_imf_context_focus_out(imf_context_); + if (!imf_context_) { + return; + } ecore_imf_context_input_panel_hide(imf_context_); } +void TizenInputMethodContext::SetEditingActive(bool active) { + if (!imf_context_) { + return; + } + editing_active_ = active; + if (active) { + ecore_imf_context_focus_in(imf_context_); + } else { + ecore_imf_context_focus_out(imf_context_); + } +} + +void TizenInputMethodContext::SetInputPanelEnabled(bool enabled) { + if (!imf_context_) { + return; + } + ecore_imf_context_input_panel_enabled_set(imf_context_, enabled); +} + +bool TizenInputMethodContext::ShouldFilterKey(const char* key) { + if (!imf_context_) { + return false; + } + if (IsInputPanelShown()) { + return true; + } + return editing_active_ && !IsNavigationOrSystemKey(key); +} + bool TizenInputMethodContext::IsInputPanelShown() { + if (!imf_context_) { + return false; + } Ecore_IMF_Input_Panel_State state = ecore_imf_context_input_panel_state_get(imf_context_); - return state == ECORE_IMF_INPUT_PANEL_STATE_SHOW; + return state == ECORE_IMF_INPUT_PANEL_STATE_SHOW || + state == ECORE_IMF_INPUT_PANEL_STATE_WILL_SHOW; } void TizenInputMethodContext::SetInputPanelLayout( const std::string& input_type) { - FT_ASSERT(imf_context_); + if (!imf_context_) { + return; + } Ecore_IMF_Input_Panel_Layout panel_layout = TextInputTypeToEcoreImfInputPanelLayout(input_type); ecore_imf_context_input_panel_layout_set(imf_context_, panel_layout); @@ -267,6 +333,9 @@ void TizenInputMethodContext::SetInputPanelLayout( void TizenInputMethodContext::SetInputPanelLayoutVariation(bool is_signed, bool is_decimal) { + if (!imf_context_) { + return; + } Ecore_IMF_Input_Panel_Layout_Numberonly_Variation variation; if (is_signed && is_decimal) { variation = @@ -282,6 +351,9 @@ void TizenInputMethodContext::SetInputPanelLayoutVariation(bool is_signed, } void TizenInputMethodContext::SetAutocapitalType(const std::string& type) { + if (!imf_context_) { + return; + } Ecore_IMF_Autocapital_Type autocapital_type = ECORE_IMF_AUTOCAPITAL_TYPE_NONE; if (type == "TextCapitalization.characters") { @@ -356,7 +428,9 @@ void TizenInputMethodContext::RegisterEventCallbacks() { } void TizenInputMethodContext::UnregisterEventCallbacks() { - FT_ASSERT(imf_context_); + if (!imf_context_) { + return; + } ecore_imf_context_event_callback_del( imf_context_, ECORE_IMF_CALLBACK_COMMIT, event_callbacks_[ECORE_IMF_CALLBACK_COMMIT]); @@ -418,7 +492,9 @@ void TizenInputMethodContext::InputPanelStateChangedCallback( } void TizenInputMethodContext::RegisterInputPanelEventCallback() { - FT_ASSERT(imf_context_); + if (!imf_context_) { + return; + } ecore_imf_context_input_panel_event_callback_add( imf_context_, ECORE_IMF_INPUT_PANEL_STATE_EVENT, @@ -426,7 +502,9 @@ void TizenInputMethodContext::RegisterInputPanelEventCallback() { } void TizenInputMethodContext::UnregisterInputPanelEventCallback() { - FT_ASSERT(imf_context_); + if (!imf_context_) { + return; + } ecore_imf_context_input_panel_event_callback_del( imf_context_, ECORE_IMF_INPUT_PANEL_STATE_EVENT, diff --git a/flutter/shell/platform/tizen/tizen_input_method_context.h b/flutter/shell/platform/tizen/tizen_input_method_context.h index 6915dd51..1e1a63c0 100644 --- a/flutter/shell/platform/tizen/tizen_input_method_context.h +++ b/flutter/shell/platform/tizen/tizen_input_method_context.h @@ -53,6 +53,12 @@ class TizenInputMethodContext { bool IsInputPanelShown(); + void SetEditingActive(bool active); + + void SetInputPanelEnabled(bool enabled); + + bool ShouldFilterKey(const char* key); + void SetInputPanelLayout(const std::string& layout); void SetInputPanelLayoutVariation(bool is_signed, bool is_decimal); @@ -93,6 +99,7 @@ class TizenInputMethodContext { Ecore_Device* ecore_device_ = nullptr; #endif Ecore_IMF_Context* imf_context_ = nullptr; + bool editing_active_ = false; OnCommit on_commit_; OnPreeditChanged on_preedit_changed_; OnPreeditStart on_preedit_start_; diff --git a/flutter/shell/platform/tizen/tizen_view_nui.cc b/flutter/shell/platform/tizen/tizen_view_nui.cc index f3b93477..a848a39f 100644 --- a/flutter/shell/platform/tizen/tizen_view_nui.cc +++ b/flutter/shell/platform/tizen/tizen_view_nui.cc @@ -90,7 +90,7 @@ void TizenViewNui::OnKey(const char* device_name, bool is_down) { bool handled = false; - if (input_method_context_->IsInputPanelShown()) { + if (input_method_context_->ShouldFilterKey(key)) { handled = input_method_context_->HandleNuiKeyEvent( device_name, device_class, device_subclass, key, string, modifiers, scan_code, timestamp, is_down); diff --git a/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc b/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc index 41d16b9d..98bfb7cd 100644 --- a/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc +++ b/flutter/shell/platform/tizen/tizen_window_ecore_wl2.cc @@ -567,7 +567,7 @@ void TizenWindowEcoreWl2::RegisterEventHandlers() { auto* key_event = reinterpret_cast(event); if (key_event->window == self->GetWindowId()) { bool handled = false; - if (self->input_method_context_->IsInputPanelShown()) { + if (self->input_method_context_->ShouldFilterKey(key_event->key)) { handled = self->input_method_context_->HandleEcoreEventKey( key_event, true); } @@ -592,7 +592,7 @@ void TizenWindowEcoreWl2::RegisterEventHandlers() { auto* key_event = reinterpret_cast(event); if (key_event->window == self->GetWindowId()) { bool handled = false; - if (self->input_method_context_->IsInputPanelShown()) { + if (self->input_method_context_->ShouldFilterKey(key_event->key)) { handled = self->input_method_context_->HandleEcoreEventKey( key_event, false); }