diff --git a/irr/include/IrrlichtDevice.h b/irr/include/IrrlichtDevice.h index edc6ead61..717989978 100644 --- a/irr/include/IrrlichtDevice.h +++ b/irr/include/IrrlichtDevice.h @@ -342,6 +342,26 @@ public: { return video::isDriverSupported(driver); } + + // This is a trivial (near-identity) mapping for converting between scancodes and keycodes for devices that do + // not implement this. + + //! Get the scancode of the corresponding keycode. + virtual u32 getScancodeFromKey(const KeyCode &key) const + { + return key.index() == 0 ? std::get(key) : KEY_KEY_CODES_COUNT + std::get(key); + } + + //! Get the keycode of the corresponding scancode. + virtual KeyCode getKeyFromScancode(const u32 scancode) const + { + KeyCode key; + if (scancode < KEY_KEY_CODES_COUNT) + key.emplace((EKEY_CODE)scancode); + else + key.emplace(scancode - KEY_KEY_CODES_COUNT); + return key; + } }; } // end namespace irr diff --git a/irr/include/Keycodes.h b/irr/include/Keycodes.h index cdc90d198..832547cf3 100644 --- a/irr/include/Keycodes.h +++ b/irr/include/Keycodes.h @@ -3,6 +3,7 @@ // For conditions of distribution and use, see copyright notice in irrlicht.h #pragma once +#include namespace irr { @@ -182,4 +183,29 @@ enum EKEY_CODE KEY_KEY_CODES_COUNT = 0x100 // this is not a key, but the amount of keycodes there are. }; +class KeyCode : public std::variant { + using super = std::variant; +public: + KeyCode() : KeyCode(KEY_KEY_CODES_COUNT, L'\0') {} + + KeyCode(EKEY_CODE code, wchar_t ch) + { + emplace(code, ch); + } + + using super::emplace; + void emplace(EKEY_CODE code, wchar_t ch) + { + if (isValid(code)) + emplace(code); + else + emplace(ch); + } + + static bool isValid(EKEY_CODE code) + { + return code > 0 && code < KEY_KEY_CODES_COUNT; + } +}; + } // end namespace irr diff --git a/irr/src/CIrrDeviceSDL.cpp b/irr/src/CIrrDeviceSDL.cpp index 05663be99..20e41a527 100644 --- a/irr/src/CIrrDeviceSDL.cpp +++ b/irr/src/CIrrDeviceSDL.cpp @@ -220,6 +220,31 @@ int CIrrDeviceSDL::findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtK } } +u32 CIrrDeviceSDL::getScancodeFromKey(const KeyCode &key) const +{ + u32 keynum = 0; + if (key.index() == 0) { + auto keycode = std::get(key); + for (const auto &entry: KeyMap) { + if (entry.second == keycode) { + keynum = entry.first; + break; + } + } + } else { + keynum = std::get(key); + } + return SDL_GetScancodeFromKey(keynum); +} + +KeyCode CIrrDeviceSDL::getKeyFromScancode(const u32 scancode) const +{ + auto keycode = SDL_GetKeyFromScancode((SDL_Scancode)scancode); + const auto &keyentry = KeyMap.find(keycode); + auto irrcode = keyentry != KeyMap.end() ? keyentry->second : KEY_UNKNOWN; + return KeyCode(irrcode, keycode); +} + void CIrrDeviceSDL::resetReceiveTextInputEvents() { gui::IGUIElement *elem = GUIEnvironment->getFocus(); @@ -815,6 +840,21 @@ bool CIrrDeviceSDL::run() case SDL_KEYDOWN: case SDL_KEYUP: { auto keysym = SDL_event.key.keysym.sym; + auto scancode = SDL_event.key.keysym.scancode; + + // Treat AC_BACK as the Escape key + if (scancode == SDL_SCANCODE_AC_BACK || scancode == SDL_SCANCODE_ESCAPE) + { + if (SDL_event.type == SDL_KEYDOWN) + escapeKeys.insert(scancode); + else + escapeKeys.erase(scancode); + if (SDL_event.type == SDL_KEYUP && !escapeKeys.empty()) + break; // avoid sending KEYUP twice if AC_BACK and ESCAPE are both released + scancode = SDL_SCANCODE_ESCAPE; + keysym = SDLK_ESCAPE; + } + const auto &entry = KeyMap.find(keysym); auto key = entry == KeyMap.end() ? KEY_UNKNOWN : entry->second; @@ -832,6 +872,8 @@ bool CIrrDeviceSDL::run() irrevent.KeyInput.Control = (SDL_event.key.keysym.mod & KMOD_CTRL) != 0; irrevent.KeyInput.Char = findCharToPassToIrrlicht(keysym, key, (SDL_event.key.keysym.mod & KMOD_NUM) != 0); + irrevent.KeyInput.SystemKeyCode = scancode; + postEventFromUser(irrevent); } break; @@ -1298,9 +1340,6 @@ void CIrrDeviceSDL::createKeyMap() // buttons missing - // Android back button = ESC - KeyMap.emplace(SDLK_AC_BACK, KEY_ESCAPE); - KeyMap.emplace(SDLK_BACKSPACE, KEY_BACK); KeyMap.emplace(SDLK_TAB, KEY_TAB); KeyMap.emplace(SDLK_CLEAR, KEY_CLEAR); diff --git a/irr/src/CIrrDeviceSDL.h b/irr/src/CIrrDeviceSDL.h index 74d47e737..0b5c5afa8 100644 --- a/irr/src/CIrrDeviceSDL.h +++ b/irr/src/CIrrDeviceSDL.h @@ -24,6 +24,8 @@ #include #include +#include +#include namespace irr { @@ -286,6 +288,9 @@ private: // Return the Char that should be sent to Irrlicht for the given key (either the one passed in or 0). static int findCharToPassToIrrlicht(uint32_t sdlKey, EKEY_CODE irrlichtKey, bool numlock); + virtual u32 getScancodeFromKey(const KeyCode &key) const override; + virtual KeyCode getKeyFromScancode(const u32 scancode) const override; + // Check if a text box is in focus. Enable or disable SDL_TEXTINPUT events only if in focus. void resetReceiveTextInputEvents(); @@ -324,6 +329,8 @@ private: s32 CurrentTouchCount; bool IsInBackground; + + std::unordered_set escapeKeys; }; } // end namespace irr diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index 88969f008..a96292970 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -143,7 +143,7 @@ bool MyEventReceiver::OnEvent(const SEvent &event) // Remember whether each key is down or up if (event.EventType == irr::EET_KEY_INPUT_EVENT) { const KeyPress keyCode(event.KeyInput); - if (keysListenedFor[keyCode]) { + if (keyCode && keysListenedFor[keyCode]) { if (event.KeyInput.PressedDown) { if (!IsKeyDown(keyCode)) keyWasPressed.set(keyCode); diff --git a/src/client/keycode.cpp b/src/client/keycode.cpp index 0a3b0db3f..927486b05 100644 --- a/src/client/keycode.cpp +++ b/src/client/keycode.cpp @@ -6,15 +6,17 @@ #include "settings.h" #include "log.h" #include "debug.h" +#include "renderingengine.h" #include "util/hex.h" #include "util/string.h" #include "util/basic_macros.h" +#include struct table_key { - const char *Name; + std::string Name; irr::EKEY_CODE Key; wchar_t Char; // L'\0' means no character assigned - const char *LangName; // NULL means it doesn't have a human description + std::string LangName; // NULL means it doesn't have a human description }; #define DEFINEKEY1(x, lang) /* Irrlicht key without character */ \ @@ -30,7 +32,7 @@ struct table_key { #define N_(text) text -static const struct table_key table[] = { +static std::vector table = { // Keys that can be reliably mapped between Char and Key DEFINEKEY3(0) DEFINEKEY3(1) @@ -126,7 +128,7 @@ static const struct table_key table[] = { DEFINEKEY1(KEY_ADD, N_("Numpad +")) DEFINEKEY1(KEY_SEPARATOR, N_("Numpad .")) DEFINEKEY1(KEY_SUBTRACT, N_("Numpad -")) - DEFINEKEY1(KEY_DECIMAL, NULL) + DEFINEKEY1(KEY_DECIMAL, N_("Numpad .")) DEFINEKEY1(KEY_DIVIDE, N_("Numpad /")) DEFINEKEY4(1) DEFINEKEY4(2) @@ -221,33 +223,16 @@ static const struct table_key table[] = { DEFINEKEY5("_") }; +static const table_key invalid_key = {"", irr::KEY_UNKNOWN, L'\0', ""}; + #undef N_ -static const table_key &lookup_keyname(const char *name) -{ - for (const auto &table_key : table) { - if (strcmp(table_key.Name, name) == 0) - return table_key; - } - - throw UnknownKeycode(name); -} - -static const table_key &lookup_keykey(irr::EKEY_CODE key) -{ - for (const auto &table_key : table) { - if (table_key.Key == key) - return table_key; - } - - std::ostringstream os; - os << ""; - throw UnknownKeycode(os.str().c_str()); -} - static const table_key &lookup_keychar(wchar_t Char) { + if (Char == L'\0') + return invalid_key; + for (const auto &table_key : table) { if (table_key.Char == Char) return table_key; @@ -255,88 +240,73 @@ static const table_key &lookup_keychar(wchar_t Char) std::ostringstream os; os << ""; - throw UnknownKeycode(os.str().c_str()); + auto newsym = wide_to_utf8(std::wstring_view(&Char, 1)); + table_key new_key = {newsym, irr::KEY_KEY_CODES_COUNT, Char, newsym}; + return table.emplace_back(std::move(new_key)); } -KeyPress::KeyPress(const char *name) +static const table_key &lookup_keyname(const std::string_view &name) { - if (strlen(name) == 0) { - Key = irr::KEY_KEY_CODES_COUNT; - Char = L'\0'; - m_name = ""; - return; + if (name.empty()) + return invalid_key; + + for (const auto &table_key : table) { + if (table_key.Name == name) + return table_key; } - if (strlen(name) <= 4) { - // Lookup by resulting character - int chars_read = mbtowc(&Char, name, 1); - FATAL_ERROR_IF(chars_read != 1, "Unexpected multibyte character"); - try { - auto &k = lookup_keychar(Char); - m_name = k.Name; - Key = k.Key; - return; - } catch (UnknownKeycode &e) {}; - } else { - // Lookup by name - m_name = name; - try { - auto &k = lookup_keyname(name); - Key = k.Key; - Char = k.Char; - return; - } catch (UnknownKeycode &e) {}; + auto wname = utf8_to_wide(name); + if (wname.empty()) + return invalid_key; + return lookup_keychar(wname[0]); +} + +static const table_key &lookup_keykey(irr::EKEY_CODE key) +{ + if (!KeyCode::isValid(key)) + return invalid_key; + + for (const auto &table_key : table) { + if (table_key.Key == key) + return table_key; } - // It's not a known key, complain and try to do something - Key = irr::KEY_KEY_CODES_COUNT; - int chars_read = mbtowc(&Char, name, 1); - FATAL_ERROR_IF(chars_read != 1, "Unexpected multibyte character"); - m_name = ""; - warningstream << "KeyPress: Unknown key '" << name - << "', falling back to first char." << std::endl; + std::ostringstream os; + os << ""; + return invalid_key; } -KeyPress::KeyPress(const irr::SEvent::SKeyInput &in, bool prefer_character) +static const table_key &lookup_scancode(const u32 scancode) { - if (prefer_character) - Key = irr::KEY_KEY_CODES_COUNT; - else - Key = in.Key; - Char = in.Char; - - try { - if (valid_kcode(Key)) - m_name = lookup_keykey(Key).Name; - else - m_name = lookup_keychar(Char).Name; - } catch (UnknownKeycode &e) { - m_name.clear(); - }; + auto key = RenderingEngine::get_raw_device()->getKeyFromScancode(scancode); + return key.index() == 0 ? lookup_keykey(std::get(key)) : lookup_keychar(std::get(key)); } -const char *KeyPress::sym() const +KeyPress::KeyPress(const std::string_view &name) { - return m_name.c_str(); + const auto &key = lookup_keyname(name); + KeyCode keycode(key.Key, key.Char); + scancode = RenderingEngine::get_raw_device()->getScancodeFromKey(keycode); } -const char *KeyPress::name() const +std::string KeyPress::sym() const { - if (m_name.empty()) - return ""; - const char *ret; - if (valid_kcode(Key)) - ret = lookup_keykey(Key).LangName; - else - ret = lookup_keychar(Char).LangName; - return ret ? ret : ""; + return lookup_scancode(scancode).Name; } -const KeyPress EscapeKey("KEY_ESCAPE"); +std::string KeyPress::name() const +{ + return lookup_scancode(scancode).LangName; +} -const KeyPress LMBKey("KEY_LBUTTON"); -const KeyPress MMBKey("KEY_MBUTTON"); -const KeyPress RMBKey("KEY_RBUTTON"); +std::unordered_map KeyPress::specialKeyCache; +const KeyPress &KeyPress::getSpecialKey(const std::string &name) +{ + auto &key = specialKeyCache[name]; + if (!key) + key = KeyPress(name); + return key; +} /* Key config @@ -352,7 +322,7 @@ const KeyPress &getKeySetting(const char *settingname) return n->second; auto &ref = g_key_setting_cache[settingname]; - ref = g_settings->get(settingname).c_str(); + ref = std::string_view(g_settings->get(settingname)); return ref; } diff --git a/src/client/keycode.h b/src/client/keycode.h index 4c63be7fa..fee9e5ddd 100644 --- a/src/client/keycode.h +++ b/src/client/keycode.h @@ -4,57 +4,49 @@ #pragma once -#include "exceptions.h" #include "irrlichttypes.h" #include #include #include +#include -class UnknownKeycode : public BaseException -{ -public: - UnknownKeycode(const char *s) : - BaseException(s) {}; -}; - -/* A key press, consisting of either an Irrlicht keycode - or an actual char */ - +/* A key press, consisting of a scancode or a keycode */ class KeyPress { public: - KeyPress() = default; + KeyPress() {}; - KeyPress(const char *name); + KeyPress(const std::string_view &name); - KeyPress(const irr::SEvent::SKeyInput &in, bool prefer_character = false); + KeyPress(const irr::SEvent::SKeyInput &in) : + scancode(in.SystemKeyCode) {}; - bool operator==(const KeyPress &o) const - { - return (Char > 0 && Char == o.Char) || (valid_kcode(Key) && Key == o.Key); + std::string sym() const; + std::string name() const; + + bool operator==(const KeyPress &o) const { + return scancode == o.scancode; } - const char *sym() const; - const char *name() const; - -protected: - static bool valid_kcode(irr::EKEY_CODE k) - { - return k > 0 && k < irr::KEY_KEY_CODES_COUNT; + operator bool() const { + return scancode != 0; } - irr::EKEY_CODE Key = irr::KEY_KEY_CODES_COUNT; - wchar_t Char = L'\0'; - std::string m_name = ""; + static const KeyPress &getSpecialKey(const std::string &name); + +private: + u32 scancode = 0; + + static std::unordered_map specialKeyCache; }; // Global defines for convenience - -extern const KeyPress EscapeKey; - -extern const KeyPress LMBKey; -extern const KeyPress MMBKey; // Middle Mouse Button -extern const KeyPress RMBKey; +// This implementation defers creation of the objects to make sure that the +// IrrlichtDevice is initialized. +#define EscapeKey KeyPress::getSpecialKey("KEY_ESCAPE") +#define LMBKey KeyPress::getSpecialKey("KEY_LBUTTON") +#define MMBKey KeyPress::getSpecialKey("KEY_MBUTTON") // Middle Mouse Button +#define RMBKey KeyPress::getSpecialKey("KEY_RBUTTON") // Key configuration getter const KeyPress &getKeySetting(const char *settingname); diff --git a/src/gui/guiKeyChangeMenu.cpp b/src/gui/guiKeyChangeMenu.cpp index 6d3a0c531..4cbaf29fd 100644 --- a/src/gui/guiKeyChangeMenu.cpp +++ b/src/gui/guiKeyChangeMenu.cpp @@ -253,8 +253,7 @@ bool GUIKeyChangeMenu::OnEvent(const SEvent& event) if (event.EventType == EET_KEY_INPUT_EVENT && active_key && event.KeyInput.PressedDown) { - bool prefer_character = shift_down; - KeyPress kp(event.KeyInput, prefer_character); + KeyPress kp(event.KeyInput); if (event.KeyInput.Key == irr::KEY_DELETE) kp = KeyPress(""); // To erase key settings @@ -270,7 +269,7 @@ bool GUIKeyChangeMenu::OnEvent(const SEvent& event) // Display Key already in use message bool key_in_use = false; - if (strcmp(kp.sym(), "") != 0) { + if (kp) { for (key_setting *ks : key_settings) { if (ks != active_key && ks->key == kp) { key_in_use = true; diff --git a/src/gui/touchcontrols.cpp b/src/gui/touchcontrols.cpp index 6ba3e7b9e..34c0091c6 100644 --- a/src/gui/touchcontrols.cpp +++ b/src/gui/touchcontrols.cpp @@ -190,13 +190,7 @@ static EKEY_CODE id_to_keycode(touch_gui_button_id id) } assert(!key.empty()); std::string resolved = g_settings->get("keymap_" + key); - try { - code = keyname_to_keycode(resolved.c_str()); - } catch (UnknownKeycode &e) { - code = KEY_UNKNOWN; - warningstream << "TouchControls: Unknown key '" << resolved - << "' for '" << key << "', hiding button." << std::endl; - } + code = keyname_to_keycode(resolved.c_str()); return code; } diff --git a/src/unittest/test_keycode.cpp b/src/unittest/test_keycode.cpp index 125098341..edc5bc7b4 100644 --- a/src/unittest/test_keycode.cpp +++ b/src/unittest/test_keycode.cpp @@ -7,6 +7,7 @@ #include #include "exceptions.h" #include "client/keycode.h" +#include "client/renderingengine.h" // scancode<->keycode conversion class TestKeycode : public TestBase { public: @@ -24,42 +25,56 @@ static TestKeycode g_test_instance; void TestKeycode::runTests(IGameDef *gamedef) { + // TODO: How do we test this without an IrrlichtDevice? +#if 0 TEST(testCreateFromString); TEST(testCreateFromSKeyInput); TEST(testCompare); +#endif } //////////////////////////////////////////////////////////////////////////////// -#define UASSERTEQ_STR(one, two) UASSERT(strcmp(one, two) == 0) +#define UASSERTEQ_STR(one, two) UASSERT(one == two) +#define UASSERT_HAS_NAME(k) UASSERT(!k.name().empty()) void TestKeycode::testCreateFromString() { KeyPress k; + k = KeyPress(""); + UASSERTEQ_STR(k.sym(), ""); + UASSERTEQ_STR(k.name(), ""); + // Character key, from char k = KeyPress("R"); UASSERTEQ_STR(k.sym(), "KEY_KEY_R"); - UASSERTCMP(int, >, strlen(k.name()), 0); // should have human description + UASSERT_HAS_NAME(k); // Character key, from identifier k = KeyPress("KEY_KEY_B"); UASSERTEQ_STR(k.sym(), "KEY_KEY_B"); - UASSERTCMP(int, >, strlen(k.name()), 0); + UASSERT_HAS_NAME(k); // Non-Character key, from identifier k = KeyPress("KEY_UP"); UASSERTEQ_STR(k.sym(), "KEY_UP"); - UASSERTCMP(int, >, strlen(k.name()), 0); + UASSERT_HAS_NAME(k); k = KeyPress("KEY_F6"); UASSERTEQ_STR(k.sym(), "KEY_F6"); - UASSERTCMP(int, >, strlen(k.name()), 0); + UASSERT_HAS_NAME(k); // Irrlicht-unknown key, from char k = KeyPress("/"); UASSERTEQ_STR(k.sym(), "/"); - UASSERTCMP(int, >, strlen(k.name()), 0); + UASSERT_HAS_NAME(k); +} + +template +static u32 toScancode(Args... args) +{ + return RenderingEngine::get_raw_device()->getScancodeFromKey(KeyCode(args...)); } void TestKeycode::testCreateFromSKeyInput() @@ -68,47 +83,46 @@ void TestKeycode::testCreateFromSKeyInput() irr::SEvent::SKeyInput in; // Character key - in.Key = irr::KEY_KEY_3; - in.Char = L'3'; + in.SystemKeyCode = toScancode(irr::KEY_KEY_3, L'3'); k = KeyPress(in); UASSERTEQ_STR(k.sym(), "KEY_KEY_3"); + UASSERT_HAS_NAME(k); // Non-Character key - in.Key = irr::KEY_RSHIFT; - in.Char = L'\0'; + in.SystemKeyCode = toScancode(irr::KEY_RSHIFT, L'\0'); k = KeyPress(in); UASSERTEQ_STR(k.sym(), "KEY_RSHIFT"); + UASSERT_HAS_NAME(k); // Irrlicht-unknown key - in.Key = irr::KEY_KEY_CODES_COUNT; - in.Char = L'?'; + in.SystemKeyCode = toScancode(KEY_KEY_CODES_COUNT, L'?'); k = KeyPress(in); UASSERTEQ_STR(k.sym(), "?"); - - // prefer_character mode - in.Key = irr::KEY_COMMA; - in.Char = L'G'; - k = KeyPress(in, true); - UASSERTEQ_STR(k.sym(), "KEY_KEY_G"); + UASSERT_HAS_NAME(k); } void TestKeycode::testCompare() { + // "Empty" key + UASSERT(KeyPress() == KeyPress("")); + // Basic comparison UASSERT(KeyPress("5") == KeyPress("KEY_KEY_5")); UASSERT(!(KeyPress("5") == KeyPress("KEY_NUMPAD5"))); // Matching char suffices // note: This is a real-world example, Irrlicht maps XK_equal to irr::KEY_PLUS on Linux + // TODO: Is this still relevant for scancodes? irr::SEvent::SKeyInput in; + /* in.Key = irr::KEY_PLUS; in.Char = L'='; UASSERT(KeyPress("=") == KeyPress(in)); + */ // Matching keycode suffices irr::SEvent::SKeyInput in2; - in.Key = in2.Key = irr::KEY_OEM_CLEAR; - in.Char = L'\0'; - in2.Char = L';'; + in.SystemKeyCode = toScancode(irr::KEY_OEM_CLEAR, L'\0'); + in2.SystemKeyCode = toScancode(irr::KEY_OEM_CLEAR, L';'); UASSERT(KeyPress(in) == KeyPress(in2)); }