diff --git a/Source/Core/Common/CMakeLists.txt b/Source/Core/Common/CMakeLists.txt index ee738ba4e3..e643e3bec9 100644 --- a/Source/Core/Common/CMakeLists.txt +++ b/Source/Core/Common/CMakeLists.txt @@ -95,6 +95,8 @@ add_library(common JitRegister.h JsonUtil.h JsonUtil.cpp + Keyboard.h + Keyboard.cpp Lazy.h LinearDiskCache.h Logging/ConsoleListener.h @@ -352,6 +354,11 @@ if(OPROFILE_FOUND) target_link_libraries(common PRIVATE OProfile::OProfile) endif() +if(ENABLE_SDL) + target_link_libraries(common PRIVATE SDL3::SDL3) + target_compile_definitions(common PRIVATE -DHAVE_SDL3) +endif() + if(ENABLE_LLVM) find_package(LLVM CONFIG) if(LLVM_FOUND) diff --git a/Source/Core/Common/Keyboard.cpp b/Source/Core/Common/Keyboard.cpp new file mode 100644 index 0000000000..85e5c9edd0 --- /dev/null +++ b/Source/Core/Common/Keyboard.cpp @@ -0,0 +1,509 @@ +// Copyright 2025 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 diff --git a/Source/Core/Common/Keyboard.h b/Source/Core/Common/Keyboard.h new file mode 100644 index 0000000000..8a3b6ca08e --- /dev/null +++ b/Source/Core/Common/Keyboard.h @@ -0,0 +1,126 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include +#include + +#include "Common/CommonTypes.h" + +namespace Common +{ +namespace HIDUsageID +{ +// See HID Usage Tables - Keyboard (0x07): +// https://usb.org/sites/default/files/hut1_21.pdf +enum +{ + A = 0x04, + B, + C, + D, + E, + F, + G, + H, + I, + J, + K, + L, + M, + N, + O, + P, + Q, + R, + S, + T, + U, + V, + W, + X, + Y, + Z, + COLON = 0x33, + A_AZERTY = Q, + M_AZERTY = COLON, + Q_AZERTY = A, + W_AZERTY = Z, + Z_AZERTY = W, + Y_QWERTZ = Z, + Z_QWERTZ = Y, +}; +} // namespace HIDUsageID + +namespace KeyboardLayout +{ +enum LayoutEnum +{ + AUTO = 0, + QWERTY = 1, + AZERTY = 2, + QWERTZ = 4, + // Translation + QWERTY_AZERTY = QWERTY | AZERTY, + QWERTY_QWERTZ = QWERTY | QWERTZ, + AZERTY_QWERTZ = AZERTY | QWERTZ +}; +} +using HIDPressedKeys = std::array; + +struct HIDPressedState +{ + u8 modifiers = 0; + HIDPressedKeys pressed_keys{}; + + auto operator<=>(const HIDPressedState&) const = default; +}; + +class KeyboardContext +{ +public: + ~KeyboardContext(); + + struct HandlerState + { + void* main_handle = nullptr; + void* renderer_handle = nullptr; + bool is_fullscreen = false; + bool is_rendering_to_main = false; + + void* GetHandle() const; + }; + + static void NotifyInit(); + static void NotifyHandlerChanged(const HandlerState& state); + static void NotifyQuit(); + static void UpdateLayout(); + static void* GetWindowHandle(); + static std::shared_ptr GetInstance(); + + HIDPressedState GetPressedState() const; + +#ifdef HAVE_SDL3 + static u32 s_sdl_init_event_type; + static u32 s_sdl_update_event_type; + static u32 s_sdl_quit_event_type; +#endif + +private: + KeyboardContext(); + + void Init(); + void Quit(); + bool IsVirtualKeyPressed(int virtual_key) const; + u8 PollHIDModifiers() const; + HIDPressedKeys PollHIDPressedKeys() const; + + bool m_is_ready = false; + int m_host_layout = KeyboardLayout::AUTO; + int m_game_layout = KeyboardLayout::AUTO; +#ifdef HAVE_SDL3 + const bool* m_keyboard_state = nullptr; +#endif +}; +} // namespace Common diff --git a/Source/Core/Core/Config/MainSettings.cpp b/Source/Core/Core/Config/MainSettings.cpp index 699e0d1cd7..8fb4704586 100644 --- a/Source/Core/Core/Config/MainSettings.cpp +++ b/Source/Core/Core/Config/MainSettings.cpp @@ -196,6 +196,10 @@ const Info MAIN_WII_SD_CARD_ENABLE_FOLDER_SYNC{ {System::Main, "Core", "WiiSDCardEnableFolderSync"}, false}; const Info MAIN_WII_SD_CARD_FILESIZE{{System::Main, "Core", "WiiSDCardFilesize"}, 0}; const Info MAIN_WII_KEYBOARD{{System::Main, "Core", "WiiKeyboard"}, false}; +const Info MAIN_WII_KEYBOARD_HOST_LAYOUT{{System::Main, "Core", "WiiKeyboardHostLayout"}, 0}; +const Info MAIN_WII_KEYBOARD_GAME_LAYOUT{{System::Main, "Core", "WiiKeyboardGameLayout"}, 0}; +const Info MAIN_WII_KEYBOARD_TRANSLATION{{System::Main, "Core", "WiiKeyboardTranslation"}, + false}; const Info MAIN_WIIMOTE_CONTINUOUS_SCANNING{ {System::Main, "Core", "WiimoteContinuousScanning"}, false}; const Info MAIN_WIIMOTE_AUTO_CONNECT_ADDRESSES{ diff --git a/Source/Core/Core/Config/MainSettings.h b/Source/Core/Core/Config/MainSettings.h index 580ccfdabf..1d39f23b0d 100644 --- a/Source/Core/Core/Config/MainSettings.h +++ b/Source/Core/Core/Config/MainSettings.h @@ -108,6 +108,9 @@ extern const Info MAIN_WII_SD_CARD; extern const Info MAIN_WII_SD_CARD_ENABLE_FOLDER_SYNC; extern const Info MAIN_WII_SD_CARD_FILESIZE; extern const Info MAIN_WII_KEYBOARD; +extern const Info MAIN_WII_KEYBOARD_HOST_LAYOUT; +extern const Info MAIN_WII_KEYBOARD_GAME_LAYOUT; +extern const Info MAIN_WII_KEYBOARD_TRANSLATION; extern const Info MAIN_WIIMOTE_CONTINUOUS_SCANNING; extern const Info MAIN_WIIMOTE_AUTO_CONNECT_ADDRESSES; extern const Info MAIN_WIIMOTE_ENABLE_SPEAKER; diff --git a/Source/Core/Core/IOS/USB/USB_KBD.cpp b/Source/Core/Core/IOS/USB/USB_KBD.cpp index c0bf361e2f..f5a4e673de 100644 --- a/Source/Core/Core/IOS/USB/USB_KBD.cpp +++ b/Source/Core/Core/IOS/USB/USB_KBD.cpp @@ -3,9 +3,6 @@ #include "Core/IOS/USB/USB_KBD.h" -#include -#include - #include "Common/FileUtil.h" #include "Common/IniFile.h" #include "Common/Logging/Log.h" @@ -16,169 +13,11 @@ #include "Core/System.h" #include "InputCommon/ControlReference/ControlReference.h" // For background input check -#ifdef _WIN32 -#include -#endif - namespace IOS::HLE { -namespace -{ -// Crazy ugly -#ifdef _WIN32 -constexpr std::array s_key_codes_qwerty{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2A, // Backspace - 0x2B, // Tab - 0x00, 0x00, - 0x00, // Clear - 0x28, // Return - 0x00, 0x00, - 0x00, // Shift - 0x00, // Control - 0x00, // ALT - 0x48, // Pause - 0x39, // Capital - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x29, // Escape - 0x00, 0x00, 0x00, 0x00, - 0x2C, // Space - 0x4B, // Prior - 0x4E, // Next - 0x4D, // End - 0x4A, // Home - 0x50, // Left - 0x52, // Up - 0x4F, // Right - 0x51, // Down - 0x00, 0x00, 0x00, - 0x46, // Print screen - 0x49, // Insert - 0x4C, // Delete - 0x00, - // 0 -> 9 - 0x27, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, - // A -> Z - 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x10, 0x11, 0x12, 0x13, - 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A, 0x1B, 0x1C, 0x1D, 0x00, 0x00, 0x00, 0x00, 0x00, - // Numpad 0 -> 9 - 0x62, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, - 0x55, // Multiply - 0x57, // Add - 0x00, // Separator - 0x56, // Subtract - 0x63, // Decimal - 0x54, // Divide - // F1 -> F12 - 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, - // F13 -> F24 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x53, // Numlock - 0x47, // Scroll lock - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Modifier keys - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x33, // ';' - 0x2E, // Plus - 0x36, // Comma - 0x2D, // Minus - 0x37, // Period - 0x38, // '/' - 0x35, // '~' - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2F, // '[' - 0x32, // '\' - 0x30, // ']' - 0x34, // ''' - 0x00, // - 0x00, // Nothing interesting past this point. -}; - -constexpr std::array s_key_codes_azerty{ - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2A, // Backspace - 0x2B, // Tab - 0x00, 0x00, - 0x00, // Clear - 0x28, // Return - 0x00, 0x00, - 0x00, // Shift - 0x00, // Control - 0x00, // ALT - 0x48, // Pause - 0x39, // Capital - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x29, // Escape - 0x00, 0x00, 0x00, 0x00, - 0x2C, // Space - 0x4B, // Prior - 0x4E, // Next - 0x4D, // End - 0x4A, // Home - 0x50, // Left - 0x52, // Up - 0x4F, // Right - 0x51, // Down - 0x00, 0x00, 0x00, - 0x46, // Print screen - 0x49, // Insert - 0x4C, // Delete - 0x00, - // 0 -> 9 - 0x27, 0x1E, 0x1F, 0x20, 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, - // A -> Z - 0x14, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A, 0x0B, 0x0C, 0x0D, 0x0E, 0x0F, 0x33, 0x11, 0x12, 0x13, - 0x04, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1D, 0x1B, 0x1C, 0x1A, 0x00, 0x00, 0x00, 0x00, 0x00, - // Numpad 0 -> 9 - 0x62, 0x59, 0x5A, 0x5B, 0x5C, 0x5D, 0x5E, 0x5F, 0x60, 0x61, - 0x55, // Multiply - 0x57, // Add - 0x00, // Separator - 0x56, // Substract - 0x63, // Decimal - 0x54, // Divide - // F1 -> F12 - 0x3A, 0x3B, 0x3C, 0x3D, 0x3E, 0x3F, 0x40, 0x41, 0x42, 0x43, 0x44, 0x45, - // F13 -> F24 - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, - 0x53, // Numlock - 0x47, // Scroll lock - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - // Modifier keys - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x30, // '$' - 0x2E, // Plus - 0x10, // Comma - 0x00, // Minus - 0x36, // Period - 0x37, // '/' - 0x34, // ' ' - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, - 0x2D, // ')' - 0x32, // '\' - 0x2F, // '^' - 0x00, // ' ' - 0x38, // '!' - 0x00, // Nothing interesting past this point. -}; -#else -constexpr std::array s_key_codes_qwerty{}; - -constexpr std::array s_key_codes_azerty{}; -#endif -} // Anonymous namespace - -USB_KBD::MessageData::MessageData(MessageType type, u8 modifiers_, PressedKeyData pressed_keys_) - : msg_type{Common::swap32(static_cast(type))}, modifiers{modifiers_}, - pressed_keys{pressed_keys_} +USB_KBD::MessageData::MessageData(MessageType type, const Common::HIDPressedState& state) + : msg_type{Common::swap32(static_cast(type))}, modifiers{state.modifiers}, + pressed_keys{state.pressed_keys} { } @@ -192,18 +31,24 @@ USB_KBD::USB_KBD(EmulationKernel& ios, const std::string& device_name) std::optional USB_KBD::Open(const OpenRequest& request) { INFO_LOG_FMT(IOS, "USB_KBD: Open"); - Common::IniFile ini; - ini.Load(File::GetUserPath(F_DOLPHINCONFIG_IDX)); - ini.GetOrCreateSection("USB Keyboard")->Get("Layout", &m_keyboard_layout, KBD_LAYOUT_QWERTY); m_message_queue = {}; - m_old_key_buffer.fill(false); - m_old_modifiers = 0x00; + m_previous_state = {}; + m_keyboard_context = Common::KeyboardContext::GetInstance(); - // m_message_queue.emplace(MessageType::KeyboardConnect, 0, PressedKeyData{}); + // m_message_queue.emplace(MessageType::KeyboardConnect, {}); return Device::Open(request); } +std::optional USB_KBD::Close(u32 fd) +{ + INFO_LOG_FMT(IOS, "USB_KBD: Close"); + + m_keyboard_context.reset(); + + return Device::Close(fd); +} + std::optional USB_KBD::Write(const ReadWriteRequest& request) { // Stubbed. @@ -223,91 +68,20 @@ std::optional USB_KBD::IOCtl(const IOCtlRequest& request) return IPCReply(IPC_SUCCESS); } -bool USB_KBD::IsKeyPressed(int key) const -{ -#ifdef _WIN32 - return (GetAsyncKeyState(key) & 0x8000) != 0; -#else - // TODO: do it for non-Windows platforms - return false; -#endif -} - void USB_KBD::Update() { - if (!Config::Get(Config::MAIN_WII_KEYBOARD) || Core::WantsDeterminism() || !m_is_active) + if (!Config::Get(Config::MAIN_WII_KEYBOARD) || Core::WantsDeterminism() || !m_is_active || + !m_keyboard_context) + { + return; + } + + const auto current_state = m_keyboard_context->GetPressedState(); + + if (current_state == m_previous_state) return; - u8 modifiers = 0x00; - PressedKeyData pressed_keys{0x00, 0x00, 0x00, 0x00, 0x00, 0x00}; - bool got_event = false; - size_t num_pressed_keys = 0; - for (size_t i = 0; i < m_old_key_buffer.size(); i++) - { - const bool key_pressed_now = IsKeyPressed(static_cast(i)); - const bool key_pressed_before = m_old_key_buffer[i]; - u8 key_code = 0; - - if (key_pressed_now ^ key_pressed_before) - { - if (key_pressed_now) - { - switch (m_keyboard_layout) - { - case KBD_LAYOUT_QWERTY: - key_code = s_key_codes_qwerty[i]; - break; - - case KBD_LAYOUT_AZERTY: - key_code = s_key_codes_azerty[i]; - break; - } - - if (key_code == 0x00) - continue; - - pressed_keys[num_pressed_keys] = key_code; - - num_pressed_keys++; - if (num_pressed_keys == pressed_keys.size()) - break; - } - - got_event = true; - } - - m_old_key_buffer[i] = key_pressed_now; - } - -#ifdef _WIN32 - if (GetAsyncKeyState(VK_LCONTROL) & 0x8000) - modifiers |= 0x01; - if (GetAsyncKeyState(VK_LSHIFT) & 0x8000) - modifiers |= 0x02; - if (GetAsyncKeyState(VK_MENU) & 0x8000) - modifiers |= 0x04; - if (GetAsyncKeyState(VK_LWIN) & 0x8000) - modifiers |= 0x08; - if (GetAsyncKeyState(VK_RCONTROL) & 0x8000) - modifiers |= 0x10; - if (GetAsyncKeyState(VK_RSHIFT) & 0x8000) - modifiers |= 0x20; - // TODO: VK_MENU is for ALT, not for ALT GR (ALT GR seems to work though...) - if (GetAsyncKeyState(VK_MENU) & 0x8000) - modifiers |= 0x40; - if (GetAsyncKeyState(VK_RWIN) & 0x8000) - modifiers |= 0x80; -#else -// TODO: modifiers for non-Windows platforms -#endif - - if (modifiers ^ m_old_modifiers) - { - got_event = true; - m_old_modifiers = modifiers; - } - - if (got_event) - m_message_queue.emplace(MessageType::Event, modifiers, pressed_keys); + m_message_queue.emplace(MessageType::Event, current_state); + m_previous_state = std::move(current_state); } } // namespace IOS::HLE diff --git a/Source/Core/Core/IOS/USB/USB_KBD.h b/Source/Core/Core/IOS/USB/USB_KBD.h index ea928b1236..41788b828b 100644 --- a/Source/Core/Core/IOS/USB/USB_KBD.h +++ b/Source/Core/Core/IOS/USB/USB_KBD.h @@ -3,12 +3,11 @@ #pragma once -#include #include #include -#include #include "Common/CommonTypes.h" +#include "Common/Keyboard.h" #include "Core/IOS/Device.h" #include "Core/IOS/IOS.h" @@ -20,6 +19,7 @@ public: USB_KBD(EmulationKernel& ios, const std::string& device_name); std::optional Open(const OpenRequest& request) override; + std::optional Close(u32 fd) override; std::optional Write(const ReadWriteRequest& request) override; std::optional IOCtl(const IOCtlRequest& request) override; void Update() override; @@ -32,8 +32,6 @@ private: Event = 2 }; - using PressedKeyData = std::array; - #pragma pack(push, 1) struct MessageData { @@ -41,26 +39,15 @@ private: u32 unk1 = 0; u8 modifiers = 0; u8 unk2 = 0; - PressedKeyData pressed_keys{}; + Common::HIDPressedKeys pressed_keys{}; - MessageData(MessageType msg_type, u8 modifiers, PressedKeyData pressed_keys); + MessageData(MessageType msg_type, const Common::HIDPressedState& state); }; static_assert(std::is_trivially_copyable_v, "MessageData must be trivially copyable, as it's copied into emulated memory."); #pragma pack(pop) std::queue m_message_queue; - - std::array m_old_key_buffer{}; - u8 m_old_modifiers = 0; - - bool IsKeyPressed(int key) const; - - // This stuff should probably die - enum - { - KBD_LAYOUT_QWERTY = 0, - KBD_LAYOUT_AZERTY = 1 - }; - int m_keyboard_layout = KBD_LAYOUT_QWERTY; + Common::HIDPressedState m_previous_state; + std::shared_ptr m_keyboard_context; }; } // namespace IOS::HLE diff --git a/Source/Core/DolphinLib.props b/Source/Core/DolphinLib.props index 085405be47..dfc6c74d88 100644 --- a/Source/Core/DolphinLib.props +++ b/Source/Core/DolphinLib.props @@ -133,6 +133,7 @@ + @@ -838,6 +839,7 @@ + diff --git a/Source/Core/DolphinQt/CMakeLists.txt b/Source/Core/DolphinQt/CMakeLists.txt index f277a982b6..b20fcc4c9e 100644 --- a/Source/Core/DolphinQt/CMakeLists.txt +++ b/Source/Core/DolphinQt/CMakeLists.txt @@ -249,6 +249,8 @@ add_executable(dolphin-emu DiscordHandler.h DiscordJoinRequestDialog.cpp DiscordJoinRequestDialog.h + EmulatedUSB/Keyboard.cpp + EmulatedUSB/Keyboard.h EmulatedUSB/WiiSpeakWindow.cpp EmulatedUSB/WiiSpeakWindow.h FIFO/FIFOAnalyzer.cpp diff --git a/Source/Core/DolphinQt/DolphinQt.vcxproj b/Source/Core/DolphinQt/DolphinQt.vcxproj index 0250f7260e..5c538b9610 100644 --- a/Source/Core/DolphinQt/DolphinQt.vcxproj +++ b/Source/Core/DolphinQt/DolphinQt.vcxproj @@ -158,6 +158,7 @@ + @@ -380,6 +381,7 @@ + diff --git a/Source/Core/DolphinQt/EmulatedUSB/Keyboard.cpp b/Source/Core/DolphinQt/EmulatedUSB/Keyboard.cpp new file mode 100644 index 0000000000..e21b390b36 --- /dev/null +++ b/Source/Core/DolphinQt/EmulatedUSB/Keyboard.cpp @@ -0,0 +1,90 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#include "DolphinQt/EmulatedUSB/Keyboard.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "Common/Keyboard.h" +#include "Core/Config/MainSettings.h" +#include "DolphinQt/Config/ConfigControls/ConfigBool.h" +#include "DolphinQt/Settings.h" + +KeyboardWindow::KeyboardWindow(QWidget* parent) : QWidget(parent) +{ + // i18n: Window for managing the Wii keyboard emulation + setWindowTitle(tr("Keyboard Manager")); + setObjectName(QStringLiteral("keyboard_manager")); + setMinimumSize(QSize(500, 200)); + + auto* main_layout = new QVBoxLayout(); + + { + auto* group = new QGroupBox(); + auto* hbox_layout = new QHBoxLayout(); + hbox_layout->setAlignment(Qt::AlignHCenter); + auto* checkbox_emulate = new QCheckBox(tr("Emulate USB keyboard"), this); + checkbox_emulate->setChecked(Settings::Instance().IsUSBKeyboardConnected()); + connect(checkbox_emulate, &QCheckBox::toggled, this, &KeyboardWindow::EmulateKeyboard); + connect(&Settings::Instance(), &Settings::USBKeyboardConnectionChanged, checkbox_emulate, + &QCheckBox::setChecked); + hbox_layout->addWidget(checkbox_emulate); + group->setLayout(hbox_layout); + + main_layout->addWidget(group); + } + + { + auto* group = new QGroupBox(tr("Layout Configuration")); + auto* grid_layout = new QGridLayout(); + auto* checkbox_translate = + new ConfigBool(tr("Enable partial translation"), Config::MAIN_WII_KEYBOARD_TRANSLATION); + grid_layout->addWidget(checkbox_translate, 0, 0, 1, 2, Qt::AlignLeft); + + auto create_combo = [checkbox_translate, grid_layout](int row, const QString& name, + const auto& config_info) { + grid_layout->addWidget(new QLabel(name), row, 0); + auto* combo = new QComboBox(); + + combo->addItem(tr("Automatic detection"), Common::KeyboardLayout::AUTO); + combo->addItem(QStringLiteral("QWERTY"), Common::KeyboardLayout::QWERTY); + combo->addItem(QStringLiteral("AZERTY"), Common::KeyboardLayout::AZERTY); + combo->addItem(QStringLiteral("QWERTZ"), Common::KeyboardLayout::QWERTZ); + combo->setCurrentIndex(combo->findData(Config::Get(config_info))); + combo->setEnabled(checkbox_translate->isChecked()); + + connect(combo, &QComboBox::currentIndexChanged, combo, [combo, config_info] { + const auto keyboard_layout = combo->currentData(); + if (!keyboard_layout.isValid()) + return; + + Config::SetBaseOrCurrent(config_info, keyboard_layout.toInt()); + Common::KeyboardContext::UpdateLayout(); + }); + connect(checkbox_translate, &QCheckBox::toggled, combo, &QComboBox::setEnabled); + + grid_layout->addWidget(combo, row, 1); + }; + + create_combo(1, tr("Host layout:"), Config::MAIN_WII_KEYBOARD_HOST_LAYOUT); + create_combo(2, tr("Game layout:"), Config::MAIN_WII_KEYBOARD_GAME_LAYOUT); + group->setLayout(grid_layout); + + main_layout->addWidget(group); + } + + setLayout(main_layout); +} + +KeyboardWindow::~KeyboardWindow() = default; + +void KeyboardWindow::EmulateKeyboard(bool emulate) const +{ + Settings::Instance().SetUSBKeyboardConnected(emulate); +} diff --git a/Source/Core/DolphinQt/EmulatedUSB/Keyboard.h b/Source/Core/DolphinQt/EmulatedUSB/Keyboard.h new file mode 100644 index 0000000000..73d3547744 --- /dev/null +++ b/Source/Core/DolphinQt/EmulatedUSB/Keyboard.h @@ -0,0 +1,17 @@ +// Copyright 2025 Dolphin Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +#pragma once + +#include + +class KeyboardWindow : public QWidget +{ + Q_OBJECT +public: + explicit KeyboardWindow(QWidget* parent = nullptr); + ~KeyboardWindow() override; + +private: + void EmulateKeyboard(bool emulate) const; +}; diff --git a/Source/Core/DolphinQt/Host.cpp b/Source/Core/DolphinQt/Host.cpp index 46b0aa8963..169f2772c0 100644 --- a/Source/Core/DolphinQt/Host.cpp +++ b/Source/Core/DolphinQt/Host.cpp @@ -16,6 +16,7 @@ #endif #include "Common/Common.h" +#include "Common/Keyboard.h" #include "Core/Config/MainSettings.h" #include "Core/ConfigManager.h" @@ -62,6 +63,11 @@ void Host::SetRenderHandle(void* handle) { m_render_to_main = Config::Get(Config::MAIN_RENDER_TO_MAIN); + Common::KeyboardContext::NotifyHandlerChanged({.main_handle = m_main_window_handle.load(), + .renderer_handle = handle, + .is_fullscreen = m_render_fullscreen.load(), + .is_rendering_to_main = m_render_to_main.load()}); + if (m_render_handle == handle) return; @@ -176,6 +182,11 @@ void Host::SetRenderFullscreen(bool fullscreen) { m_render_fullscreen = fullscreen; + Common::KeyboardContext::NotifyHandlerChanged({.main_handle = m_main_window_handle.load(), + .renderer_handle = m_render_handle.load(), + .is_fullscreen = fullscreen, + .is_rendering_to_main = m_render_to_main.load()}); + if (g_gfx && g_gfx->IsFullscreen() != fullscreen && g_ActiveConfig.ExclusiveFullscreenEnabled()) { RunWithGPUThreadInactive([fullscreen] { g_gfx->SetFullscreen(fullscreen); }); diff --git a/Source/Core/DolphinQt/MainWindow.cpp b/Source/Core/DolphinQt/MainWindow.cpp index 488ca8ecd6..c2afac0a64 100644 --- a/Source/Core/DolphinQt/MainWindow.cpp +++ b/Source/Core/DolphinQt/MainWindow.cpp @@ -93,6 +93,7 @@ #include "DolphinQt/Debugger/ThreadWidget.h" #include "DolphinQt/Debugger/WatchWidget.h" #include "DolphinQt/DiscordHandler.h" +#include "DolphinQt/EmulatedUSB/Keyboard.h" #include "DolphinQt/EmulatedUSB/WiiSpeakWindow.h" #include "DolphinQt/FIFO/FIFOPlayerWindow.h" #include "DolphinQt/GCMemcardManager.h" @@ -579,6 +580,7 @@ void MainWindow::ConnectMenuBar() connect(m_menu_bar, &MenuBar::ShowSkylanderPortal, this, &MainWindow::ShowSkylanderPortal); connect(m_menu_bar, &MenuBar::ShowInfinityBase, this, &MainWindow::ShowInfinityBase); connect(m_menu_bar, &MenuBar::ShowWiiSpeakWindow, this, &MainWindow::ShowWiiSpeakWindow); + connect(m_menu_bar, &MenuBar::ShowKeyboard, this, &MainWindow::ShowKeyboard); connect(m_menu_bar, &MenuBar::ConnectWiiRemote, this, &MainWindow::OnConnectWiiRemote); #ifdef USE_RETRO_ACHIEVEMENTS @@ -1425,6 +1427,18 @@ void MainWindow::ShowWiiSpeakWindow() m_wii_speak_window->activateWindow(); } +void MainWindow::ShowKeyboard() +{ + if (!m_keyboard_window) + { + m_keyboard_window = new KeyboardWindow(); + } + + m_keyboard_window->show(); + m_keyboard_window->raise(); + m_keyboard_window->activateWindow(); +} + void MainWindow::StateLoad() { QString dialog_path = (Config::Get(Config::MAIN_CURRENT_STATE_PATH).empty()) ? diff --git a/Source/Core/DolphinQt/MainWindow.h b/Source/Core/DolphinQt/MainWindow.h index d648e6189c..0ac2d4ed81 100644 --- a/Source/Core/DolphinQt/MainWindow.h +++ b/Source/Core/DolphinQt/MainWindow.h @@ -36,6 +36,7 @@ class GBATASInputWindow; class GCTASInputWindow; class HotkeyScheduler; class InfinityBaseWindow; +class KeyboardWindow; class JITWidget; class LogConfigWidget; class LogWidget; @@ -177,6 +178,7 @@ private: void ShowSkylanderPortal(); void ShowInfinityBase(); void ShowWiiSpeakWindow(); + void ShowKeyboard(); void ShowMemcardManager(); void ShowResourcePackManager(); void ShowCheatsManager(); @@ -249,6 +251,7 @@ private: SkylanderPortalWindow* m_skylander_window = nullptr; InfinityBaseWindow* m_infinity_window = nullptr; WiiSpeakWindow* m_wii_speak_window = nullptr; + KeyboardWindow* m_keyboard_window = nullptr; MappingWindow* m_hotkey_window = nullptr; FreeLookWindow* m_freelook_window = nullptr; diff --git a/Source/Core/DolphinQt/MenuBar.cpp b/Source/Core/DolphinQt/MenuBar.cpp index 6f21030294..82b6291557 100644 --- a/Source/Core/DolphinQt/MenuBar.cpp +++ b/Source/Core/DolphinQt/MenuBar.cpp @@ -280,6 +280,7 @@ void MenuBar::AddToolsMenu() usb_device_menu->addAction(tr("&Skylanders Portal"), this, &MenuBar::ShowSkylanderPortal); usb_device_menu->addAction(tr("&Infinity Base"), this, &MenuBar::ShowInfinityBase); usb_device_menu->addAction(tr("&Wii Speak"), this, &MenuBar::ShowWiiSpeakWindow); + usb_device_menu->addAction(tr("&Keyboard"), this, &MenuBar::ShowKeyboard); tools_menu->addMenu(usb_device_menu); tools_menu->addSeparator(); diff --git a/Source/Core/DolphinQt/MenuBar.h b/Source/Core/DolphinQt/MenuBar.h index 934772e72c..075155739c 100644 --- a/Source/Core/DolphinQt/MenuBar.h +++ b/Source/Core/DolphinQt/MenuBar.h @@ -95,6 +95,7 @@ signals: void ShowSkylanderPortal(); void ShowInfinityBase(); void ShowWiiSpeakWindow(); + void ShowKeyboard(); void ConnectWiiRemote(int id); #ifdef USE_RETRO_ACHIEVEMENTS diff --git a/Source/Core/DolphinQt/Settings.cpp b/Source/Core/DolphinQt/Settings.cpp index cb76f0f5bf..15ad3f153b 100644 --- a/Source/Core/DolphinQt/Settings.cpp +++ b/Source/Core/DolphinQt/Settings.cpp @@ -24,6 +24,7 @@ #include "Common/Config/Config.h" #include "Common/Contains.h" #include "Common/FileUtil.h" +#include "Common/Keyboard.h" #include "Common/StringUtil.h" #include "Core/AchievementManager.h" @@ -789,6 +790,10 @@ void Settings::SetUSBKeyboardConnected(bool connected) if (IsUSBKeyboardConnected() != connected) { Config::SetBaseOrCurrent(Config::MAIN_WII_KEYBOARD, connected); + if (connected) + Common::KeyboardContext::NotifyInit(); + else + Common::KeyboardContext::NotifyQuit(); emit USBKeyboardConnectionChanged(connected); } } diff --git a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp index 8e4043f4a9..f7e32fb4ef 100644 --- a/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp +++ b/Source/Core/InputCommon/ControllerInterface/SDL/SDL.cpp @@ -3,6 +3,7 @@ #include "InputCommon/ControllerInterface/SDL/SDL.h" +#include #include #include #include @@ -14,12 +15,89 @@ #include #include "Common/Event.h" +#include "Common/Keyboard.h" #include "Common/Logging/Log.h" #include "Common/ScopeGuard.h" +#include "Core/Host.h" #include "InputCommon/ControllerInterface/ControllerInterface.h" #include "InputCommon/ControllerInterface/SDL/SDLGamepad.h" +namespace +{ +using UniqueSDLWindow = std::unique_ptr; + +// Based on sdl2-compat 76eb981a4c376bcaf615c0af37d46512ba45cfb8 +SDL_Window* SDL_CreateWindowFrom(void* handle) +{ + SDL_PropertiesID props = SDL_CreateProperties(); + if (!props) + return nullptr; + + if (const char* hint = SDL_GetHint("SDL_VIDEO_WINDOW_SHARE_PIXEL_FORMAT"); hint) + { + // This hint is a pointer (in string form) of the address of + // the window to share a pixel format with + SDL_Window* other_window = nullptr; + if (SDL_sscanf(hint, "%p", &other_window)) + { + void* ptr = SDL_GetPointerProperty(SDL_GetWindowProperties(other_window), + SDL_PROP_WINDOW_WIN32_HWND_POINTER, nullptr); + SDL_SetPointerProperty(props, SDL_PROP_WINDOW_CREATE_WIN32_PIXEL_FORMAT_HWND_POINTER, ptr); + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true); + } + } + + if (SDL_GetHintBoolean("SDL_VIDEO_FOREIGN_WINDOW_OPENGL", false)) + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_OPENGL_BOOLEAN, true); + + if (SDL_GetHintBoolean("SDL_VIDEO_FOREIGN_WINDOW_VULKAN", false)) + SDL_SetBooleanProperty(props, SDL_PROP_WINDOW_CREATE_VULKAN_BOOLEAN, true); + + SDL_SetPointerProperty(props, "sdl2-compat.external_window", handle); + SDL_Window* window = SDL_CreateWindowWithProperties(props); + SDL_DestroyProperties(props); + + // SDL3 has per-window text input, so we must enable on this window if it's active + if (SDL_EventEnabled(SDL_EVENT_TEXT_INPUT)) + { + props = SDL_CreateProperties(); + SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_TYPE_NUMBER, SDL_TEXTINPUT_TYPE_TEXT); + SDL_SetNumberProperty(props, SDL_PROP_TEXTINPUT_CAPITALIZATION_NUMBER, SDL_CAPITALIZE_NONE); + SDL_SetBooleanProperty(props, SDL_PROP_TEXTINPUT_AUTOCORRECT_BOOLEAN, false); + SDL_StartTextInputWithProperties(window, props); + SDL_DestroyProperties(props); + } + + return window; +} + +std::optional UpdateKeyboardHandle(UniqueSDLWindow* unique_window) +{ + std::optional error; + + void* keyboard_handle = Common::KeyboardContext::GetWindowHandle(); + SDL_Window* keyboard_window = SDL_CreateWindowFrom(keyboard_handle); + if (keyboard_window == nullptr) + error = SDL_GetError(); + + unique_window->reset(keyboard_window); + if (error.has_value()) + return error; + + // SDL aggressive hooking might make the window borderless sometimes + if (!Host_RendererIsFullscreen()) + { + SDL_SetWindowFullscreen(keyboard_window, 0); + SDL_SetWindowBordered(keyboard_window, true); + } + + Common::KeyboardContext::UpdateLayout(); + + return error; +} +} // namespace + namespace ciface::SDL { @@ -40,6 +118,7 @@ private: Uint32 m_stop_event_type; Uint32 m_populate_event_type; std::thread m_hotplug_thread; + UniqueSDLWindow m_keyboard_window{nullptr, SDL_DestroyWindow}; }; std::unique_ptr CreateInputBackend(ControllerInterface* controller_interface) @@ -155,7 +234,7 @@ InputBackend::InputBackend(ControllerInterface* controller_interface) return; } - const Uint32 custom_events_start = SDL_RegisterEvents(2); + const Uint32 custom_events_start = SDL_RegisterEvents(5); if (custom_events_start == static_cast(-1)) { ERROR_LOG_FMT(CONTROLLERINTERFACE, "SDL failed to register custom events"); @@ -163,6 +242,9 @@ InputBackend::InputBackend(ControllerInterface* controller_interface) } m_stop_event_type = custom_events_start; m_populate_event_type = custom_events_start + 1; + Common::KeyboardContext::s_sdl_init_event_type = custom_events_start + 2; + Common::KeyboardContext::s_sdl_update_event_type = custom_events_start + 3; + Common::KeyboardContext::s_sdl_quit_event_type = custom_events_start + 4; // Drain all of the events and add the initial joysticks before returning. Otherwise, the // individual joystick events as well as the custom populate event will be handled _after_ @@ -285,6 +367,55 @@ bool InputBackend::HandleEventAndContinue(const SDL_Event& e) { return false; } + else if (e.type == Common::KeyboardContext::s_sdl_init_event_type) + { + const auto wsi = GetControllerInterface().GetWindowSystemInfo(); + if (wsi.type == WindowSystemType::X11) + { + // Avoid a crash with Xwayland when the wrong driver is picked + if (!SDL_SetHint(SDL_HINT_VIDEO_DRIVER, "x11")) + { + WARN_LOG_FMT(IOS_USB, "SDL failed to pick driver to capture keyboard input: {}", + SDL_GetError()); + } + } + + if (!SDL_InitSubSystem(SDL_INIT_VIDEO)) + { + ERROR_LOG_FMT(IOS_USB, "SDL failed to init subsystem to capture keyboard input: {}", + SDL_GetError()); + return true; + } + + if (const auto error = UpdateKeyboardHandle(&m_keyboard_window); error.has_value()) + { + ERROR_LOG_FMT(IOS_USB, "SDL failed to attach window to capture keyboard input: {}", *error); + return true; + } + } + else if (e.type == Common::KeyboardContext::s_sdl_update_event_type) + { + if (!SDL_WasInit(SDL_INIT_VIDEO)) + return true; + + // Release previous SDLWindow + m_keyboard_window.reset(); + + if (const auto error = UpdateKeyboardHandle(&m_keyboard_window); error.has_value()) + { + ERROR_LOG_FMT(IOS_USB, "SDL failed to switch window to capture keyboard input: {}", *error); + return true; + } + } + else if (e.type == Common::KeyboardContext::s_sdl_quit_event_type) + { + m_keyboard_window.reset(); + SDL_QuitSubSystem(SDL_INIT_VIDEO); + } + else if (e.type == SDL_EVENT_KEYMAP_CHANGED) + { + Common::KeyboardContext::UpdateLayout(); + } return true; }