| // Copyright (c) 2016 The Chromium 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 "ui/events/keycodes/platform_key_map_win.h" |
| |
| #include <algorithm> |
| #include <utility> |
| |
| #include "base/feature_list.h" |
| #include "base/lazy_instance.h" |
| #include "base/logging.h" |
| #include "base/memory/ptr_util.h" |
| #include "base/stl_util.h" |
| #include "base/threading/thread_local.h" |
| |
| #include "ui/events/event_constants.h" |
| #include "ui/events/event_utils.h" |
| #include "ui/events/keycodes/dom/keycode_converter.h" |
| #include "ui/events/keycodes/keyboard_code_conversion.h" |
| |
| namespace ui { |
| |
| namespace { |
| |
| // List of modifiers mentioned in https://w3c.github.io/uievents/#keys-modifiers |
| // Some modifiers are commented out because they usually don't change keys. |
| const EventFlags modifier_flags[] = { |
| EF_SHIFT_DOWN, EF_CONTROL_DOWN, EF_ALT_DOWN, |
| // EF_COMMAND_DOWN, |
| // EF_NUM_LOCK_ON, |
| EF_CAPS_LOCK_ON, |
| // EF_SCROLL_LOCK_ON, |
| |
| // Simulated as Control+Alt. |
| // EF_ALTGR_DOWN, |
| }; |
| |
| void SetModifierState(BYTE* keyboard_state, int flags) { |
| // According to MSDN GetKeyState(): |
| // 1. If the high-order bit is 1, the key is down; otherwise, it is up. |
| // 2. If the low-order bit is 1, the key is toggled. A key, such as the |
| // CAPS LOCK key, is toggled if it is turned on. The key is off and |
| // untoggled if the low-order bit is 0. |
| // See https://msdn.microsoft.com/en-us/library/windows/desktop/ms646301.aspx |
| if (flags & EF_SHIFT_DOWN) |
| keyboard_state[VK_SHIFT] |= 0x80; |
| |
| if (flags & EF_CONTROL_DOWN) |
| keyboard_state[VK_CONTROL] |= 0x80; |
| |
| if (flags & EF_ALT_DOWN) |
| keyboard_state[VK_MENU] |= 0x80; |
| |
| if (flags & EF_CAPS_LOCK_ON) |
| keyboard_state[VK_CAPITAL] |= 0x01; |
| |
| DCHECK_EQ(flags & ~(EF_SHIFT_DOWN | EF_CONTROL_DOWN | EF_ALT_DOWN | |
| EF_CAPS_LOCK_ON), |
| 0); |
| } |
| |
| constexpr int kControlAndAltFlags = EF_CONTROL_DOWN | EF_ALT_DOWN; |
| |
| bool HasControlAndAlt(int flags) { |
| return (flags & kControlAndAltFlags) == kControlAndAltFlags; |
| } |
| |
| int ReplaceAltGraphWithControlAndAlt(int flags) { |
| return (flags & EF_ALTGR_DOWN) |
| ? ((flags & ~EF_ALTGR_DOWN) | kControlAndAltFlags) |
| : flags; |
| } |
| |
| const int kModifierFlagsCombinations = (1 << base::size(modifier_flags)) - 1; |
| |
| int GetModifierFlags(int combination) { |
| int flags = EF_NONE; |
| for (size_t i = 0; i < base::size(modifier_flags); ++i) { |
| if (combination & (1 << i)) |
| flags |= modifier_flags[i]; |
| } |
| return flags; |
| } |
| |
| // This table must be sorted by |key_code| for binary search. |
| const struct NonPrintableKeyEntry { |
| KeyboardCode key_code; |
| DomKey dom_key; |
| } kNonPrintableKeyMap[] = { |
| {VKEY_CANCEL, DomKey::CANCEL}, |
| {VKEY_BACK, DomKey::BACKSPACE}, |
| {VKEY_TAB, DomKey::TAB}, |
| {VKEY_CLEAR, DomKey::CLEAR}, |
| {VKEY_RETURN, DomKey::ENTER}, |
| {VKEY_SHIFT, DomKey::SHIFT}, |
| {VKEY_CONTROL, DomKey::CONTROL}, |
| {VKEY_MENU, DomKey::ALT}, |
| {VKEY_PAUSE, DomKey::PAUSE}, |
| {VKEY_CAPITAL, DomKey::CAPS_LOCK}, |
| // VKEY_KANA == VKEY_HANGUL |
| {VKEY_JUNJA, DomKey::JUNJA_MODE}, |
| {VKEY_FINAL, DomKey::FINAL_MODE}, |
| // VKEY_HANJA == VKEY_KANJI |
| {VKEY_ESCAPE, DomKey::ESCAPE}, |
| {VKEY_CONVERT, DomKey::CONVERT}, |
| {VKEY_NONCONVERT, DomKey::NON_CONVERT}, |
| {VKEY_ACCEPT, DomKey::ACCEPT}, |
| {VKEY_MODECHANGE, DomKey::MODE_CHANGE}, |
| // VKEY_SPACE |
| {VKEY_PRIOR, DomKey::PAGE_UP}, |
| {VKEY_NEXT, DomKey::PAGE_DOWN}, |
| {VKEY_END, DomKey::END}, |
| {VKEY_HOME, DomKey::HOME}, |
| {VKEY_LEFT, DomKey::ARROW_LEFT}, |
| {VKEY_UP, DomKey::ARROW_UP}, |
| {VKEY_RIGHT, DomKey::ARROW_RIGHT}, |
| {VKEY_DOWN, DomKey::ARROW_DOWN}, |
| {VKEY_SELECT, DomKey::SELECT}, |
| {VKEY_PRINT, DomKey::PRINT}, |
| {VKEY_EXECUTE, DomKey::EXECUTE}, |
| {VKEY_SNAPSHOT, DomKey::PRINT_SCREEN}, |
| {VKEY_INSERT, DomKey::INSERT}, |
| {VKEY_DELETE, DomKey::DEL}, |
| {VKEY_HELP, DomKey::HELP}, |
| // VKEY_0..9 |
| // VKEY_A..Z |
| {VKEY_LWIN, DomKey::META}, |
| // VKEY_COMMAND == VKEY_LWIN |
| {VKEY_RWIN, DomKey::META}, |
| {VKEY_APPS, DomKey::CONTEXT_MENU}, |
| {VKEY_SLEEP, DomKey::STANDBY}, |
| // VKEY_NUMPAD0..9 |
| // VKEY_MULTIPLY, VKEY_ADD, VKEY_SEPARATOR, VKEY_SUBTRACT, VKEY_DECIMAL, |
| // VKEY_DIVIDE |
| {VKEY_F1, DomKey::F1}, |
| {VKEY_F2, DomKey::F2}, |
| {VKEY_F3, DomKey::F3}, |
| {VKEY_F4, DomKey::F4}, |
| {VKEY_F5, DomKey::F5}, |
| {VKEY_F6, DomKey::F6}, |
| {VKEY_F7, DomKey::F7}, |
| {VKEY_F8, DomKey::F8}, |
| {VKEY_F9, DomKey::F9}, |
| {VKEY_F10, DomKey::F10}, |
| {VKEY_F11, DomKey::F11}, |
| {VKEY_F12, DomKey::F12}, |
| {VKEY_F13, DomKey::F13}, |
| {VKEY_F14, DomKey::F14}, |
| {VKEY_F15, DomKey::F15}, |
| {VKEY_F16, DomKey::F16}, |
| {VKEY_F17, DomKey::F17}, |
| {VKEY_F18, DomKey::F18}, |
| {VKEY_F19, DomKey::F19}, |
| {VKEY_F20, DomKey::F20}, |
| {VKEY_F21, DomKey::F21}, |
| {VKEY_F22, DomKey::F22}, |
| {VKEY_F23, DomKey::F23}, |
| {VKEY_F24, DomKey::F24}, |
| {VKEY_NUMLOCK, DomKey::NUM_LOCK}, |
| {VKEY_SCROLL, DomKey::SCROLL_LOCK}, |
| {VKEY_LSHIFT, DomKey::SHIFT}, |
| {VKEY_RSHIFT, DomKey::SHIFT}, |
| {VKEY_LCONTROL, DomKey::CONTROL}, |
| {VKEY_RCONTROL, DomKey::CONTROL}, |
| {VKEY_LMENU, DomKey::ALT}, |
| {VKEY_RMENU, DomKey::ALT}, |
| {VKEY_BROWSER_BACK, DomKey::BROWSER_BACK}, |
| {VKEY_BROWSER_FORWARD, DomKey::BROWSER_FORWARD}, |
| {VKEY_BROWSER_REFRESH, DomKey::BROWSER_REFRESH}, |
| {VKEY_BROWSER_STOP, DomKey::BROWSER_STOP}, |
| {VKEY_BROWSER_SEARCH, DomKey::BROWSER_SEARCH}, |
| {VKEY_BROWSER_FAVORITES, DomKey::BROWSER_FAVORITES}, |
| {VKEY_BROWSER_HOME, DomKey::BROWSER_HOME}, |
| {VKEY_VOLUME_MUTE, DomKey::AUDIO_VOLUME_MUTE}, |
| {VKEY_VOLUME_DOWN, DomKey::AUDIO_VOLUME_DOWN}, |
| {VKEY_VOLUME_UP, DomKey::AUDIO_VOLUME_UP}, |
| {VKEY_MEDIA_NEXT_TRACK, DomKey::MEDIA_TRACK_NEXT}, |
| {VKEY_MEDIA_PREV_TRACK, DomKey::MEDIA_TRACK_PREVIOUS}, |
| {VKEY_MEDIA_STOP, DomKey::MEDIA_STOP}, |
| {VKEY_MEDIA_PLAY_PAUSE, DomKey::MEDIA_PLAY_PAUSE}, |
| {VKEY_MEDIA_LAUNCH_MAIL, DomKey::LAUNCH_MAIL}, |
| {VKEY_MEDIA_LAUNCH_MEDIA_SELECT, DomKey::LAUNCH_MEDIA_PLAYER}, |
| {VKEY_MEDIA_LAUNCH_APP1, DomKey::LAUNCH_MY_COMPUTER}, |
| {VKEY_MEDIA_LAUNCH_APP2, DomKey::LAUNCH_CALCULATOR}, |
| // VKEY_OEM_1..8, 102, PLUS, COMMA, MINUS, PERIOD |
| {VKEY_ALTGR, DomKey::ALT_GRAPH}, |
| {VKEY_PROCESSKEY, DomKey::PROCESS}, |
| // VKEY_PACKET - Used to pass Unicode char, considered as printable key. |
| {VKEY_ATTN, DomKey::ATTN}, |
| {VKEY_CRSEL, DomKey::CR_SEL}, |
| {VKEY_EXSEL, DomKey::EX_SEL}, |
| {VKEY_EREOF, DomKey::ERASE_EOF}, |
| {VKEY_PLAY, DomKey::PLAY}, |
| {VKEY_ZOOM, DomKey::ZOOM_TOGGLE}, |
| // TODO(input-dev): Handle VKEY_NONAME, VKEY_PA1. |
| // https://crbug.com/616910 |
| {VKEY_OEM_CLEAR, DomKey::CLEAR}, |
| }; |
| |
| // Disambiguates the meaning of certain non-printable keys which have different |
| // meanings under different languages, but use the same VKEY code. |
| DomKey LanguageSpecificOemKeyboardCodeToDomKey(KeyboardCode key_code, |
| HKL layout) { |
| WORD language = LOWORD(layout); |
| WORD primary_language = PRIMARYLANGID(language); |
| if (primary_language == LANG_KOREAN) { |
| switch (key_code) { |
| case VKEY_HANGUL: |
| return DomKey::HANGUL_MODE; |
| case VKEY_HANJA: |
| return DomKey::HANJA_MODE; |
| default: |
| return DomKey::NONE; |
| } |
| } else if (primary_language == LANG_JAPANESE) { |
| switch (key_code) { |
| // VKEY_KANA isn't generated by any modern layouts but is a listed value |
| // that third-party apps might synthesize, so we handle it anyway. |
| case VKEY_KANA: |
| case VKEY_ATTN: |
| return DomKey::KANA_MODE; |
| case VKEY_KANJI: |
| return DomKey::KANJI_MODE; |
| case VKEY_OEM_ATTN: |
| return DomKey::ALPHANUMERIC; |
| case VKEY_OEM_FINISH: |
| return DomKey::KATAKANA; |
| case VKEY_OEM_COPY: |
| return DomKey::HIRAGANA; |
| case VKEY_DBE_SBCSCHAR: |
| return DomKey::HANKAKU; |
| case VKEY_DBE_DBCSCHAR: |
| return DomKey::ZENKAKU; |
| case VKEY_OEM_BACKTAB: |
| return DomKey::ROMAJI; |
| default: |
| return DomKey::NONE; |
| } |
| } |
| return DomKey::NONE; |
| } |
| |
| DomKey NonPrintableKeyboardCodeToDomKey(KeyboardCode key_code, HKL layout) { |
| // 1. Check if |key_code| has a |layout|-specific meaning. |
| const DomKey key = LanguageSpecificOemKeyboardCodeToDomKey(key_code, layout); |
| if (key != DomKey::NONE) |
| return key; |
| |
| // 2. Most |key_codes| have the same meaning regardless of |layout|. |
| const NonPrintableKeyEntry* result = std::lower_bound( |
| std::begin(kNonPrintableKeyMap), std::end(kNonPrintableKeyMap), key_code, |
| [](const NonPrintableKeyEntry& entry, KeyboardCode needle) { |
| return entry.key_code < needle; |
| }); |
| if (result != std::end(kNonPrintableKeyMap) && result->key_code == key_code) |
| return result->dom_key; |
| |
| return DomKey::NONE; |
| } |
| |
| } // anonymous namespace |
| |
| PlatformKeyMap::PlatformKeyMap() {} |
| |
| PlatformKeyMap::PlatformKeyMap(HKL layout) { |
| UpdateLayout(layout); |
| } |
| |
| PlatformKeyMap::~PlatformKeyMap() {} |
| |
| // static |
| PlatformKeyMap* PlatformKeyMap::GetThreadLocalPlatformKeyMap() { |
| // DestructorAtExit so the main thread's instance is cleaned up between tests |
| // in the same process. |
| static base::LazyInstance<base::ThreadLocalOwnedPointer<PlatformKeyMap>>:: |
| DestructorAtExit platform_key_map_tls_instance = |
| LAZY_INSTANCE_INITIALIZER; |
| |
| auto& platform_key_map_tls = platform_key_map_tls_instance.Get(); |
| PlatformKeyMap* platform_key_map = platform_key_map_tls.Get(); |
| if (!platform_key_map) { |
| auto new_platform_key_map = base::WrapUnique(new PlatformKeyMap); |
| platform_key_map = new_platform_key_map.get(); |
| platform_key_map_tls.Set(std::move(new_platform_key_map)); |
| } |
| |
| return platform_key_map; |
| } |
| |
| DomKey PlatformKeyMap::DomKeyFromKeyboardCodeImpl(KeyboardCode key_code, |
| int* flags) const { |
| // Windows expresses right-Alt as VKEY_MENU with the extended flag set. |
| // This key should generate AltGraph under layouts which use that modifier. |
| if (key_code == VKEY_MENU && has_alt_graph_ && (*flags & EF_IS_EXTENDED_KEY)) |
| return DomKey::ALT_GRAPH; |
| |
| DomKey key = NonPrintableKeyboardCodeToDomKey(key_code, keyboard_layout_); |
| if (key != DomKey::NONE) |
| return key; |
| |
| // AltGraph is expressed as Control & Alt modifiers in the lookup below. |
| const int lookup_flags = ReplaceAltGraphWithControlAndAlt(*flags); |
| const int flags_to_try[] = { |
| // Trying to match Firefox's behavior and UIEvents DomKey guidelines. |
| // If the combination doesn't produce a printable character, the key value |
| // should be the key with no modifiers except for Shift and AltGr. |
| // See https://w3c.github.io/uievents/#keys-guidelines |
| lookup_flags, lookup_flags & (EF_SHIFT_DOWN | EF_CAPS_LOCK_ON), EF_NONE, |
| }; |
| |
| for (auto try_flags : flags_to_try) { |
| const auto& it = printable_keycode_to_key_.find( |
| std::make_pair(static_cast<int>(key_code), try_flags)); |
| if (it != printable_keycode_to_key_.end()) { |
| key = it->second; |
| if (key != DomKey::NONE) { |
| // If we find a character with |try_flags| including Control and Alt |
| // then this is an AltGraph-shifted event. |
| if (HasControlAndAlt(try_flags)) |
| *flags = ReplaceControlAndAltWithAltGraph(*flags); |
| return key; |
| } |
| } |
| |
| // If we found nothing with no flags set, there is nothing left to try. |
| if (try_flags == EF_NONE) |
| break; |
| } |
| |
| // Return DomKey::UNIDENTIFIED to prevent US layout fall-back. |
| return DomKey::UNIDENTIFIED; |
| } |
| |
| // static |
| DomKey PlatformKeyMap::DomKeyFromKeyboardCode(KeyboardCode key_code, |
| int* flags) { |
| // Use TLS because KeyboardLayout is per thread. |
| // However currently PlatformKeyMap will only be used by the host application, |
| // which is just one process and one thread. |
| PlatformKeyMap* platform_key_map = GetThreadLocalPlatformKeyMap(); |
| |
| HKL current_layout = ::GetKeyboardLayout(0); |
| platform_key_map->UpdateLayout(current_layout); |
| return platform_key_map->DomKeyFromKeyboardCodeImpl(key_code, flags); |
| } |
| |
| // static |
| int PlatformKeyMap::ReplaceControlAndAltWithAltGraph(int flags) { |
| if (!HasControlAndAlt(flags)) |
| return flags; |
| return (flags & ~kControlAndAltFlags) | EF_ALTGR_DOWN; |
| } |
| |
| // static |
| bool PlatformKeyMap::UsesAltGraph() { |
| PlatformKeyMap* platform_key_map = GetThreadLocalPlatformKeyMap(); |
| |
| HKL current_layout = ::GetKeyboardLayout(0); |
| platform_key_map->UpdateLayout(current_layout); |
| return platform_key_map->has_alt_graph_; |
| } |
| |
| void PlatformKeyMap::UpdateLayout(HKL layout) { |
| if (layout == keyboard_layout_) |
| return; |
| |
| BYTE keyboard_state_to_restore[256]; |
| if (!::GetKeyboardState(keyboard_state_to_restore)) |
| return; |
| |
| // TODO(input-dev): Optimize layout switching (see crbug.com/587147). |
| keyboard_layout_ = layout; |
| printable_keycode_to_key_.clear(); |
| has_alt_graph_ = false; |
| |
| // Map size for some sample keyboard layouts: |
| // US: 476, French: 602, Persian: 482, Vietnamese: 1436 |
| printable_keycode_to_key_.reserve(1500); |
| |
| for (int modifier_combination = 0; |
| modifier_combination <= kModifierFlagsCombinations; |
| ++modifier_combination) { |
| BYTE keyboard_state[256]; |
| memset(keyboard_state, 0, sizeof(keyboard_state)); |
| |
| // Setting up keyboard state for modifiers. |
| int flags = GetModifierFlags(modifier_combination); |
| SetModifierState(keyboard_state, flags); |
| |
| for (int key_code = 0; key_code <= 0xFF; ++key_code) { |
| wchar_t translated_chars[5]; |
| int rv = ::ToUnicodeEx(key_code, 0, keyboard_state, translated_chars, |
| base::size(translated_chars), 0, keyboard_layout_); |
| |
| if (rv == -1) { |
| // Dead key, injecting VK_SPACE to get character representation. |
| BYTE empty_state[256]; |
| memset(empty_state, 0, sizeof(empty_state)); |
| rv = ::ToUnicodeEx(VK_SPACE, 0, empty_state, translated_chars, |
| base::size(translated_chars), 0, keyboard_layout_); |
| // Expecting a dead key character (not followed by a space). |
| if (rv == 1) { |
| printable_keycode_to_key_[std::make_pair(static_cast<int>(key_code), |
| flags)] = |
| DomKey::DeadKeyFromCombiningCharacter(translated_chars[0]); |
| } else { |
| // TODO(input-dev): Check if this will actually happen. |
| } |
| } else if (rv == 1) { |
| if (translated_chars[0] >= 0x20) { |
| printable_keycode_to_key_[std::make_pair(static_cast<int>(key_code), |
| flags)] = |
| DomKey::FromCharacter(translated_chars[0]); |
| |
| // Detect whether the layout makes use of AltGraph. |
| if (HasControlAndAlt(flags)) { |
| has_alt_graph_ = true; |
| } |
| } else { |
| // Ignores legacy non-printable control characters. |
| } |
| } else { |
| // TODO(input-dev): Handle rv <= -2 and rv >= 2. |
| } |
| } |
| } |
| ::SetKeyboardState(keyboard_state_to_restore); |
| } |
| |
| } // namespace ui |