From 02ca2e6451f9b3008c368c99107d25ecae2f626f Mon Sep 17 00:00:00 2001 From: Pit64 Date: Wed, 10 Dec 2025 11:15:59 +0100 Subject: [PATCH 1/3] fix(frontend): fix 7z warning on compatible arcade systems --- .../es-app/src/views/ViewController.cpp | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/projects/frontend/es-app/src/views/ViewController.cpp b/projects/frontend/es-app/src/views/ViewController.cpp index 7fcc8d4bfa..b5ae2f20b9 100755 --- a/projects/frontend/es-app/src/views/ViewController.cpp +++ b/projects/frontend/es-app/src/views/ViewController.cpp @@ -600,19 +600,22 @@ bool ViewController::CheckExtensions(const EmulatorData& emulator) return true; } // Check extension in the zip itself - extensions = emulator.CoreInfo().FileExtensions(); - if (!extensions.empty()) extensions.Insert(0, ' ').Append(' '); - SevenZipInspector zip(mGameToLaunch->RomPath()); - bool compatible = extensions.empty(); // No extension to check == compatible - for(int i = (int)zip.AllFiles().size(); --i >= 0;) - if (String extFile = String(' ').Append(zip.AllFiles()[i].Extension().LowerCase()).Append(' '); - extensions.Contains(extFile)) - compatible = true; - if (!compatible) + if (!mGameToLaunch->System().IsArcade()) { - String text = (_F(_("This 7zipped game does not contain any file supported by the selected emulator. Would you like to run the game anyway, even though there's a high chance it will not work?")) / mGameToLaunch->Name()).ToString(); - mWindow.pushGui((new GuiMsgBox(mWindow, text, _("YES"), [this] { LaunchCheck(); }, _("NO"), nullptr))->SetDefaultButton(1)); - return true; + extensions = emulator.CoreInfo().FileExtensions(); + if (!extensions.empty()) extensions.Insert(0, ' ').Append(' '); + SevenZipInspector zip(mGameToLaunch->RomPath()); + bool compatible = extensions.empty(); // No extension to check == compatible + for(int i = (int)zip.AllFiles().size(); --i >= 0;) + if (String extFile = String(' ').Append(zip.AllFiles()[i].Extension().LowerCase()).Append(' '); + extensions.Contains(extFile)) + compatible = true; + if (!compatible) + { + String text = (_F(_("This 7zipped game does not contain any file supported by the selected emulator. Would you like to run the game anyway, even though there's a high chance it will not work?")) / mGameToLaunch->Name()).ToString(); + mWindow.pushGui((new GuiMsgBox(mWindow, text, _("YES"), [this] { LaunchCheck(); }, _("NO"), nullptr))->SetDefaultButton(1)); + return true; + } } } else -- GitLab From 493fa07af4d25a896a9c1789417dd93a7434dede Mon Sep 17 00:00:00 2001 From: Pit64 Date: Wed, 10 Dec 2025 19:29:41 +0100 Subject: [PATCH 2/3] fix(frontend): fix theme option sort order --- projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp | 2 +- projects/frontend/es-core/src/themes/ThemeOption.h | 9 ++++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp b/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp index bba8254d06..91069d29d5 100644 --- a/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp +++ b/projects/frontend/es-app/src/guis/menus/MenuBuilder.cpp @@ -108,7 +108,7 @@ void MenuBuilder::BuildThemeOptionSelector(const InheritableContext& context, co // Build list SelectorEntry::List list; for (const ThemeOption::Value& s : values) - list.push_back({ s.IsIndexed() ? s.Translated().ToTrimLeft("0123456789-. ") : s.Translated(), s.Raw(), s.Raw() == realSelected }); + list.push_back({ s.IsIndexed() ? s.Translated().SubString(s.TextStart()) : s.Translated(), s.Raw(), s.Raw() == realSelected }); // Create menu entry ItemSelector* existing = AsList(item.Type()); diff --git a/projects/frontend/es-core/src/themes/ThemeOption.h b/projects/frontend/es-core/src/themes/ThemeOption.h index d1298478bc..fe99687407 100644 --- a/projects/frontend/es-core/src/themes/ThemeOption.h +++ b/projects/frontend/es-core/src/themes/ThemeOption.h @@ -19,6 +19,7 @@ struct ThemeOption String mRaw; //!< Raw value (english or non-translated) String mTranslated; //!< Translated value int mIndex; //!< extracted index + int mTextStart; //!< Position where text starts (after index prefix) bool mIndexed; //!< Index is valid ? //! Extract index @@ -30,16 +31,22 @@ struct ThemeOption if (pos == 0) return; mIndex = (pos == std::string::npos) ? mRaw.AsInt() : mRaw.AsInt(mRaw[pos]); mIndexed = true; + // Calculate text start position (skip spaces, separator, and more spaces) + mTextStart = (int)pos; + while (mTextStart < (int)mRaw.size() && mRaw[mTextStart] == ' ') mTextStart++; + if (mTextStart < (int)mRaw.size() && (mRaw[mTextStart] == '-' || mRaw[mTextStart] == '.')) mTextStart++; + while (mTextStart < (int)mRaw.size() && mRaw[mTextStart] == ' ') mTextStart++; } public: //! Constructor - Value(const String& raw, const String& translated) : mRaw(raw), mTranslated(translated), mIndex(0), mIndexed(false) { ExtractIndex(); } + Value(const String& raw, const String& translated) : mRaw(raw), mTranslated(translated), mIndex(0), mTextStart(0), mIndexed(false) { ExtractIndex(); } //! Get raw string [[nodiscard]] const String& Raw() const { return mRaw; } //! Get translated, or raw if no translation has been recorded [[nodiscard]] const String& Translated() const { return mTranslated.empty() ? mRaw : mTranslated; } [[nodiscard]] int Index() const { return mIndex; } + [[nodiscard]] int TextStart() const { return mTextStart; } [[nodiscard]] bool IsIndexed() const { return mIndexed; } }; -- GitLab From 4781f24beec6405a0a174eb6faf7cac73b4d173c Mon Sep 17 00:00:00 2001 From: Pit64 Date: Thu, 11 Dec 2025 11:17:02 +0100 Subject: [PATCH 3/3] fix(frontend): fix keyboard layout on kmsdrm --- projects/frontend/es-app/src/MainRunner.cpp | 7 +- .../es-core/src/input/InputManager.cpp | 22 +- .../frontend/es-core/src/input/InputManager.h | 36 ++- .../src/input/KeyboardLayoutMapper.cpp | 237 ++++++++++++++++++ .../es-core/src/input/KeyboardLayoutMapper.h | 103 ++++++++ 5 files changed, 393 insertions(+), 12 deletions(-) create mode 100644 projects/frontend/es-core/src/input/KeyboardLayoutMapper.cpp create mode 100644 projects/frontend/es-core/src/input/KeyboardLayoutMapper.h diff --git a/projects/frontend/es-app/src/MainRunner.cpp b/projects/frontend/es-app/src/MainRunner.cpp index 02fe7b0d14..fcd2dd7672 100644 --- a/projects/frontend/es-app/src/MainRunner.cpp +++ b/projects/frontend/es-app/src/MainRunner.cpp @@ -379,7 +379,12 @@ MainRunner::ExitState MainRunner::MainLoop(ApplicationWindow& window, SystemMana case SDL_QUIT: return ExitState::Quit; case SDL_TEXTINPUT: { - window.textInput(sdlevent.text.text); + if (inputManager.IsUsingNativeSDL()) { window.textInput(sdlevent.text.text); } + else + { + String remappedText = inputManager.RemapTextInput(sdlevent.text.text); + window.textInput(remappedText.c_str()); + } break; } case SDL_JOYHATMOTION: diff --git a/projects/frontend/es-core/src/input/InputManager.cpp b/projects/frontend/es-core/src/input/InputManager.cpp index f310346408..af3326f19a 100644 --- a/projects/frontend/es-core/src/input/InputManager.cpp +++ b/projects/frontend/es-core/src/input/InputManager.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #define KEYBOARD_GUID_STRING { 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF } @@ -24,6 +25,8 @@ InputManager::InputManager(IKeyboardShortcut* interface) , mScancodePreviousStates() , mHub("DevInputWatch") , mShortcutInterface(interface) + , mKeyboardLayoutMapper(RecalboxConf::Instance().AsString("system.kblayout", "us")) + , mAltGrPressed(false) , mJoystickChangePending(false) , mJoystickChangePendingRemoved(false) { @@ -322,7 +325,15 @@ InputCompactEvent InputManager::ManageHatEvent(const SDL_JoyHatEvent& hat) InputCompactEvent InputManager::ManageKeyEvent(const SDL_KeyboardEvent& key, bool down) { - InputEvent event = InputEvent(InputEvent::sKeyboardDevice, InputEvent::EventType::Key, key.keysym.sym, down ? 1 : 0); + // Track AltGr key state (Right Alt) + if (key.keysym.sym == SDLK_RALT || key.keysym.scancode == SDL_SCANCODE_RALT) + { + mAltGrPressed = down; + } + + // Map scancode to keycode using the configured keyboard layout + SDL_Keycode mappedKey = mKeyboardLayoutMapper.MapScancode(key.keysym.scancode); + InputEvent event = InputEvent(InputEvent::sKeyboardDevice, InputEvent::EventType::Key, mappedKey, down ? 1 : 0); // Ignore repeat events if (key.repeat != 0u) return { InputCompactEvent::Entry::Nothing, InputCompactEvent::Entry::Nothing, 0, mKeyboard, event }; // Quit? @@ -557,10 +568,10 @@ OrderedDevices InputManager::GetMappedDeviceList(const InputMapper& mapper) return devices; } -String InputManager::GetMappedDeviceListConfiguration(const InputMapper& mapper, bool lighgunGame) +String InputManager::GetMappedDeviceListConfiguration(const InputMapper& mapper, bool lightgunGame) { String command; - InputMapper::PadList list = mapper.GetPads(lighgunGame ? InputMapper::LightGuns : InputMapper::Controllers); + InputMapper::PadList list = mapper.GetPads(lightgunGame ? InputMapper::LightGuns : InputMapper::Controllers); if (list.empty()) list = mapper.GetPads(); for (int player = 0; player < (int)list.size(); ++player) @@ -664,4 +675,9 @@ void InputManager::GetRawButtonEvents(Array& to) device.second.GetRawButtonEvents(to); } +String InputManager::RemapTextInput(const char* text) const +{ + return mKeyboardLayoutMapper.RemapText(text, mAltGrPressed); +} + diff --git a/projects/frontend/es-core/src/input/InputManager.h b/projects/frontend/es-core/src/input/InputManager.h index 000677aa8c..9802db014b 100644 --- a/projects/frontend/es-core/src/input/InputManager.h +++ b/projects/frontend/es-core/src/input/InputManager.h @@ -10,6 +10,7 @@ #include #include "IKeyboardShortcut.h" #include "utils/os/fs/watching/FileSystemWatcherHub.h" +#include "KeyboardLayoutMapper.h" class WindowManager; @@ -98,7 +99,7 @@ class InputManager : public StaticLifeCycleControler InputDevice& GetDeviceConfigurationFromId(SDL_JoystickID deviceId); /*! - * @brief Generate an ordered device list in function of player devices configuratons + * @brief Generate an ordered device list in function of player devices configurations * @return OrderedDevice object */ OrderedDevices GetMappedDeviceList(const InputMapper& mapper); @@ -108,10 +109,10 @@ class InputManager : public StaticLifeCycleControler * ready to be used in the configgen * @return Configuration string */ - String GetMappedDeviceListConfiguration(const InputMapper& mapper, bool lighgunGame); + String GetMappedDeviceListConfiguration(const InputMapper& mapper, bool lightgunGame); /*! - * @brief Lookup Xml configuration for a particular device, lookinf for matching + * @brief Lookup Xml configuration for a particular device, looking for matching * guid and/or name * @param device Device to look for configuration * @return @@ -119,13 +120,13 @@ class InputManager : public StaticLifeCycleControler bool LookupDeviceXmlConfiguration(InputDevice& device); /*! - * @brief Log a detailled report of the raw input event + * @brief Log a detailed report of the raw input event * @param event Input event */ static void LogRawEvent(const InputEvent& event); /*! - * @brief Log a detailled report of the input compact event + * @brief Log a detailed report of the input compact event * @param event compact event */ static void LogCompactEvent(const InputCompactEvent& event); @@ -137,7 +138,7 @@ class InputManager : public StaticLifeCycleControler void AddNotificationInterface(IInputChange* interface); /*! - * @brief Remove a notofocation interface + * @brief Remove a notification interface * Does nothing if the given interface has not been previously added * @param interface Interface to remove */ @@ -178,6 +179,19 @@ class InputManager : public StaticLifeCycleControler */ void GetRawButtonEvents(Array& to); + /*! + * @brief Remap text input characters based on keyboard layout + * @param text Input text from SDL_TEXTINPUT (assumes US layout) + * @return Remapped text for the actual keyboard layout + */ + String RemapTextInput(const char* text) const; + + /*! + * @brief Check if using native SDL keyboard mapping (X11/Wayland) + * @return True if using native SDL, false if custom remapping is needed + */ + bool IsUsingNativeSDL() const { return mKeyboardLayoutMapper.IsUsingNativeSDL(); } + private: //! Device list typedef std::vector InputDeviceList; @@ -212,6 +226,12 @@ class InputManager : public StaticLifeCycleControler //! Shortcut interface IKeyboardShortcut* mShortcutInterface; + //! Keyboard layout mapper + KeyboardLayoutMapper mKeyboardLayoutMapper; + + //! AltGr key state tracking + bool mAltGrPressed; + //! Joystick change pendings bool mJoystickChangePending; //! joystick change pending - added or removed? @@ -230,7 +250,7 @@ class InputManager : public StaticLifeCycleControler [[nodiscard]] bool IsInitialized() const { return !mIdToDevices.empty(); } /*! - * @brief Get the GUID string of an SDL joystik + * @brief Get the GUID string of an SDL joystick * @param joystick SDL Joystick handle * @return GUID string */ @@ -242,7 +262,7 @@ class InputManager : public StaticLifeCycleControler void ClearAllConfigurations(); /*! - * @brief Build current jostick list + * @brief Build current joystick list * @return Joystick list */ std::vector BuildCurrentDeviceList(); diff --git a/projects/frontend/es-core/src/input/KeyboardLayoutMapper.cpp b/projects/frontend/es-core/src/input/KeyboardLayoutMapper.cpp new file mode 100644 index 0000000000..67bf9d2718 --- /dev/null +++ b/projects/frontend/es-core/src/input/KeyboardLayoutMapper.cpp @@ -0,0 +1,237 @@ +// +// Created by Pit64 on 11/12/2025. +// +#include "KeyboardLayoutMapper.h" +#include + +KeyboardLayoutMapper::KeyboardLayoutMapper(const String& layout) + : mLayout(layout) + , mUsingNativeSDL(false) +{ + InitializeMappings(); + { LOG(LogInfo) << "[KeyboardLayoutMapper] Initialized layout: " << mLayout; } +} + +SDL_Keycode KeyboardLayoutMapper::MapScancode(SDL_Scancode const scancode) const +{ + const SDL_Keycode* mapped = mScancodeMap.try_get(scancode); + if (mapped != nullptr) + return *mapped; + + return SDL_GetKeyFromScancode(scancode); +} + +String KeyboardLayoutMapper::RemapText(const char* text, const bool altGrPressed) const +{ + if (text == nullptr || text[0] == '\0') + return String(); + + if (mCharMap.empty() && mAltGrCharMap.empty()) + return String(text); + + String result; + auto p = reinterpret_cast(text); + + while (*p != '\0') + { + unsigned int codepoint; + int len = 1; + + // Decode UTF-8 character + if (*p < 0x80) + { + codepoint = *p; + } + else if ((*p & 0xE0) == 0xC0) + { + codepoint = ((*p & 0x1F) << 6) | (p[1] & 0x3F); + len = 2; + } + else if ((*p & 0xF0) == 0xE0) + { + codepoint = ((*p & 0x0F) << 12) | ((p[1] & 0x3F) << 6) | (p[2] & 0x3F); + len = 3; + } + else if ((*p & 0xF8) == 0xF0) + { + codepoint = ((*p & 0x07) << 18) | ((p[1] & 0x3F) << 12) | ((p[2] & 0x3F) << 6) | (p[3] & 0x3F); + len = 4; + } + else + { + // Invalid UTF-8, skip + p++; + continue; + } + + // Try to map the codepoint + if (altGrPressed) + { + const char* const* altGrMapped = mAltGrCharMap.try_get(codepoint); + if (altGrMapped != nullptr) + { + result.Append(*altGrMapped); + p += len; + continue; + } + } + + const char* const* mapped = mCharMap.try_get(codepoint); + if (mapped != nullptr) + { + result.Append(*mapped); + } + else + { + // No mapping, copy original UTF-8 sequence + result.Append(reinterpret_cast(p), len); + } + + p += len; + } + + return result; +} + +void KeyboardLayoutMapper::InitializeMappings() +{ + mScancodeMap.clear(); + mCharMap.clear(); + mAltGrCharMap.clear(); + mUsingNativeSDL = false; + + const char* videoDriver = SDL_GetCurrentVideoDriver(); + if (videoDriver != nullptr) + { + if (strcmp(videoDriver, "x11") == 0 || strcmp(videoDriver, "wayland") == 0) + { + mUsingNativeSDL = true; + return; + } + } + + if (mLayout == "fr" || mLayout == "fr_FR") + AddFrenchLayout(); + else if (mLayout == "de" || mLayout == "de_DE") + AddGermanLayout(); + else if (mLayout == "es" || mLayout == "es_ES") + AddSpanishLayout(); + else if (mLayout == "it" || mLayout == "it_IT") + AddItalianLayout(); + else if (mLayout == "dk" || mLayout == "dk_DK") + AddDanishLayout(); + else if (mLayout == "uk" || mLayout == "en_GB") + AddUKLayout(); +} + +void KeyboardLayoutMapper::AddFrenchLayout() +{ + static const HashMap frenchCharMap = { + {'a', "q"}, {'q', "a"}, {'z', "w"}, {'w', "z"}, {'A', "Q"}, + {'Q', "A"}, {'Z', "W"}, {'W', "Z"}, {'`', "²"}, {'1', "&"}, + {'2', "é"}, {'3', "\""}, {'4', "'"}, {'5', "("}, {'6', "-"}, + {'7', "è"}, {'8', "_"}, {'9', "ç"}, {'0', "à"}, {'-', ")"}, + {'[', "^"}, {']', "$"}, {';', "m"}, {'\'', "ù"}, {'\\', "*"}, + {'m', ","}, {',', ";"}, {'.', ":"}, {'/', "!"}, {'!', "1"}, + {'@', "2"}, {'#', "3"}, {'$', "4"}, {'%', "5"}, {'^', "6"}, + {'&', "7"}, {'*', "8"}, {'(', "9"}, {')', "0"}, {'_', "°"}, + {'{', "¨"}, {'}', "£"}, {':', "%"}, {'|', "μ"}, {'M', "?"}, + {'<', "."}, {'>', "/"}, {'?', "§"} + }; + + static const HashMap frenchAltGrMap = { + {'2', "~"}, {'3', "#"}, {'4', "{"}, {'5', "["}, {'6', "|"}, + {'7', "`"}, {'8', "\\"}, {'9', "^"}, {'0', "@"}, {')', "]"}, + {'=', "}"}, {'e', "€"} + }; + + mCharMap = frenchCharMap; + mAltGrCharMap = frenchAltGrMap; +} + +void KeyboardLayoutMapper::AddGermanLayout() +{ + static const HashMap germanCharMap = { + {'z', "y"}, {'y', "z"}, {'Z', "Y"}, {'Y', "Z"}, {'-', "ß"}, + {'_', "?"}, {'[', "ü"}, {'{', "Ü"},{';', "ö"}, {':', "Ö"}, + {'\'', "ä"}, {'"', "Ä"},{'`', "^"}, {'~', "°"}, {'=', "´"}, + {'+', "`"} + }; + + static const HashMap germanAltGrMap = { + {'q', "@"}, {'e', "€"}, {'7', "{"}, {'8', "["},{'9', "]"}, + {'0', "}"}, {'-', "\\"}, {'=', "~"},{'<', "|"}, {'+', "~"} + }; + + mCharMap = germanCharMap; + mAltGrCharMap = germanAltGrMap; +} + +void KeyboardLayoutMapper::AddSpanishLayout() +{ + static const HashMap spanishCharMap = { + {'`', "º"}, {'-', "'"}, {'=', "¡"}, {'[', "`"}, {']', "+"}, + {'/', "-"}, {0x00AA, "~"}, {'@', "\""}, {'#', "."}, + {'^', "&"}, {'&', "/"}, {'*', "("}, {'(', ")"}, {')', "="}, + {'_', "?"}, {'+', "¿"}, {'{', "^"}, {'}', "*"}, {':', "¨"}, + {'"', "ç"}, {'<', ";"}, {'>', ":"}, {'?', "_"} + }; + + static const HashMap spanishAltGrMap = { + {'`', "\\"}, {'1', "|"}, {'2', "@"}, {'3', "#"}, {'4', "~"}, + {'6', "¬"}, {'e', "€"}, {';', "{"}, {'\'', "}"} + }; + + mCharMap = spanishCharMap; + mAltGrCharMap = spanishAltGrMap; +} + +void KeyboardLayoutMapper::AddItalianLayout() +{ + static const HashMap italianCharMap = { + {'`', "\\"}, {'-', "'"}, {'=', "ì"}, {'~', "|"}, {'_', "?"}, + {'+', "^"}, {'@', "\""}, {'#', "£"}, {'^', "&"}, {'&', "/"}, + {'*', "("}, {'(', ")"}, {')', "="}, {'[', "è"}, {']', "+"}, + {'{', "é"}, {'}', "*"}, {';', "ò"}, {':', "ç"}, {'\'', "à"}, + {'"', "°"}, {'\\', "ù"}, {'|', "§"}, {'/', "-"}, {'?', "_"} + }; + + static const HashMap italianAltGrCharMap = { + {'[', "["}, {']', "]"}, {';', "@"}, {'\'', "#"}, + {'\\', "{"}, {'=', "}"}, {'e', "€"} + }; + + mCharMap = italianCharMap; + mAltGrCharMap = italianAltGrCharMap; +} + +void KeyboardLayoutMapper::AddDanishLayout() +{ + static const HashMap danishCharMap = { + {'-', "+"}, {'=', "´"},{'_', "?"}, {'+', "`"},{'[', "å"}, + {']', "¨"}, {'{', "Å"}, {'}', "^"},{';', "æ"}, {':', "Æ"}, + {'\'', "ø"}, {'"', "Ø"},{'\\', "'"}, {'|', "*"},{'/', "-"}, + {'?', "_"} + }; + + static const HashMap danishAltGrMap = { + {'2', "@"}, {'3', "£"}, {'4', "$"}, {'5', "€"},{'7', "{"}, + {'8', "["}, {'9', "]"}, {'0', "}"},{'[', "\\"}, {']', "~"}, + {'e', "€"} + }; + + mCharMap = danishCharMap; + mAltGrCharMap = danishAltGrMap; +} + +void KeyboardLayoutMapper::AddUKLayout() +{ + static const HashMap UKCharMap = { + {'~', "¬"}, {'@', "\""}, {'#', "£"}, {'"', "@"} + }; + + static const HashMap UKAltGrCharMap = {}; + + mCharMap = UKCharMap; + mAltGrCharMap = UKAltGrCharMap; +} \ No newline at end of file diff --git a/projects/frontend/es-core/src/input/KeyboardLayoutMapper.h b/projects/frontend/es-core/src/input/KeyboardLayoutMapper.h new file mode 100644 index 0000000000..b200694874 --- /dev/null +++ b/projects/frontend/es-core/src/input/KeyboardLayoutMapper.h @@ -0,0 +1,103 @@ +// +// Created by Pit64 on 11/12/2025. +// +#pragma once + +#include +#include +#include + +/*! + * @brief Keyboard layout mapper for handling different keyboard layouts (AZERTY, QWERTZ, etc.) + * + * This class provides generic keyboard layout mapping capabilities to support + * international keyboards. It maps physical key scancodes to logical keycodes + * and remaps text input characters based on the configured layout. + */ +class KeyboardLayoutMapper +{ +public: + /*! + * @brief Initialize the mapper with a specific keyboard layout + * @param layout Layout identifier (e.g., "fr", "de", "uk", "us") + */ + explicit KeyboardLayoutMapper(const String& layout); + + /*! + * @brief Map a scancode to the appropriate keycode for the current layout + * @param scancode Physical key scancode + * @return Logical keycode for the configured layout + */ + SDL_Keycode MapScancode(SDL_Scancode scancode) const; + + /*! + * @brief Remap text input characters for the current layout + * @param text Input text (assumed to be US QWERTY) + * @param altGrPressed Whether the AltGr (Right Alt) key is pressed + * @return Remapped text for the actual layout + */ + String RemapText(const char* text, bool altGrPressed) const; + + /*! + * @brief Get the current keyboard layout identifier + * @return Layout identifier string + */ + const String& GetLayout() const { return mLayout; } + + /*! + * @brief Check if using native SDL mapping (X11/Wayland) + * @return True if using native SDL, false if custom remapping is needed + */ + bool IsUsingNativeSDL() const { return mUsingNativeSDL; } + +private: + //! Current keyboard layout + String mLayout; + + //! Whether we're using native SDL mapping (X11/Wayland) or custom remapping + bool mUsingNativeSDL; + + //! Scancode to keycode mapping for the current layout + HashMap mScancodeMap; + + //! Character remapping for text input + HashMap mCharMap; + + //! Character remapping for text input with AltGr pressed + HashMap mAltGrCharMap; + + /*! + * @brief Initialize the layout-specific mappings + */ + void InitializeMappings(); + + /*! + * @brief Add French AZERTY layout mappings + */ + void AddFrenchLayout(); + + /*! + * @brief Add German QWERTZ layout mappings + */ + void AddGermanLayout(); + + /*! + * @brief Add Spanish layout mappings + */ + void AddSpanishLayout(); + + /*! + * @brief Add Italian layout mappings + */ + void AddItalianLayout(); + + /*! + * @brief Add Danish layout mappings + * */ + void AddDanishLayout(); + + /*! + * @brief Add UK QWERTY layout mappings + */ + void AddUKLayout(); +}; \ No newline at end of file -- GitLab