-
Notifications
You must be signed in to change notification settings - Fork 6k
Implement delayed key event synthesis for Windows #23524
Changes from all commits
15639b8
25c6337
995c65f
bbb0253
b22d189
ee4ab04
76ce7c4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -10,6 +10,10 @@ | |
|
|
||
| #include "flutter/shell/platform/common/cpp/json_message_codec.h" | ||
|
|
||
| namespace flutter { | ||
|
|
||
| namespace { | ||
|
|
||
| static constexpr char kChannelName[] = "flutter/keyevent"; | ||
|
|
||
| static constexpr char kKeyCodeKey[] = "keyCode"; | ||
|
|
@@ -18,33 +22,35 @@ static constexpr char kCharacterCodePointKey[] = "characterCodePoint"; | |
| static constexpr char kModifiersKey[] = "modifiers"; | ||
| static constexpr char kKeyMapKey[] = "keymap"; | ||
| static constexpr char kTypeKey[] = "type"; | ||
| static constexpr char kHandledKey[] = "handled"; | ||
|
|
||
| static constexpr char kWindowsKeyMap[] = "windows"; | ||
| static constexpr char kKeyUp[] = "keyup"; | ||
| static constexpr char kKeyDown[] = "keydown"; | ||
|
|
||
| namespace flutter { | ||
| // The maximum number of pending events to keep before | ||
| // emitting a warning on the console about unhandled events. | ||
| static constexpr int kMaxPendingEvents = 1000; | ||
|
|
||
| // Re-definition of the modifiers for compatibility with the Flutter framework. | ||
| // These have to be in sync with the framework's RawKeyEventDataWindows | ||
| // modifiers definition. | ||
| // https://github.com/flutter/flutter/blob/19ff596979e407c484a32f4071420fca4f4c885f/packages/flutter/lib/src/services/raw_keyboard_windows.dart#L203 | ||
| const int kShift = 1 << 0; | ||
| const int kShiftLeft = 1 << 1; | ||
| const int kShiftRight = 1 << 2; | ||
| const int kControl = 1 << 3; | ||
| const int kControlLeft = 1 << 4; | ||
| const int kControlRight = 1 << 5; | ||
| const int kAlt = 1 << 6; | ||
| const int kAltLeft = 1 << 7; | ||
| const int kAltRight = 1 << 8; | ||
| const int kWinLeft = 1 << 9; | ||
| const int kWinRight = 1 << 10; | ||
| const int kCapsLock = 1 << 11; | ||
| const int kNumLock = 1 << 12; | ||
| const int kScrollLock = 1 << 13; | ||
| static constexpr int kShift = 1 << 0; | ||
| static constexpr int kShiftLeft = 1 << 1; | ||
| static constexpr int kShiftRight = 1 << 2; | ||
| static constexpr int kControl = 1 << 3; | ||
| static constexpr int kControlLeft = 1 << 4; | ||
| static constexpr int kControlRight = 1 << 5; | ||
| static constexpr int kAlt = 1 << 6; | ||
| static constexpr int kAltLeft = 1 << 7; | ||
| static constexpr int kAltRight = 1 << 8; | ||
| static constexpr int kWinLeft = 1 << 9; | ||
| static constexpr int kWinRight = 1 << 10; | ||
| static constexpr int kCapsLock = 1 << 11; | ||
| static constexpr int kNumLock = 1 << 12; | ||
| static constexpr int kScrollLock = 1 << 13; | ||
|
|
||
| namespace { | ||
| /// Calls GetKeyState() an all modifier keys and packs the result in an int, | ||
| /// with the re-defined values declared above for compatibility with the Flutter | ||
| /// framework. | ||
|
|
@@ -81,25 +87,130 @@ int GetModsForKeyState() { | |
| mods |= kScrollLock; | ||
| return mods; | ||
| } | ||
|
|
||
| // This uses event data instead of generating a serial number because | ||
| // information can't be attached to the redispatched events, so it has to be | ||
| // possible to compute an ID from the identifying data in the event when it is | ||
| // received again in order to differentiate between events that are new, and | ||
| // events that have been redispatched. | ||
| // | ||
| // Another alternative would be to compute a checksum from all the data in the | ||
| // event (just compute it over the bytes in the struct, probably skipping | ||
| // timestamps), but the fields used below are enough to differentiate them, and | ||
| // since Windows does some processing on the events (coming up with virtual key | ||
| // codes, setting timestamps, etc.), it's not clear that the redispatched | ||
| // events would have the same checksums. | ||
| uint64_t CalculateEventId(int scancode, int action, bool extended) { | ||
| // Calculate a key event ID based on the scan code of the key pressed, | ||
| // and the flags we care about. | ||
| return scancode | (((action == WM_KEYUP ? KEYEVENTF_KEYUP : 0x0) | | ||
| (extended ? KEYEVENTF_EXTENDEDKEY : 0x0)) | ||
| << 16); | ||
| } | ||
|
|
||
| } // namespace | ||
|
|
||
| KeyEventHandler::KeyEventHandler(flutter::BinaryMessenger* messenger) | ||
| KeyEventHandler::KeyEventHandler(flutter::BinaryMessenger* messenger, | ||
| KeyEventHandler::SendInputDelegate send_input) | ||
| : channel_( | ||
| std::make_unique<flutter::BasicMessageChannel<rapidjson::Document>>( | ||
| messenger, | ||
| kChannelName, | ||
| &flutter::JsonMessageCodec::GetInstance())) {} | ||
| &flutter::JsonMessageCodec::GetInstance())), | ||
| send_input_(send_input) { | ||
| assert(send_input != nullptr); | ||
| } | ||
|
|
||
| KeyEventHandler::~KeyEventHandler() = default; | ||
|
|
||
| void KeyEventHandler::TextHook(FlutterWindowsView* view, | ||
| const std::u16string& code_point) {} | ||
|
|
||
| void KeyEventHandler::KeyboardHook(FlutterWindowsView* view, | ||
| KEYBDINPUT* KeyEventHandler::FindPendingEvent(uint64_t id) { | ||
| if (pending_events_.empty()) { | ||
| return nullptr; | ||
| } | ||
| for (auto iter = pending_events_.begin(); iter != pending_events_.end(); | ||
| ++iter) { | ||
| if (iter->first == id) { | ||
| return &iter->second; | ||
| } | ||
| } | ||
| return nullptr; | ||
| } | ||
|
|
||
| void KeyEventHandler::RemovePendingEvent(uint64_t id) { | ||
| for (auto iter = pending_events_.begin(); iter != pending_events_.end(); | ||
| ++iter) { | ||
| if (iter->first == id) { | ||
| pending_events_.erase(iter); | ||
| return; | ||
| } | ||
| } | ||
| std::cerr << "Tried to remove pending event with id " << id | ||
| << ", but the event was not found." << std::endl; | ||
| } | ||
|
|
||
| void KeyEventHandler::AddPendingEvent(uint64_t id, | ||
| int scancode, | ||
| int action, | ||
| bool extended) { | ||
| if (pending_events_.size() > kMaxPendingEvents) { | ||
| std::cerr | ||
| << "There are " << pending_events_.size() | ||
| << " keyboard events that have not yet received a response from the " | ||
| << "framework. Are responses being sent?" << std::endl; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Exit with
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wasn't sure it merited crashing the app though. But I guess it's pretty unlikely, and getting crash reports will be better than failing silently. I don't see any macro called
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Sorry, it's actually
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done. |
||
| } | ||
| KEYBDINPUT key_event = KEYBDINPUT{0}; | ||
| key_event.wScan = scancode; | ||
| key_event.dwFlags = KEYEVENTF_SCANCODE | | ||
| (extended ? KEYEVENTF_EXTENDEDKEY : 0x0) | | ||
| (action == WM_KEYUP ? KEYEVENTF_KEYUP : 0x0); | ||
| pending_events_.push_back(std::make_pair(id, key_event)); | ||
| } | ||
|
|
||
| void KeyEventHandler::HandleResponse(bool handled, | ||
| uint64_t id, | ||
| int action, | ||
| bool extended, | ||
| int scancode, | ||
| int character) { | ||
| if (handled) { | ||
| this->RemovePendingEvent(id); | ||
| } else { | ||
| // Since the framework didn't handle the event, we inject a newly | ||
| // synthesized one. We let Windows figure out the virtual key and | ||
| // character for the given scancode, as well as a new timestamp. | ||
| const KEYBDINPUT* key_event = this->FindPendingEvent(id); | ||
| if (key_event == nullptr) { | ||
| std::cerr << "Unable to find event " << id << " in pending events queue."; | ||
| return; | ||
| } | ||
| INPUT input_event; | ||
| input_event.type = INPUT_KEYBOARD; | ||
| input_event.ki = *key_event; | ||
| UINT accepted = send_input_(1, &input_event, sizeof(input_event)); | ||
| if (accepted != 1) { | ||
| std::cerr << "Unable to synthesize event for unhandled keyboard event " | ||
| "with scancode " | ||
| << scancode << " (character " << character << ")" << std::endl; | ||
| } | ||
| } | ||
| } | ||
|
|
||
| bool KeyEventHandler::KeyboardHook(FlutterWindowsView* view, | ||
| int key, | ||
| int scancode, | ||
| int action, | ||
| char32_t character) { | ||
| char32_t character, | ||
| bool extended) { | ||
| const uint64_t id = CalculateEventId(scancode, action, extended); | ||
| if (FindPendingEvent(id) != nullptr) { | ||
| // Don't pass messages that we synthesized to the framework again. | ||
| RemovePendingEvent(id); | ||
| return false; | ||
| } | ||
|
|
||
| // TODO: Translate to a cross-platform key code system rather than passing | ||
| // the native key code. | ||
| rapidjson::Document event(rapidjson::kObjectType); | ||
|
|
@@ -119,9 +230,17 @@ void KeyEventHandler::KeyboardHook(FlutterWindowsView* view, | |
| break; | ||
| default: | ||
| std::cerr << "Unknown key event action: " << action << std::endl; | ||
| return; | ||
| return false; | ||
| } | ||
| channel_->Send(event); | ||
| AddPendingEvent(id, scancode, action, extended); | ||
| channel_->Send(event, [this, id, action, extended, scancode, character]( | ||
| const uint8_t* reply, size_t reply_size) { | ||
| auto decoded = flutter::JsonMessageCodec::GetInstance().DecodeMessage( | ||
| reply, reply_size); | ||
| bool handled = (*decoded)[kHandledKey].GetBool(); | ||
| this->HandleResponse(handled, id, action, extended, scancode, character); | ||
| }); | ||
| return true; | ||
| } | ||
|
|
||
| } // namespace flutter | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why use this complicated calculation instead of self-incrementing ID?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because I can't attach any information to the redispatched events, so I have to be able to compute an ID from the identifying data in the event when I receive it again in order to differentiate between events that are new, and events that have been redispatched.
Another alternative would be to compute a checksum from all the data in the event (just compute it over the bytes in the struct, skipping timestamps), but I think that these fields are enough to differentiate them, and since Windows does some processing on the events, coming up with virtual key codes, setting timestamps, etc., so I'm not sure that the redispatched events would have the same checksums.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Great explanation. Can you add it to the code?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea, done!