diff --git a/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder.cc b/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder.cc index 37bdc46c63..44fa1b36ac 100644 --- a/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder.cc +++ b/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder.cc @@ -375,6 +375,15 @@ typedef struct { double timestamp; } SyncStateLoopContext; +// Context variables for the foreach call used to find the physical key from +// a modifier logical key. +typedef struct { + bool known_modifier_physical_key; + uint64_t logical_key; + uint64_t physical_key_from_event; + uint64_t corrected_physical_key; +} ModifierLogicalToPhysicalContext; + } // namespace // Update the pressing record. @@ -696,6 +705,69 @@ static void synchronize_lock_states_loop_body(gpointer key, } } +// Find if a given physical key is the primary physical of one of the known +// modifier keys. +// +// This is used as the body of a loop over #modifier_bit_to_checked_keys. +static void is_known_modifier_physical_key_loop_body(gpointer key, + gpointer value, + gpointer user_data) { + ModifierLogicalToPhysicalContext* context = + reinterpret_cast(user_data); + FlKeyEmbedderCheckedKey* checked_key = + reinterpret_cast(value); + + if (checked_key->primary_physical_key == context->physical_key_from_event) { + context->known_modifier_physical_key = true; + } +} + +// Return the primary physical key of a known modifier key which matches the +// given logical key. +// +// This is used as the body of a loop over #modifier_bit_to_checked_keys. +static void find_physical_from_logical_loop_body(gpointer key, + gpointer value, + gpointer user_data) { + ModifierLogicalToPhysicalContext* context = + reinterpret_cast(user_data); + FlKeyEmbedderCheckedKey* checked_key = + reinterpret_cast(value); + + if (checked_key->primary_logical_key == context->logical_key || + checked_key->secondary_logical_key == context->logical_key) { + context->corrected_physical_key = checked_key->primary_physical_key; + } +} + +static uint64_t corrected_modifier_physical_key( + GHashTable* modifier_bit_to_checked_keys, + uint64_t physical_key_from_event, + uint64_t logical_key) { + ModifierLogicalToPhysicalContext logical_to_physical_context; + logical_to_physical_context.known_modifier_physical_key = false; + logical_to_physical_context.physical_key_from_event = physical_key_from_event; + logical_to_physical_context.logical_key = logical_key; + // If no match is found, defaults to the physical key retrieved from the + // event. + logical_to_physical_context.corrected_physical_key = physical_key_from_event; + + // Check if the physical key is one of the known modifier physical key. + g_hash_table_foreach(modifier_bit_to_checked_keys, + is_known_modifier_physical_key_loop_body, + &logical_to_physical_context); + + // If the physical key matches a known modifier key, find the modifier + // physical key from the logical key. + if (logical_to_physical_context.known_modifier_physical_key) { + g_hash_table_foreach(modifier_bit_to_checked_keys, + find_physical_from_logical_loop_body, + &logical_to_physical_context); + } + + return logical_to_physical_context.corrected_physical_key; +} + static void fl_key_embedder_responder_handle_event_impl( FlKeyResponder* responder, FlKeyEvent* event, @@ -707,10 +779,12 @@ static void fl_key_embedder_responder_handle_event_impl( g_return_if_fail(event != nullptr); g_return_if_fail(callback != nullptr); - const uint64_t physical_key = event_to_physical_key(event); const uint64_t logical_key = specified_logical_key != 0 ? specified_logical_key : event_to_logical_key(event); + const uint64_t physical_key_from_event = event_to_physical_key(event); + const uint64_t physical_key = corrected_modifier_physical_key( + self->modifier_bit_to_checked_keys, physical_key_from_event, logical_key); const double timestamp = event_to_timestamp(event); const bool is_down_event = event->is_press; diff --git a/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder_test.cc b/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder_test.cc index 7806b9b1c6..67337b3f32 100644 --- a/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder_test.cc +++ b/engine/src/flutter/shell/platform/linux/fl_key_embedder_responder_test.cc @@ -23,6 +23,7 @@ constexpr guint16 kKeyCodeDigit1 = 0x0au; constexpr guint16 kKeyCodeKeyA = 0x26u; constexpr guint16 kKeyCodeShiftLeft = 0x32u; constexpr guint16 kKeyCodeShiftRight = 0x3Eu; +constexpr guint16 kKeyCodeAltLeft = 0x40u; constexpr guint16 kKeyCodeAltRight = 0x6Cu; constexpr guint16 kKeyCodeNumpad1 = 0x57u; constexpr guint16 kKeyCodeNumLock = 0x4Du; @@ -1734,3 +1735,73 @@ TEST(FlKeyEmbedderResponderTest, HandlesShiftAltVersusGroupNext) { clear_g_call_records(); g_object_unref(responder); } + +// Shift + AltLeft results in GDK event whose keyval is MetaLeft but whose +// keycode is either AltLeft or Shift keycode (depending on which one was +// released last). The physical key is usually deduced from the keycode, but in +// this case (Shift + AltLeft) a correction is needed otherwise the physical +// key won't be the MetaLeft one. +// Regression test for https://github.com/flutter/flutter/issues/96082 +TEST(FlKeyEmbedderResponderTest, HandlesShiftAltLeftIsMetaLeft) { + EXPECT_EQ(g_call_records, nullptr); + g_call_records = g_ptr_array_new_with_free_func(g_object_unref); + FlKeyResponder* responder = FL_KEY_RESPONDER( + fl_key_embedder_responder_new(record_calls_in(g_call_records))); + + g_expected_handled = true; + guint32 now_time = 1; + // A convenient shorthand to simulate events. + auto send_key_event = [responder, &now_time](bool is_press, guint keyval, + guint16 keycode, int state) { + now_time += 1; + int user_data = 123; // Arbitrary user data + fl_key_responder_handle_event( + responder, + fl_key_event_new_by_mock(now_time, is_press, keyval, keycode, state, + kIsModifier), + verify_response_handled, &user_data); + }; + + FlKeyEmbedderCallRecord* record; + + // ShiftLeft + AltLeft + send_key_event(kPress, GDK_KEY_Shift_L, kKeyCodeShiftLeft, 0x2000000); + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalShiftLeft); + EXPECT_EQ(record->event->logical, kLogicalShiftLeft); + EXPECT_EQ(record->event->synthesized, false); + + send_key_event(kPress, GDK_KEY_Meta_L, kKeyCodeAltLeft, 0x2000001); + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalMetaLeft); + EXPECT_EQ(record->event->logical, kLogicalMetaLeft); + EXPECT_EQ(record->event->synthesized, false); + + send_key_event(kRelease, GDK_KEY_Meta_L, kKeyCodeAltLeft, 0x2002000); + send_key_event(kRelease, GDK_KEY_Shift_L, kKeyCodeShiftLeft, 0x2000000); + g_ptr_array_clear(g_call_records); + + // ShiftRight + AltLeft + send_key_event(kPress, GDK_KEY_Shift_R, kKeyCodeShiftRight, 0x2000000); + EXPECT_EQ(g_call_records->len, 1u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 0)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalShiftRight); + EXPECT_EQ(record->event->logical, kLogicalShiftRight); + EXPECT_EQ(record->event->synthesized, false); + + send_key_event(kPress, GDK_KEY_Meta_L, kKeyCodeAltLeft, 0x2000001); + EXPECT_EQ(g_call_records->len, 2u); + record = FL_KEY_EMBEDDER_CALL_RECORD(g_ptr_array_index(g_call_records, 1)); + EXPECT_EQ(record->event->type, kFlutterKeyEventTypeDown); + EXPECT_EQ(record->event->physical, kPhysicalMetaLeft); + EXPECT_EQ(record->event->logical, kLogicalMetaLeft); + EXPECT_EQ(record->event->synthesized, false); + + clear_g_call_records(); + g_object_unref(responder); +}