// Copyright 2026 Dolphin Emulator Project // SPDX-License-Identifier: GPL-2.0-or-later #include "Common/Keyboard.h" #include #include #include #include #include #ifdef __APPLE__ #include #include "Common/ScopeGuard.h" #endif #ifdef HAVE_SDL3 #include #include // Will be overridden by Dolphin's SDL InputBackend u32 Common::KeyboardContext::s_sdl_init_event_type(-1); u32 Common::KeyboardContext::s_sdl_update_event_type(-1); u32 Common::KeyboardContext::s_sdl_quit_event_type(-1); #endif #include "Core/Config/MainSettings.h" #include "Core/Config/SYSCONFSettings.h" #include "DiscIO/Enums.h" namespace { // Translate HID usage ID based on the host and the game keyboard layout: // - we need to take into account the host layout as we receive raw scan codes // - we need to consider the game layout as it might be different from the host one u8 TranslateUsageID(u8 usage_id, int host_layout, int game_layout) { if (host_layout == game_layout) return usage_id; // Currently, the translation is partial (i.e. alpha only) if (usage_id != Common::HIDUsageID::M_AZERTY && (usage_id < Common::HIDUsageID::A || usage_id > Common::HIDUsageID::Z)) { return usage_id; } switch (host_layout | game_layout) { case Common::KeyboardLayout::AZERTY_QWERTZ: { static const std::map TO_QWERTZ{ {Common::HIDUsageID::A_AZERTY, Common::HIDUsageID::A}, {Common::HIDUsageID::Z_AZERTY, Common::HIDUsageID::Z_QWERTZ}, {Common::HIDUsageID::Y, Common::HIDUsageID::Y_QWERTZ}, {Common::HIDUsageID::Q_AZERTY, Common::HIDUsageID::Q}, {Common::HIDUsageID::M_AZERTY, Common::HIDUsageID::M}, {Common::HIDUsageID::W_AZERTY, Common::HIDUsageID::W}, {Common::HIDUsageID::M, Common::HIDUsageID::M_AZERTY}, }; static const std::map TO_AZERTY{ {Common::HIDUsageID::Q, Common::HIDUsageID::Q_AZERTY}, {Common::HIDUsageID::W, Common::HIDUsageID::W_AZERTY}, {Common::HIDUsageID::Z_QWERTZ, Common::HIDUsageID::Z_AZERTY}, {Common::HIDUsageID::A, Common::HIDUsageID::A_AZERTY}, {Common::HIDUsageID::M_AZERTY, Common::HIDUsageID::M}, {Common::HIDUsageID::Y_QWERTZ, Common::HIDUsageID::Y}, {Common::HIDUsageID::M, Common::HIDUsageID::M_AZERTY}, }; const auto& map = game_layout == Common::KeyboardLayout::QWERTZ ? TO_QWERTZ : TO_AZERTY; if (const auto it{map.find(usage_id)}; it != map.end()) return it->second; break; } case Common::KeyboardLayout::QWERTY_AZERTY: { static constexpr std::array, 3> BI_MAP{ {{Common::HIDUsageID::Q, Common::HIDUsageID::A}, {Common::HIDUsageID::W, Common::HIDUsageID::Z}, {Common::HIDUsageID::M, Common::HIDUsageID::M_AZERTY}}}; for (const auto& [a, b] : BI_MAP) { if (usage_id == a) return b; else if (usage_id == b) return a; } break; } case Common::KeyboardLayout::QWERTY_QWERTZ: { if (usage_id == Common::HIDUsageID::Y) return Common::HIDUsageID::Z; else if (usage_id == Common::HIDUsageID::Z) return Common::HIDUsageID::Y; break; } default: // Shouldn't happen break; } return usage_id; } int GetHostLayout() { const int layout = Config::Get(Config::MAIN_WII_KEYBOARD_HOST_LAYOUT); if (layout != Common::KeyboardLayout::AUTO) return layout; #ifdef __APPLE__ auto current_layout = TISCopyCurrentKeyboardLayoutInputSource(); Common::ScopeGuard guard([¤t_layout] { CFRelease(current_layout); }); auto layout_data = static_cast( TISGetInputSourceProperty(current_layout, kTISPropertyUnicodeKeyLayoutData)); if (!layout_data) return Common::KeyboardLayout::QWERTY; auto unicode_layout = reinterpret_cast(CFDataGetBytePtr(layout_data)); using VkToLayout = std::tuple; for (auto& [key_code, c, l] : std::array{VkToLayout(kVK_ANSI_Y, 'z', Common::KeyboardLayout::QWERTZ), VkToLayout(kVK_ANSI_Q, 'a', Common::KeyboardLayout::AZERTY)}) { UInt32 dead_key_state = 0; std::array str{}; UniCharCount real_length = 0; UCKeyTranslate(unicode_layout, key_code, kUCKeyActionDown, 0, LMGetKbdType(), kUCKeyTranslateNoDeadKeysMask, &dead_key_state, str.size(), &real_length, str.data()); if (real_length && (str[0] == c || str[0] == (c - 0x20))) return l; } #elif defined(HAVE_SDL3) if (const SDL_Keycode key_code = SDL_GetKeyFromScancode(SDL_SCANCODE_Y, SDL_KMOD_NONE, false); key_code == SDLK_Z) { return Common::KeyboardLayout::QWERTZ; } if (const SDL_Keycode key_code = SDL_GetKeyFromScancode(SDL_SCANCODE_Q, SDL_KMOD_NONE, false); key_code == SDLK_A) { return Common::KeyboardLayout::AZERTY; } #endif return Common::KeyboardLayout::QWERTY; } int GetGameLayout() { const int layout = Config::Get(Config::MAIN_WII_KEYBOARD_GAME_LAYOUT); if (layout != Common::KeyboardLayout::AUTO) return layout; const DiscIO::Language language = static_cast(Config::Get(Config::SYSCONF_LANGUAGE)); switch (language) { case DiscIO::Language::French: return Common::KeyboardLayout::AZERTY; case DiscIO::Language::German: return Common::KeyboardLayout::QWERTZ; default: return Common::KeyboardLayout::QWERTY; } } u8 MapVirtualKeyToHID(u8 virtual_key, int host_layout, int game_layout) { #ifdef __APPLE__ static const std::map VK_TO_HID{{kVK_ANSI_A, 0x04}, {kVK_ANSI_B, 0x05}, {kVK_ANSI_C, 0x06}, {kVK_ANSI_D, 0x07}, {kVK_ANSI_E, 0x08}, {kVK_ANSI_F, 0x09}, {kVK_ANSI_G, 0x0a}, {kVK_ANSI_H, 0x0b}, {kVK_ANSI_I, 0x0c}, {kVK_ANSI_J, 0x0d}, {kVK_ANSI_K, 0x0e}, {kVK_ANSI_L, 0x0f}, {kVK_ANSI_M, 0x10}, {kVK_ANSI_N, 0x11}, {kVK_ANSI_O, 0x12}, {kVK_ANSI_P, 0x13}, {kVK_ANSI_Q, 0x14}, {kVK_ANSI_R, 0x15}, {kVK_ANSI_S, 0x16}, {kVK_ANSI_T, 0x17}, {kVK_ANSI_U, 0x18}, {kVK_ANSI_V, 0x19}, {kVK_ANSI_W, 0x1a}, {kVK_ANSI_X, 0x1b}, {kVK_ANSI_Y, 0x1c}, {kVK_ANSI_Z, 0x1d}, {kVK_ANSI_1, 0x1e}, {kVK_ANSI_2, 0x1f}, {kVK_ANSI_3, 0x20}, {kVK_ANSI_4, 0x21}, {kVK_ANSI_5, 0x22}, {kVK_ANSI_6, 0x23}, {kVK_ANSI_7, 0x24}, {kVK_ANSI_8, 0x25}, {kVK_ANSI_9, 0x26}, {kVK_ANSI_0, 0x27}, {kVK_Return, 0x28}, {kVK_Escape, 0x29}, {kVK_Delete, 0x2a}, {kVK_Tab, 0x2b}, {kVK_Space, 0x2c}, {kVK_ANSI_Minus, 0x2d}, {kVK_ANSI_Equal, 0x2e}, {kVK_ANSI_LeftBracket, 0x2f}, {kVK_ANSI_RightBracket, 0x30}, {kVK_ANSI_Backslash, 0x31}, // Missing: Non-US # {kVK_ANSI_Semicolon, 0x33}, {kVK_ANSI_Quote, 0x34}, {kVK_ANSI_Grave, 0x35}, {kVK_ANSI_Comma, 0x36}, {kVK_ANSI_Period, 0x37}, {kVK_ANSI_Slash, 0x38}, {kVK_CapsLock, 0x39}, {kVK_F1, 0x3a}, {kVK_F2, 0x3b}, {kVK_F3, 0x3c}, {kVK_F4, 0x3d}, {kVK_F5, 0x3e}, {kVK_F6, 0x3f}, {kVK_F7, 0x40}, {kVK_F8, 0x41}, {kVK_F9, 0x42}, {kVK_F10, 0x43}, {kVK_F11, 0x44}, {kVK_F12, 0x45}, // Missing: PrintScreen, Scroll Lock, Pause {kVK_Help, 0x49}, {kVK_Home, 0x4a}, {kVK_PageUp, 0x4b}, {kVK_ForwardDelete, 0x4c}, {kVK_End, 0x4d}, {kVK_PageDown, 0x4e}, {kVK_RightArrow, 0x4f}, {kVK_LeftArrow, 0x50}, {kVK_DownArrow, 0x51}, {kVK_UpArrow, 0x52}, {kVK_ANSI_KeypadClear, 0x53}, {kVK_ANSI_KeypadDivide, 0x54}, {kVK_ANSI_KeypadMultiply, 0x55}, {kVK_ANSI_KeypadMinus, 0x56}, {kVK_ANSI_KeypadPlus, 0x57}, {kVK_ANSI_KeypadEnter, 0x58}, {kVK_ANSI_Keypad1, 0x59}, {kVK_ANSI_Keypad2, 0x5a}, {kVK_ANSI_Keypad3, 0x5b}, {kVK_ANSI_Keypad4, 0x5c}, {kVK_ANSI_Keypad5, 0x5d}, {kVK_ANSI_Keypad6, 0x5e}, {kVK_ANSI_Keypad7, 0x5f}, {kVK_ANSI_Keypad8, 0x60}, {kVK_ANSI_Keypad9, 0x61}, {kVK_ANSI_Keypad0, 0x62}, {kVK_ANSI_KeypadDecimal, 0x63}, // Missing: Non-US |, Application, Power {kVK_ANSI_KeypadEquals, 0x67}, {kVK_Control, 0xe0}, {kVK_Shift, 0xe1}, {kVK_Option, 0xe2}, {kVK_Command, 0xe3}, {kVK_RightControl, 0xe4}, {kVK_RightShift, 0xe5}, {kVK_RightOption, 0xe6}, {kVK_RightCommand, 0xe7}}; const auto it{VK_TO_HID.find(virtual_key)}; u8 usage_id = (it == VK_TO_HID.end()) ? 0 : it->second; #else // SDL3 keyboard state uses scan codes already based on HID usage id u8 usage_id = virtual_key; #endif if (Config::Get(Config::MAIN_WII_KEYBOARD_TRANSLATION)) usage_id = TranslateUsageID(usage_id, host_layout, game_layout); return usage_id; } std::weak_ptr s_keyboard_context; std::mutex s_keyboard_context_mutex; // Will be updated by DolphinQt's Host: // - SetRenderHandle // - SetFullscreen Common::KeyboardContext::HandlerState s_handler_state{}; } // Anonymous namespace namespace Common { KeyboardContext::KeyboardContext() { if (Config::Get(Config::MAIN_WII_KEYBOARD)) Init(); } KeyboardContext::~KeyboardContext() { if (Config::Get(Config::MAIN_WII_KEYBOARD)) Quit(); } void KeyboardContext::Init() { #ifdef __APPLE__ CFNotificationCenterAddObserver( CFNotificationCenterGetDistributedCenter(), this, [](CFNotificationCenterRef /* center */, void* /* observer */, CFStringRef /* name */, const void* /* object */, CFDictionaryRef /* userInfo */) { KeyboardContext::UpdateLayout(); }, kTISNotifySelectedKeyboardInputSourceChanged, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately); #elif defined(HAVE_SDL3) SDL_Event event{s_sdl_init_event_type}; SDL_PushEvent(&event); m_keyboard_state = SDL_GetKeyboardState(nullptr); #endif UpdateLayout(); m_is_ready = true; } void KeyboardContext::Quit() { m_is_ready = false; #ifdef __APPLE__ CFNotificationCenterRemoveEveryObserver(CFNotificationCenterGetDistributedCenter(), this); #elif defined(HAVE_SDL3) SDL_Event event{s_sdl_quit_event_type}; SDL_PushEvent(&event); #endif } void* KeyboardContext::HandlerState::GetHandle() const { #ifdef _WIN32 if (is_rendering_to_main && !is_fullscreen) return main_handle; #endif return renderer_handle; } void KeyboardContext::NotifyInit() { if (auto self = s_keyboard_context.lock()) self->Init(); } void KeyboardContext::NotifyHandlerChanged(const KeyboardContext::HandlerState& state) { s_handler_state = state; if (s_keyboard_context.expired()) return; #ifdef __APPLE__ // Quartz doesn't seem impacted #elif defined(HAVE_SDL3) SDL_Event event{s_sdl_update_event_type}; SDL_PushEvent(&event); #endif } void KeyboardContext::NotifyQuit() { if (auto self = s_keyboard_context.lock()) self->Quit(); } void KeyboardContext::UpdateLayout() { if (auto self = s_keyboard_context.lock()) { self->m_host_layout = GetHostLayout(); self->m_game_layout = GetGameLayout(); } } void* KeyboardContext::GetWindowHandle() { return s_handler_state.GetHandle(); } std::shared_ptr KeyboardContext::GetInstance() { const std::lock_guard guard(s_keyboard_context_mutex); std::shared_ptr ptr = s_keyboard_context.lock(); if (!ptr) { ptr = std::shared_ptr(new KeyboardContext); s_keyboard_context = ptr; } return ptr; } HIDPressedState KeyboardContext::GetPressedState() const { return m_is_ready ? HIDPressedState{.modifiers = PollHIDModifiers(), .pressed_keys = PollHIDPressedKeys()} : HIDPressedState{}; } bool KeyboardContext::IsVirtualKeyPressed(int virtual_key) const { #ifdef __APPLE__ return CGEventSourceKeyState(kCGEventSourceStateHIDSystemState, CGKeyCode(virtual_key)); #elif defined(HAVE_SDL3) if (virtual_key >= SDL_SCANCODE_COUNT) return false; return m_keyboard_state[virtual_key]; #else // TODO: Android implementation return false; #endif } u8 KeyboardContext::PollHIDModifiers() const { u8 modifiers = 0; using VkHidPair = std::pair; // References: // https://stackoverflow.com/a/16125341 // https://wiki.libsdl.org/SDL3/SDL_Scancode // https://www.usb.org/document-library/device-class-definition-hid-111 // // HID modifiers: // Bit 0 - LEFT CTRL // Bit 1 - LEFT SHIFT // Bit 2 - LEFT ALT // Bit 3 - LEFT GUI // Bit 4 - RIGHT CTRL // Bit 5 - RIGHT SHIFT // Bit 6 - RIGHT ALT // Bit 7 - RIGHT GUI static const std::vector MODIFIERS_MAP{ #ifdef __APPLE__ {kVK_Control, 0x01}, {kVK_Shift, 0x02}, {kVK_Option, 0x04}, {kVK_Command, 0x08}, {kVK_RightControl, 0x10}, {kVK_RightShift, 0x20}, {kVK_RightOption, 0x40}, {kVK_RightCommand, 0x80} #elif defined(HAVE_SDL3) {SDL_SCANCODE_LCTRL, 0x01}, {SDL_SCANCODE_LSHIFT, 0x02}, {SDL_SCANCODE_LALT, 0x04}, {SDL_SCANCODE_LGUI, 0x08}, {SDL_SCANCODE_RCTRL, 0x10}, {SDL_SCANCODE_RSHIFT, 0x20}, {SDL_SCANCODE_RALT, 0x40}, {SDL_SCANCODE_RGUI, 0x80} #else // TODO: Android implementation #endif }; for (const auto& [virtual_key, hid_modifier] : MODIFIERS_MAP) { if (IsVirtualKeyPressed(virtual_key)) modifiers |= hid_modifier; } return modifiers; } HIDPressedKeys KeyboardContext::PollHIDPressedKeys() const { HIDPressedKeys pressed_keys{}; auto it = pressed_keys.begin(); #ifdef __APPLE__ // See comment about keycodes in QuartzKeyboardAndMouse.mm static auto KEYCODES = std::ranges::filter_view(std::ranges::iota_view{0, 0x7f}, [](int keycode) { switch (keycode) { case kVK_Command: case kVK_RightCommand: case kVK_Shift: case kVK_RightShift: case kVK_Option: case kVK_RightOption: case kVK_Control: case kVK_RightControl: return false; default: return true; } }); static const std::vector VIRTUAL_KEYS{KEYCODES.begin(), KEYCODES.end()}; #elif defined(HAVE_SDL3) static constexpr std::ranges::iota_view SCANCODES{int(SDL_SCANCODE_A), int(SDL_SCANCODE_LCTRL)}; static const std::vector VIRTUAL_KEYS{SCANCODES.begin(), SCANCODES.end()}; #else static constexpr std::vector VIRTUAL_KEYS{}; #endif for (int virtual_key : VIRTUAL_KEYS) { if (!IsVirtualKeyPressed(virtual_key)) continue; *it = MapVirtualKeyToHID(static_cast(virtual_key), m_host_layout, m_game_layout); if (++it == pressed_keys.end()) break; } return pressed_keys; } } // namespace Common