| // Copyright 2013 The Flutter Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "flutter/shell/platform/windows/keyboard_key_embedder_handler.h" |
| |
| #include <assert.h> |
| #include <windows.h> |
| |
| #include <chrono> |
| #include <iostream> |
| #include <string> |
| |
| #include "flutter/shell/platform/windows/keyboard_win32_common.h" |
| |
| namespace flutter { |
| |
| namespace { |
| // An arbitrary size for the character cache in bytes. |
| // |
| // It should hold a UTF-32 character encoded in UTF-8 as well as the trailing |
| // '\0'. |
| constexpr size_t kCharacterCacheSize = 8; |
| |
| constexpr SHORT kStateMaskToggled = 0x01; |
| constexpr SHORT kStateMaskPressed = 0x80; |
| |
| const char* empty_character = ""; |
| |
| // Get some bits of the char, from the start'th bit from the right (excluded) |
| // to the end'th bit from the right (included). |
| // |
| // For example, _GetBit(0x1234, 8, 4) => 0x3. |
| char _GetBit(char32_t ch, size_t start, size_t end) { |
| return (ch >> end) & ((1 << (start - end)) - 1); |
| } |
| } // namespace |
| |
| std::string ConvertChar32ToUtf8(char32_t ch) { |
| std::string result; |
| assert(0 <= ch && ch <= 0x10FFFF); |
| if (ch <= 0x007F) { |
| result.push_back(ch); |
| } else if (ch <= 0x07FF) { |
| result.push_back(0b11000000 + _GetBit(ch, 11, 6)); |
| result.push_back(0b10000000 + _GetBit(ch, 6, 0)); |
| } else if (ch <= 0xFFFF) { |
| result.push_back(0b11100000 + _GetBit(ch, 16, 12)); |
| result.push_back(0b10000000 + _GetBit(ch, 12, 6)); |
| result.push_back(0b10000000 + _GetBit(ch, 6, 0)); |
| } else { |
| result.push_back(0b11110000 + _GetBit(ch, 21, 18)); |
| result.push_back(0b10000000 + _GetBit(ch, 18, 12)); |
| result.push_back(0b10000000 + _GetBit(ch, 12, 6)); |
| result.push_back(0b10000000 + _GetBit(ch, 6, 0)); |
| } |
| return result; |
| } |
| |
| KeyboardKeyEmbedderHandler::KeyboardKeyEmbedderHandler( |
| SendEventHandler send_event, |
| GetKeyStateHandler get_key_state, |
| MapVirtualKeyToScanCode map_virtual_key_to_scan_code) |
| : perform_send_event_(send_event), |
| get_key_state_(get_key_state), |
| response_id_(1) { |
| InitCriticalKeys(map_virtual_key_to_scan_code); |
| } |
| |
| KeyboardKeyEmbedderHandler::~KeyboardKeyEmbedderHandler() = default; |
| |
| static bool isEasciiPrintable(int codeUnit) { |
| return (codeUnit <= 0x7f && codeUnit >= 0x20) || |
| (codeUnit <= 0xff && codeUnit >= 0x80); |
| } |
| |
| // Converts upper letters to lower letters in ASCII and extended ASCII, and |
| // returns as-is otherwise. |
| // |
| // Independent of locale. |
| static uint64_t toLower(uint64_t n) { |
| constexpr uint64_t lower_a = 0x61; |
| constexpr uint64_t upper_a = 0x41; |
| constexpr uint64_t upper_z = 0x5a; |
| |
| constexpr uint64_t lower_a_grave = 0xe0; |
| constexpr uint64_t upper_a_grave = 0xc0; |
| constexpr uint64_t upper_thorn = 0xde; |
| constexpr uint64_t division = 0xf7; |
| |
| // ASCII range. |
| if (n >= upper_a && n <= upper_z) { |
| return n - upper_a + lower_a; |
| } |
| |
| // EASCII range. |
| if (n >= upper_a_grave && n <= upper_thorn && n != division) { |
| return n - upper_a_grave + lower_a_grave; |
| } |
| |
| return n; |
| } |
| |
| // Transform scancodes sent by windows to scancodes written in Chromium spec. |
| static uint16_t normalizeScancode(int windowsScanCode, bool extended) { |
| // In Chromium spec the extended bit is shown as 0xe000 bit, |
| // e.g. PageUp is represented as 0xe049. |
| return (windowsScanCode & 0xff) | (extended ? 0xe000 : 0); |
| } |
| |
| uint64_t KeyboardKeyEmbedderHandler::ApplyPlaneToId(uint64_t id, |
| uint64_t plane) { |
| return (id & valueMask) | plane; |
| } |
| |
| uint64_t KeyboardKeyEmbedderHandler::GetPhysicalKey(int scancode, |
| bool extended) { |
| int chromiumScancode = normalizeScancode(scancode, extended); |
| auto resultIt = windowsToPhysicalMap_.find(chromiumScancode); |
| if (resultIt != windowsToPhysicalMap_.end()) |
| return resultIt->second; |
| return ApplyPlaneToId(scancode, windowsPlane); |
| } |
| |
| uint64_t KeyboardKeyEmbedderHandler::GetLogicalKey(int key, |
| bool extended, |
| int scancode) { |
| if (key == VK_PROCESSKEY) { |
| return VK_PROCESSKEY; |
| } |
| |
| // Normally logical keys should only be derived from key codes, but since some |
| // key codes are either 0 or ambiguous (multiple keys using the same key |
| // code), these keys are resolved by scan codes. |
| auto numpadIter = |
| scanCodeToLogicalMap_.find(normalizeScancode(scancode, extended)); |
| if (numpadIter != scanCodeToLogicalMap_.cend()) |
| return numpadIter->second; |
| |
| // Check if the keyCode is one we know about and have a mapping for. |
| auto logicalIt = windowsToLogicalMap_.find(key); |
| if (logicalIt != windowsToLogicalMap_.cend()) |
| return logicalIt->second; |
| |
| // Upper case letters should be normalized into lower case letters. |
| if (isEasciiPrintable(key)) { |
| return ApplyPlaneToId(toLower(key), unicodePlane); |
| } |
| |
| return ApplyPlaneToId(toLower(key), windowsPlane); |
| } |
| |
| void KeyboardKeyEmbedderHandler::KeyboardHookImpl( |
| int key, |
| int scancode, |
| int action, |
| char32_t character, |
| bool extended, |
| bool was_down, |
| std::function<void(bool)> callback) { |
| const uint64_t physical_key = GetPhysicalKey(scancode, extended); |
| const uint64_t logical_key = GetLogicalKey(key, extended, scancode); |
| assert(action == WM_KEYDOWN || action == WM_KEYUP || |
| action == WM_SYSKEYDOWN || action == WM_SYSKEYUP); |
| |
| auto last_logical_record_iter = pressingRecords_.find(physical_key); |
| const bool had_record = last_logical_record_iter != pressingRecords_.end(); |
| const uint64_t last_logical_record = |
| had_record ? last_logical_record_iter->second : 0; |
| |
| // The logical key for the current "tap sequence". |
| // |
| // Key events are formed in tap sequences: down, repeats, up. The logical key |
| // stays consistent throughout a tap sequence, which is this value. |
| uint64_t sequence_logical_key = |
| had_record ? last_logical_record : logical_key; |
| |
| if (sequence_logical_key == VK_PROCESSKEY) { |
| // VK_PROCESSKEY means that the key press is used by an IME. These key |
| // presses are considered handled and not sent to Flutter. These events must |
| // be filtered by result_logical_key because the key up event of such |
| // presses uses the "original" logical key. |
| callback(true); |
| return; |
| } |
| |
| const bool is_event_down = action == WM_KEYDOWN || action == WM_SYSKEYDOWN; |
| |
| UpdateLastSeenCritialKey(key, physical_key, sequence_logical_key); |
| // Synchronize the toggled states of critical keys (such as whether CapsLocks |
| // is enabled). Toggled states can only be changed upon a down event, so if |
| // the recorded toggled state does not match the true state, this function |
| // will synthesize (an up event if the key is recorded pressed, then) a down |
| // event. |
| // |
| // After this function, all critical keys will have their toggled state |
| // updated to the true state, while the critical keys whose toggled state have |
| // been changed will be pressed regardless of their true pressed state. |
| // Updating the pressed state will be done by SynchronizeCritialPressedStates. |
| SynchronizeCritialToggledStates(key, is_event_down); |
| // Synchronize the pressed states of critical keys (such as whether CapsLocks |
| // is pressed). |
| // |
| // After this function, all critical keys except for the target key will have |
| // their toggled state and pressed state matched with their true states. The |
| // target key's pressed state will be updated immediately after this. |
| SynchronizeCritialPressedStates(key, physical_key, is_event_down); |
| |
| // The resulting event's `type`. |
| FlutterKeyEventType type; |
| character = UndeadChar(character); |
| char character_bytes[kCharacterCacheSize]; |
| // What pressingRecords_[physical_key] should be after the KeyboardHookImpl |
| // returns (0 if the entry should be removed). |
| uint64_t eventual_logical_record; |
| uint64_t result_logical_key; |
| |
| if (is_event_down) { |
| if (had_record) { |
| if (was_down) { |
| // A normal repeated key. |
| type = kFlutterKeyEventTypeRepeat; |
| assert(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; |
| } |
| } else { |
| // A normal down event (whether the system event is a repeat or not). |
| type = kFlutterKeyEventTypeDown; |
| assert(!had_record); |
| ConvertUtf32ToUtf8_(character_bytes, character); |
| eventual_logical_record = logical_key; |
| result_logical_key = logical_key; |
| } |
| } else { // isPhysicalDown is false |
| if (last_logical_record == 0) { |
| // The physical key has been released before. It might indicate a missed |
| // event due to loss of focus, or multiple keyboards pressed keys with the |
| // same physical key. Ignore the up event. |
| callback(true); |
| return; |
| } else { |
| // A normal up event. |
| type = kFlutterKeyEventTypeUp; |
| assert(had_record); |
| // Up events never have character. |
| character_bytes[0] = '\0'; |
| eventual_logical_record = 0; |
| result_logical_key = last_logical_record; |
| } |
| } |
| |
| if (eventual_logical_record != 0) { |
| pressingRecords_[physical_key] = eventual_logical_record; |
| } else { |
| auto record_iter = pressingRecords_.find(physical_key); |
| assert(record_iter != pressingRecords_.end()); |
| pressingRecords_.erase(record_iter); |
| } |
| |
| FlutterKeyEvent key_data{ |
| .struct_size = sizeof(FlutterKeyEvent), |
| .timestamp = static_cast<double>( |
| std::chrono::duration_cast<std::chrono::microseconds>( |
| std::chrono::high_resolution_clock::now().time_since_epoch()) |
| .count()), |
| .type = type, |
| .physical = physical_key, |
| .logical = result_logical_key, |
| .character = character_bytes, |
| .synthesized = false, |
| }; |
| |
| response_id_ += 1; |
| uint64_t response_id = response_id_; |
| PendingResponse pending{ |
| .callback = |
| [this, callback = std::move(callback)](bool handled, |
| uint64_t response_id) { |
| auto found = pending_responses_.find(response_id); |
| if (found != pending_responses_.end()) { |
| pending_responses_.erase(found); |
| } |
| callback(handled); |
| }, |
| .response_id = response_id, |
| }; |
| auto pending_ptr = std::make_unique<PendingResponse>(std::move(pending)); |
| pending_responses_[response_id] = std::move(pending_ptr); |
| SendEvent(key_data, KeyboardKeyEmbedderHandler::HandleResponse, |
| reinterpret_cast<void*>(pending_responses_[response_id].get())); |
| } |
| |
| void KeyboardKeyEmbedderHandler::KeyboardHook( |
| int key, |
| int scancode, |
| int action, |
| char32_t character, |
| bool extended, |
| bool was_down, |
| std::function<void(bool)> callback) { |
| sent_any_events = false; |
| KeyboardHookImpl(key, scancode, action, character, extended, was_down, |
| std::move(callback)); |
| if (!sent_any_events) { |
| FlutterKeyEvent empty_event{ |
| .struct_size = sizeof(FlutterKeyEvent), |
| .timestamp = static_cast<double>( |
| std::chrono::duration_cast<std::chrono::microseconds>( |
| std::chrono::high_resolution_clock::now().time_since_epoch()) |
| .count()), |
| .type = kFlutterKeyEventTypeDown, |
| .physical = 0, |
| .logical = 0, |
| .character = empty_character, |
| .synthesized = false, |
| }; |
| SendEvent(empty_event, nullptr, nullptr); |
| } |
| } |
| |
| void KeyboardKeyEmbedderHandler::UpdateLastSeenCritialKey( |
| int virtual_key, |
| uint64_t physical_key, |
| uint64_t logical_key) { |
| auto found = critical_keys_.find(virtual_key); |
| if (found != critical_keys_.end()) { |
| found->second.physical_key = physical_key; |
| found->second.logical_key = logical_key; |
| } |
| } |
| |
| void KeyboardKeyEmbedderHandler::SynchronizeCritialToggledStates( |
| int event_virtual_key, |
| bool is_event_down) { |
| // NowState ----------------> PreEventState --------------> TrueState |
| // Synchronization Event |
| for (auto& kv : critical_keys_) { |
| UINT virtual_key = kv.first; |
| CriticalKey& key_info = kv.second; |
| if (key_info.physical_key == 0) { |
| // Never seen this key. |
| continue; |
| } |
| assert(key_info.logical_key != 0); |
| |
| // Check toggling state first, because it might alter pressing state. |
| if (key_info.check_toggled) { |
| const bool target_is_pressed = |
| pressingRecords_.find(key_info.physical_key) != |
| pressingRecords_.end(); |
| // The togglable keys observe a 4-phase cycle: |
| // |
| // Phase# 0 1 2 3 |
| // Event Down Up Down Up |
| // Pressed 0 1 0 1 |
| // Toggled 0 1 1 0 |
| const bool true_toggled = get_key_state_(virtual_key) & kStateMaskToggled; |
| bool pre_event_toggled = true_toggled; |
| // Check if the main event's key is the key being checked. If it's the |
| // non-repeat down event, toggle the state. |
| if (virtual_key == event_virtual_key && !target_is_pressed && |
| is_event_down) { |
| pre_event_toggled = !pre_event_toggled; |
| } |
| if (key_info.toggled_on != pre_event_toggled) { |
| // If the key is pressed, release it first. |
| if (target_is_pressed) { |
| SendEvent(SynthesizeSimpleEvent( |
| kFlutterKeyEventTypeUp, key_info.physical_key, |
| key_info.logical_key, empty_character), |
| nullptr, nullptr); |
| } |
| // Synchronizing toggle state always ends with the key being pressed. |
| pressingRecords_[key_info.physical_key] = key_info.logical_key; |
| SendEvent(SynthesizeSimpleEvent(kFlutterKeyEventTypeDown, |
| key_info.physical_key, |
| key_info.logical_key, empty_character), |
| nullptr, nullptr); |
| } |
| key_info.toggled_on = true_toggled; |
| } |
| } |
| } |
| |
| void KeyboardKeyEmbedderHandler::SynchronizeCritialPressedStates( |
| int event_virtual_key, |
| int event_physical_key, |
| bool is_event_down) { |
| // During an incoming event, there might be a synthesized Flutter event for |
| // each key of each pressing goal, followed by an eventual main Flutter |
| // event. |
| // |
| // NowState ----------------> PreEventState --------------> TrueState |
| // Synchronization Event |
| // |
| // The goal of the synchronization algorithm is to derive a pre-event state |
| // that can satisfy the true state (`true_pressed`) after the event, and that |
| // requires as few synthesized events based on the current state |
| // (`now_pressed`) as possible. |
| for (auto& kv : critical_keys_) { |
| UINT virtual_key = kv.first; |
| CriticalKey& key_info = kv.second; |
| if (key_info.physical_key == 0) { |
| // Never seen this key. |
| continue; |
| } |
| assert(key_info.logical_key != 0); |
| if (key_info.check_pressed) { |
| SHORT true_pressed = get_key_state_(virtual_key) & kStateMaskPressed; |
| auto pressing_record_iter = pressingRecords_.find(key_info.physical_key); |
| bool now_pressed = pressing_record_iter != pressingRecords_.end(); |
| bool pre_event_pressed = true_pressed; |
| // Check if the main event is the key being checked to get the correct |
| // target state. |
| if (is_event_down) { |
| // For down events, this key is the event key if they have the same |
| // virtual key, because virtual key represents "functionality." |
| if (virtual_key == event_virtual_key) { |
| pre_event_pressed = false; |
| } |
| } else { |
| // For up events, this key is the event key if they have the same |
| // physical key, because it is necessary to ensure that the physical |
| // key is correctly released. |
| // |
| // In that case, although the previous state should be pressed, don't |
| // synthesize a down event even if it's not. The later code will handle |
| // such cases by skipping abrupt up events. Obviously don't synthesize |
| // up events either. |
| if (event_physical_key == key_info.physical_key) { |
| continue; |
| } |
| } |
| if (now_pressed != pre_event_pressed) { |
| if (now_pressed) { |
| pressingRecords_.erase(pressing_record_iter); |
| } else { |
| pressingRecords_[key_info.physical_key] = key_info.logical_key; |
| } |
| const char* empty_character = ""; |
| SendEvent( |
| SynthesizeSimpleEvent( |
| now_pressed ? kFlutterKeyEventTypeUp : kFlutterKeyEventTypeDown, |
| key_info.physical_key, key_info.logical_key, empty_character), |
| nullptr, nullptr); |
| } |
| } |
| } |
| } |
| |
| void KeyboardKeyEmbedderHandler::HandleResponse(bool handled, void* user_data) { |
| PendingResponse* pending = reinterpret_cast<PendingResponse*>(user_data); |
| auto callback = std::move(pending->callback); |
| callback(handled, pending->response_id); |
| } |
| |
| void KeyboardKeyEmbedderHandler::InitCriticalKeys( |
| MapVirtualKeyToScanCode map_virtual_key_to_scan_code) { |
| auto createCheckedKey = [this, &map_virtual_key_to_scan_code]( |
| UINT virtual_key, bool extended, |
| bool check_pressed, |
| bool check_toggled) -> CriticalKey { |
| UINT scan_code = map_virtual_key_to_scan_code(virtual_key, extended); |
| return CriticalKey{ |
| .physical_key = GetPhysicalKey(scan_code, extended), |
| .logical_key = GetLogicalKey(virtual_key, extended, scan_code), |
| .check_pressed = check_pressed || check_toggled, |
| .check_toggled = check_toggled, |
| .toggled_on = check_toggled |
| ? !!(get_key_state_(virtual_key) & kStateMaskToggled) |
| : false, |
| }; |
| }; |
| |
| // TODO(dkwingsmt): Consider adding more critical keys here. |
| // https://github.com/flutter/flutter/issues/76736 |
| critical_keys_.emplace(VK_LSHIFT, |
| createCheckedKey(VK_LSHIFT, false, true, false)); |
| critical_keys_.emplace(VK_RSHIFT, |
| createCheckedKey(VK_RSHIFT, false, true, false)); |
| critical_keys_.emplace(VK_LCONTROL, |
| createCheckedKey(VK_LCONTROL, false, true, false)); |
| critical_keys_.emplace(VK_RCONTROL, |
| createCheckedKey(VK_RCONTROL, true, true, false)); |
| critical_keys_.emplace(VK_LMENU, |
| createCheckedKey(VK_LMENU, false, true, false)); |
| critical_keys_.emplace(VK_RMENU, |
| createCheckedKey(VK_RMENU, true, true, false)); |
| critical_keys_.emplace(VK_LWIN, createCheckedKey(VK_LWIN, true, true, false)); |
| critical_keys_.emplace(VK_RWIN, createCheckedKey(VK_RWIN, true, true, false)); |
| |
| critical_keys_.emplace(VK_CAPITAL, |
| createCheckedKey(VK_CAPITAL, false, true, true)); |
| critical_keys_.emplace(VK_SCROLL, |
| createCheckedKey(VK_SCROLL, false, true, true)); |
| critical_keys_.emplace(VK_NUMLOCK, |
| createCheckedKey(VK_NUMLOCK, true, true, true)); |
| } |
| |
| void KeyboardKeyEmbedderHandler::ConvertUtf32ToUtf8_(char* out, char32_t ch) { |
| if (ch == 0) { |
| out[0] = '\0'; |
| return; |
| } |
| std::string result = ConvertChar32ToUtf8(ch); |
| strcpy_s(out, kCharacterCacheSize, result.c_str()); |
| } |
| |
| FlutterKeyEvent KeyboardKeyEmbedderHandler::SynthesizeSimpleEvent( |
| FlutterKeyEventType type, |
| uint64_t physical, |
| uint64_t logical, |
| const char* character) { |
| return FlutterKeyEvent{ |
| .struct_size = sizeof(FlutterKeyEvent), |
| .timestamp = static_cast<double>( |
| std::chrono::duration_cast<std::chrono::microseconds>( |
| std::chrono::high_resolution_clock::now().time_since_epoch()) |
| .count()), |
| .type = type, |
| .physical = physical, |
| .logical = logical, |
| .character = character, |
| .synthesized = true, |
| }; |
| } |
| |
| void KeyboardKeyEmbedderHandler::SendEvent(const FlutterKeyEvent& event, |
| FlutterKeyEventCallback callback, |
| void* user_data) { |
| sent_any_events = true; |
| perform_send_event_(event, callback, user_data); |
| } |
| |
| } // namespace flutter |