Fix Windows Enter key after focus loss (#178523)

Fix issue: https://github.com/flutter/flutter/issues/169447

Problem: On Windows, pressing Enter after refocusing the app could drop
the first keydown because the embedder still thought the key was
pressed, causing a double-press requirement and HardwareKeyboard
assertions.
Fix: In KeyboardKeyEmbedderHandler, synthesize a key-up whenever Win32
reports a “fresh” down for a key still tracked as pressed, so Flutter
sees a release before the new down. This keeps modifier/lock-key
synchronization intact.

## Pre-launch Checklist

- [x] I read the [Contributor Guide] and followed the process outlined
there for submitting PRs.
- [x] I read the [Tree Hygiene] wiki page, which explains my
responsibilities.
- [x] I read and followed the [Flutter Style Guide], including [Features
we expect every widget to implement].
- [x] I signed the [CLA].
- [x] I listed at least one issue that this PR fixes in the description
above.
- [x] I updated/added relevant documentation (doc comments with `///`).
- [x] I added new tests to check the change I am making, or this PR is
[test-exempt].
- [x] I followed the [breaking change policy] and added [Data Driven
Fixes] where supported.
- [x] All existing and new tests are passing.
This commit is contained in:
DoLT 2025-11-21 23:32:10 +09:00 committed by GitHub
parent 4c0d007210
commit 8111945eac
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 70 additions and 20 deletions

View File

@ -136,3 +136,4 @@ Jin Jeongsu <jinjs.dev@gmail.com>
Mairon Slusarz <maironlucaslusarz@gmail.com>
Ricardo Dalarme <ricardodalarme@outlook.com>
yiiim <ybz975218925@live.com>
letrungdo <letrdo@gmail.com>

View File

@ -225,22 +225,24 @@ void KeyboardKeyEmbedderHandler::KeyboardHookImpl(
uint64_t result_logical_key;
if (is_event_down) {
if (had_record && !was_down) {
// Windows delivered a new down event without a matching up event. This
// happens if the window lost focus before it could receive the key up.
// Synthesize an up event so the framework releases the key before
// processing the incoming down.
SendSynthesizeUpEvent(physical_key, last_logical_record);
last_logical_record_iter = pressingRecords_.find(physical_key);
had_record = last_logical_record_iter != pressingRecords_.end();
last_logical_record = had_record ? last_logical_record_iter->second : 0;
}
if (had_record) {
if (was_down) {
// A normal repeated key.
type = kFlutterKeyEventTypeRepeat;
FML_DCHECK(had_record);
ConvertUtf32ToUtf8_(character_bytes, character);
eventual_logical_record = last_logical_record;
result_logical_key = last_logical_record;
} else {
// A non-repeated key has been pressed that has the exact physical key
// as a currently pressed one, usually indicating multiple keyboards are
// pressing keys with the same physical key, or the up event was lost
// during a loss of focus. The down event is ignored.
callback(true);
return;
}
// A normal repeated key.
type = kFlutterKeyEventTypeRepeat;
FML_DCHECK(had_record);
ConvertUtf32ToUtf8_(character_bytes, character);
eventual_logical_record = last_logical_record;
result_logical_key = last_logical_record;
} else {
// A normal down event (whether the system event is a repeat or not).
type = kFlutterKeyEventTypeDown;

View File

@ -747,17 +747,30 @@ TEST(KeyboardKeyEmbedderHandlerTest, RepeatedDownIsIgnored) {
// KeyA's key up is missed.
// Press A again (should yield an empty event)
// Press A again (should synthesize an up event followed by a new down).
last_handled = false;
handler->KeyboardHook(
kVirtualKeyA, kScanCodeKeyA, WM_KEYDOWN, 'a', false, false,
[&last_handled](bool handled) { last_handled = handled; });
EXPECT_EQ(last_handled, true);
EXPECT_EQ(results.size(), 1);
EXPECT_EQ(last_handled, false);
ASSERT_EQ(results.size(), 2u);
event = &results[0];
EXPECT_EQ(event->physical, 0);
EXPECT_EQ(event->logical, 0);
EXPECT_EQ(event->type, kFlutterKeyEventTypeUp);
EXPECT_EQ(event->physical, kPhysicalKeyA);
EXPECT_EQ(event->logical, kLogicalKeyA);
EXPECT_STREQ(event->character, "");
EXPECT_EQ(event->synthesized, true);
EXPECT_EQ(event->callback, nullptr);
event = &results[1];
EXPECT_EQ(event->type, kFlutterKeyEventTypeDown);
EXPECT_EQ(event->physical, kPhysicalKeyA);
EXPECT_EQ(event->logical, kLogicalKeyA);
EXPECT_STREQ(event->character, "a");
EXPECT_EQ(event->synthesized, false);
event->callback(true, event->user_data);
EXPECT_EQ(last_handled, true);
results.clear();
}

View File

@ -1027,6 +1027,40 @@ TEST_F(KeyboardTest, RestartClearsKeyboardState) {
EXPECT_EQ(tester.RedispatchedMessageCountAndClear(), 0);
}
// Press Enter, lose focus before the up event arrives, and press Enter again
// once focus returns. The second down event should not be dropped.
TEST_F(KeyboardTest, FreshKeyDownAfterMissedUpIsDelivered) {
KeyboardTester tester{GetContext()};
tester.Responding(true);
tester.InjectKeyboardChanges(std::vector<KeyboardChange>{
WmKeyDownInfo{VK_RETURN, kScanCodeEnter, kNotExtended, kWasUp}.Build(
kWmResultZero)});
tester.InjectKeyboardChanges(std::vector<KeyboardChange>{
WmKeyDownInfo{VK_RETURN, kScanCodeEnter, kNotExtended, kWasUp}.Build(
kWmResultZero)});
ASSERT_EQ(tester.key_calls.size(), 3u);
EXPECT_CALL_IS_EVENT(tester.key_calls[0], kFlutterKeyEventTypeDown,
kPhysicalEnter, kLogicalEnter, "", kNotSynthesized);
EXPECT_CALL_IS_EVENT(tester.key_calls[1], kFlutterKeyEventTypeUp,
kPhysicalEnter, kLogicalEnter, "", kSynthesized);
EXPECT_CALL_IS_EVENT(tester.key_calls[2], kFlutterKeyEventTypeDown,
kPhysicalEnter, kLogicalEnter, "", kNotSynthesized);
tester.clear_key_calls();
tester.InjectKeyboardChanges(std::vector<KeyboardChange>{
WmKeyUpInfo{VK_RETURN, kScanCodeEnter, kNotExtended}.Build(
kWmResultZero)});
ASSERT_EQ(tester.key_calls.size(), 1u);
EXPECT_CALL_IS_EVENT(tester.key_calls[0], kFlutterKeyEventTypeUp,
kPhysicalEnter, kLogicalEnter, "", kNotSynthesized);
tester.clear_key_calls();
EXPECT_EQ(tester.RedispatchedMessageCountAndClear(), 0u);
}
// Press Shift-A. This is special because Win32 gives 'A' as character for the
// KeyA press.
TEST_F(KeyboardTest, ShiftLeftKeyA) {