diff --git a/builtin/common/settings/components.lua b/builtin/common/settings/components.lua index 38af1b0862..00bd1bfee4 100644 --- a/builtin/common/settings/components.lua +++ b/builtin/common/settings/components.lua @@ -432,34 +432,21 @@ end function make.key(setting) local btn_bind = "bind_" .. setting.name - local btn_clear = "unbind_" .. setting.name + local btn_edit = "edit_" .. setting.name local function add_conflict_warnings(fs, height) - local value = core.settings:get(setting.name) + local value = core.settings:get(setting.name):split("|") if value == "" then return height end - local critical_keys = { - keymap_drop = true, - keymap_dig = true, - keymap_place = true, - } - for _, o in ipairs(core.full_settingtypes) do if o.type == "key" and o.name ~= setting.name and - core.are_keycodes_equal(core.settings:get(o.name), value) then + is_keybinding_critical(setting.name, o.name) and + has_keybinding_conflict(core.settings:get(o.name):split("|"), value) then - local is_current_close_world = setting.name == "keymap_close_world" - local is_other_close_world = o.name == "keymap_close_world" - local is_current_critical = critical_keys[setting.name] - local is_other_critical = critical_keys[o.name] - - if (is_other_critical or is_current_critical) or - (not is_current_close_world and not is_other_close_world) then - table.insert(fs, ("label[0,%f;%s]"):format(height + 0.3, - core.colorize(mt_color_orange, fgettext([[Conflicts with "$1"]], fgettext(o.readable_name))))) - height = height + 0.6 - end + table.insert(fs, ("label[0,%f;%s]"):format(height + 0.3, + core.colorize(mt_color_orange, fgettext([[Conflicts with "$1"]], fgettext(o.readable_name))))) + height = height + 0.6 end end return height @@ -471,30 +458,32 @@ function make.key(setting) get_formspec = function(self, avail_w) self.resettable = core.settings:has(setting.name) - local btn_bind_width = math.max(2.5, avail_w / 2) - local value = core.settings:get(setting.name) + local btn_width = math.max(2.5, avail_w / 2) + local value = core.settings:get(setting.name):split("|") local fs = { ("label[0,0.4;%s]"):format(get_label(setting)), - ("button_key[%f,0;%f,0.8;%s;%s]"):format( - btn_bind_width, btn_bind_width - 0.8, - btn_bind, core.formspec_escape(value)), - ("image_button[%f,0;0.8,0.8;%s;%s;]"):format(avail_w - 0.8, - core.formspec_escape(defaulttexturedir .. "clear.png"), - btn_clear), - ("tooltip[%s;%s]"):format(btn_clear, fgettext("Remove keybinding")), + ("tooltip[%s;%s]"):format(btn_edit, fgettext("Edit keybindings")), } + if #value <= 1 then + table.insert(fs, ("button_key[%f,0;%f,0.8;%s;%s]"):format( + btn_width, btn_width-0.8, btn_bind, value[1] or "")) + table.insert(fs, ("image_button[%f,0;0.8,0.8;%s;%s;]"):format(avail_w - 0.8, + core.formspec_escape(defaulttexturedir.."overflow_btn.png"), btn_edit)) + else + table.insert(fs, ("button[%f,0;%f,0.8;%s;%s]"):format( + btn_width, btn_width, btn_edit, fgettext("Edit"))) + end local height = 0.8 height = add_conflict_warnings(fs, height) return table.concat(fs), height end, - on_submit = function(self, fields) + on_submit = function(self, fields, tabview) if fields[btn_bind] then core.settings:set(setting.name, fields[btn_bind]) return true - elseif fields[btn_clear] then - core.settings:set(setting.name, "") - return true + elseif fields[btn_edit] then + return show_change_keybinding_dlg(setting, tabview) end end, } diff --git a/builtin/common/settings/dlg_change_keybinding.lua b/builtin/common/settings/dlg_change_keybinding.lua new file mode 100644 index 0000000000..4a97b9f8b9 --- /dev/null +++ b/builtin/common/settings/dlg_change_keybinding.lua @@ -0,0 +1,173 @@ +-- Luanti +-- SPDX-License-Identifier: LGPL-2.1-or-later +local function get_formspec(dialogdata) + local name = dialogdata.setting.name + local readable_name = name + if dialogdata.setting.readable_name then + readable_name = ("%s (%s)"):format(fgettext(dialogdata.setting.readable_name), name) + end + + local value = dialogdata.value + local selection = dialogdata.selection + + local fs = { + "formspec_version[4]", + "size[6,9]", + ("label[0.5,0.8;%s]"):format(readable_name), + ("button[0.5,5.7;5,0.8;btn_add;%s]"):format(fgettext("Add keybinding")), + ("button_key[0.5,6.7;4.2,0.8;btn_bind;%s]"):format(core.formspec_escape(value[selection] or "")), + ("image_button[4.7,6.7;0.8,0.8;%s;btn_clear;]"):format( + core.formspec_escape(defaulttexturedir .. "clear.png")), + ("tooltip[btn_clear;%s]"):format(fgettext("Remove keybinding")), + ("button[3.1,7.7;2.4,0.8;btn_close;%s]"):format(fgettext("Cancel")), + ("button[0.5,7.7;2.4,0.8;btn_save;%s]"):format(fgettext("Save")), + } + + local warning = "" + local cells = {} + for idx, key in ipairs(value) do + local prefix = "" + for _, o in ipairs(core.full_settingtypes) do + if o.type == "key" and o.name ~= name and + is_keybinding_critical(name, o.name) and + has_keybinding_conflict(core.settings:get(o.name):split("|"), key) then + prefix = mt_color_orange + if idx == selection then + warning = core.colorize(mt_color_orange, fgettext([[Conflicts with "$1"]], fgettext_ne(o.readable_name))) + end + break + end + end + table.insert(cells, core.formspec_escape(prefix .. core.get_keycode_name(key))) + end + table.insert(fs, ("textlist[0.5,1.3;5,3.8;keylist;%s;%d;false]"):format(table.concat(cells, ","), selection)) + table.insert(fs, ("label[0.5,5.4;%s]"):format(warning)) + + return table.concat(fs) +end + +local function buttonhandler(self, fields) + local name = self.data.setting.name + if fields.quit or fields.btn_close then + self:delete() + return true + elseif fields.btn_save then + local value = {} + for _, v in ipairs(self.data.value) do + if v ~= "" then -- filter out "empty" keybindings + table.insert(value, v) + end + end + core.settings:set(name, table.concat(value, "|")) + self:delete() + return true + elseif fields.btn_clear then + local selection = self.data.selection + table.remove(self.data.value, selection) + self.data.selection = math.max(1, math.min(selection, #self.data.value)) + return true + elseif fields.btn_add then + table.insert(self.data.value, "") + self.data.selection = #self.data.value + return true + elseif fields.btn_bind then + self.data.value[self.data.selection] = fields.btn_bind + return true + elseif fields.keylist then + local event = core.explode_textlist_event(fields.keylist) + if event.type == "CHG" or event.type == "DCL" then + self.data.selection = event.index + return true + end + end + return false +end + +local formspec_handlers = {} +if INIT == "pause_menu" then + core.register_on_formspec_input(function(formname, fields) + if formspec_handlers[formname] then + formspec_handlers[formname](fields) + return true + end + end) +end + +local is_mainmenu = INIT == "mainmenu" +function show_change_keybinding_dlg(setting, tabview) + local dlg + if is_mainmenu then + dlg = dialog_create("dlg_change_keybinding", + get_formspec, buttonhandler) + else + local name = "__builtin:rebind_" .. setting.name + dlg = { + show = function() + if dlg.removed then + --core.open_settings("controls_keyboard_and_mouse") + tabview:show() + else + core.show_formspec(name, get_formspec(dlg.data)) + end + end, + delete = function() + core.show_formspec(name, "") + formspec_handlers[name] = nil + dlg.removed = true + end, + data = {}, + } + formspec_handlers[name] = function(fields) + if buttonhandler(dlg, fields) then + dlg:show() + end + end + end + + dlg.data.setting = setting + dlg.data.value = core.settings:get(setting.name):split("|") + dlg.data.selection = 1 + + if is_mainmenu then + dlg:set_parent(tabview) + tabview:hide() + end + dlg:show() + + return is_mainmenu +end + +function has_keybinding_conflict(t1, t2) -- not local as it is also used by the make.key component + if type(t2) == "string" then + if type(t1) == "string" then + return core.are_keycodes_equal(t1, t2) + else + for _, v1 in pairs(t1) do + if core.are_keycodes_equal(v1, t2) then + return true + end + end + end + else + for _, v2 in pairs(t2) do + if has_keybinding_conflict(t1, v2) then + return true + end + end + end + return false +end + +local critical_keys = { + keymap_drop = true, + keymap_dig = true, + keymap_place = true, +} +function is_keybinding_critical(n1, n2) + local is_current_close_world = n1 == "keymap_close_world" + local is_other_close_world = n2 == "keymap_close_world" + local is_current_critical = critical_keys[n1] + local is_other_critical = critical_keys[n2] + return (is_other_critical or is_current_critical) or + (not is_current_close_world and not is_other_close_world) +end diff --git a/builtin/common/settings/dlg_settings.lua b/builtin/common/settings/dlg_settings.lua index 77fc8be3f0..f92af751a7 100644 --- a/builtin/common/settings/dlg_settings.lua +++ b/builtin/common/settings/dlg_settings.lua @@ -814,7 +814,9 @@ else -- case it's a no-op core.show_formspec("__builtin:settings", "") end - - core.show_formspec("__builtin:settings", get_formspec(dialog.data)) + dialog.show = function() -- Used by the keybinding form + core.show_formspec("__builtin:settings", get_formspec(dialog.data)) + end + dialog:show() end end diff --git a/builtin/common/settings/init.lua b/builtin/common/settings/init.lua index 71a95424ba..d61d074921 100644 --- a/builtin/common/settings/init.lua +++ b/builtin/common/settings/init.lua @@ -6,6 +6,7 @@ local path = core.get_builtin_path() .. "common" .. DIR_DELIM .. "settings" .. D dofile(path .. "settingtypes.lua") dofile(path .. "dlg_change_mapgen_flags.lua") +dofile(path .. "dlg_change_keybinding.lua") dofile(path .. "dlg_settings.lua") -- Uncomment to generate 'minetest.conf.example' and 'settings_translation_file.cpp'. diff --git a/builtin/mainmenu/dlg_rebind_keys.lua b/builtin/mainmenu/dlg_rebind_keys.lua index ec4d1357fd..a1e033531d 100644 --- a/builtin/mainmenu/dlg_rebind_keys.lua +++ b/builtin/mainmenu/dlg_rebind_keys.lua @@ -74,6 +74,17 @@ local function create_rebind_keys_dlg() return dlg end +local function normalize_key_setting(str) + if str == "|" then + return core.normalize_keycode(str) + end + local t = string.split(str, "|") + for k, v in pairs(t) do + t[k] = core.normalize_keycode(v) + end + return table.concat(t, "|") +end + function migrate_keybindings(parent) -- Show migration dialog if the user upgraded from an earlier version -- and this has not yet been shown before, *or* if keys settings had to be changed @@ -86,7 +97,7 @@ function migrate_keybindings(parent) local settings = core.settings:to_table() for name, value in pairs(settings) do if name:match("^keymap_") then - local normalized = core.normalize_keycode(value) + local normalized = normalize_key_setting(value) if value ~= normalized then has_migration = true core.settings:set(name, normalized) diff --git a/irr/include/IEventReceiver.h b/irr/include/IEventReceiver.h index dc37aa766a..b070860dce 100644 --- a/irr/include/IEventReceiver.h +++ b/irr/include/IEventReceiver.h @@ -86,6 +86,17 @@ enum EEVENT_TYPE }; +//! Enumeration for "user" event types. +enum UserEventType: s32 +{ + //! In-game touch buttons. + /** This event is currently only fired by the in-game touch controller. + UserData1: The GameKeyType of the button. + UserData2: Whether the button is pressed or released. + */ + USER_EVENT_GAME_KEY = 1 +}; + //! Enumeration for all mouse input events enum EMOUSE_INPUT_EVENT { @@ -510,6 +521,9 @@ struct SEvent //! Any kind of user event. struct SUserEvent { + //! Event code + s32 code; + //! Some user specified data as int size_t UserData1; diff --git a/irr/src/CIrrDeviceLinux.cpp b/irr/src/CIrrDeviceLinux.cpp index 3ec9e6481f..20c2000b66 100644 --- a/irr/src/CIrrDeviceLinux.cpp +++ b/irr/src/CIrrDeviceLinux.cpp @@ -912,6 +912,7 @@ bool CIrrDeviceLinux::run() } else { // we assume it's a user message irrevent.EventType = EET_USER_EVENT; + irrevent.UserEvent.code = 0; irrevent.UserEvent.UserData1 = static_cast(event.xclient.data.l[0]); irrevent.UserEvent.UserData2 = static_cast(event.xclient.data.l[1]); postEventFromUser(irrevent); diff --git a/irr/src/CIrrDeviceSDL.cpp b/irr/src/CIrrDeviceSDL.cpp index a44213d7cc..23a449f3eb 100644 --- a/irr/src/CIrrDeviceSDL.cpp +++ b/irr/src/CIrrDeviceSDL.cpp @@ -945,6 +945,7 @@ bool CIrrDeviceSDL::run() case SDL_USEREVENT: irrevent.EventType = EET_USER_EVENT; + irrevent.UserEvent.code = SDL_event.user.code; irrevent.UserEvent.UserData1 = reinterpret_cast(SDL_event.user.data1); irrevent.UserEvent.UserData2 = reinterpret_cast(SDL_event.user.data2); diff --git a/irr/src/CIrrDeviceWin32.cpp b/irr/src/CIrrDeviceWin32.cpp index 63954de8ce..720cc848a6 100644 --- a/irr/src/CIrrDeviceWin32.cpp +++ b/irr/src/CIrrDeviceWin32.cpp @@ -675,6 +675,7 @@ LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam) case WM_USER: event.EventType = EET_USER_EVENT; + event.UserEvent.code = 0; event.UserEvent.UserData1 = static_cast(wParam); event.UserEvent.UserData2 = static_cast(lParam); dev = getDeviceFromHWnd(hWnd); diff --git a/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index 52e73356cb..e9f90c7820 100644 --- a/src/client/inputhandler.cpp +++ b/src/client/inputhandler.cpp @@ -26,7 +26,7 @@ void MyEventReceiver::reloadKeybindings() keybindings[KeyType::DIG] = getKeySetting("keymap_dig"); keybindings[KeyType::PLACE] = getKeySetting("keymap_place"); - keybindings[KeyType::ESC] = EscapeKey; + keybindings[KeyType::ESC] = std::vector{EscapeKey}; keybindings[KeyType::AUTOFORWARD] = getKeySetting("keymap_autoforward"); @@ -76,7 +76,11 @@ void MyEventReceiver::reloadKeybindings() // First clear all keys, then re-add the ones we listen for keysListenedFor.clear(); for (int i = 0; i < KeyType::INTERNAL_ENUM_COUNT; i++) { - listenForKey(keybindings[i], static_cast(i)); + GameKeyType game_key = static_cast(i); + keybindings[i].emplace_back(game_key); + for (auto key: keybindings[i]) { + listenForKey(key, game_key); + } } } @@ -85,13 +89,11 @@ bool MyEventReceiver::setKeyDown(KeyPress keyCode, bool is_down) if (keysListenedFor.find(keyCode) == keysListenedFor.end()) // ignore irrelevant key input return false; auto action = keysListenedFor[keyCode]; - if (is_down) { + if (is_down) physicalKeyDown.insert(keyCode); - setKeyDown(action, true); - } else { + else physicalKeyDown.erase(keyCode); - setKeyDown(action, false); - } + setKeyDown(action, checkKeyDown(action)); return true; } @@ -109,6 +111,15 @@ void MyEventReceiver::setKeyDown(GameKeyType action, bool is_down) } } +bool MyEventReceiver::checkKeyDown(GameKeyType action) +{ + for (auto key: keybindings[action]) { + if (physicalKeyDown.find(key) != physicalKeyDown.end()) + return true; + } + return false; +} + bool MyEventReceiver::OnEvent(const SEvent &event) { if (event.EventType == EET_LOG_TEXT_EVENT) { @@ -138,7 +149,7 @@ bool MyEventReceiver::OnEvent(const SEvent &event) if (event.EventType == EET_KEY_INPUT_EVENT) { KeyPress keyCode(event.KeyInput); - if (keyCode == getKeySetting("keymap_fullscreen")) { + if (keySettingHasMatch("keymap_fullscreen", keyCode)) { if (event.KeyInput.PressedDown && !fullscreen_is_down) { IrrlichtDevice *device = RenderingEngine::get_raw_device(); @@ -152,7 +163,7 @@ bool MyEventReceiver::OnEvent(const SEvent &event) fullscreen_is_down = event.KeyInput.PressedDown; return true; - } else if (keyCode == getKeySetting("keymap_close_world")) { + } else if (keySettingHasMatch("keymap_close_world", keyCode)) { close_world_down = event.KeyInput.PressedDown; } else if (keyCode == EscapeKey) { @@ -216,6 +227,10 @@ bool MyEventReceiver::OnEvent(const SEvent &event) default: break; } + } else if (event.EventType == EET_USER_EVENT && event.UserEvent.code == USER_EVENT_GAME_KEY) { + KeyPress keyCode(static_cast(event.UserEvent.UserData1)); + setKeyDown(keyCode, event.UserEvent.UserData2); + return true; } // tell Irrlicht to continue processing this event diff --git a/src/client/inputhandler.h b/src/client/inputhandler.h index ce2c41a637..e273f65a0e 100644 --- a/src/client/inputhandler.h +++ b/src/client/inputhandler.h @@ -97,13 +97,14 @@ private: bool setKeyDown(KeyPress keyCode, bool is_down); void setKeyDown(GameKeyType action, bool is_down); + bool checkKeyDown(GameKeyType action); /* This is faster than using getKeySetting with the tradeoff that functions * using it must make sure that it's initialised before using it and there is * no error handling (for example bounds checking). This is useful here as the * faster (up to 10x faster) key lookup is an asset. */ - std::array keybindings; + std::array, KeyType::INTERNAL_ENUM_COUNT> keybindings; s32 mouse_wheel = 0; diff --git a/src/client/keycode.cpp b/src/client/keycode.cpp index 50adcca889..b0e45da493 100644 --- a/src/client/keycode.cpp +++ b/src/client/keycode.cpp @@ -13,13 +13,6 @@ #include #include -struct table_key { - std::string Name; // An EKEY_CODE 'symbol' name as a string - EKEY_CODE Key; - wchar_t Char; // L'\0' means no character assigned - std::string LangName; // empty string means it doesn't have a human description -}; - #define DEFINEKEY1(x, lang) /* Irrlicht key without character */ \ { #x, x, L'\0', lang }, #define DEFINEKEY2(x, ch, lang) /* Irrlicht key with character */ \ @@ -33,7 +26,7 @@ struct table_key { #define N_(text) text -static std::vector table = { +std::vector KeyPress::keycode_table = { // Keys that can be reliably mapped between Char and Key DEFINEKEY3(0) DEFINEKEY3(1) @@ -224,18 +217,17 @@ static std::vector table = { DEFINEKEY5("^") DEFINEKEY5("_") }; - -static const table_key invalid_key = {"", KEY_UNKNOWN, L'\0', ""}; +const KeyPress::table_key KeyPress::invalid_key = {"", KEY_UNKNOWN, L'\0', ""}; #undef N_ -static const table_key &lookup_keychar(wchar_t Char) +const KeyPress::table_key &KeyPress::lookupKeychar(wchar_t Char) { if (Char == L'\0') return invalid_key; - for (const auto &table_key : table) { + for (const auto &table_key : keycode_table) { if (table_key.Char == Char) return table_key; } @@ -243,15 +235,15 @@ static const table_key &lookup_keychar(wchar_t Char) // Create a new entry in the lookup table if one is not available. auto newsym = wide_to_utf8(std::wstring_view(&Char, 1)); table_key new_key {newsym, KEY_KEY_CODES_COUNT, Char, newsym}; - return table.emplace_back(std::move(new_key)); + return keycode_table.emplace_back(std::move(new_key)); } -static const table_key &lookup_keykey(EKEY_CODE key) +const KeyPress::table_key &KeyPress::lookupKeykey(EKEY_CODE key) { if (!Keycode::isValid(key)) return invalid_key; - for (const auto &table_key : table) { + for (const auto &table_key : keycode_table) { if (table_key.Key == key) return table_key; } @@ -259,12 +251,12 @@ static const table_key &lookup_keykey(EKEY_CODE key) return invalid_key; } -static const table_key &lookup_keyname(std::string_view name) +const KeyPress::table_key &KeyPress::lookupKeyname(std::string_view name) { if (name.empty()) return invalid_key; - for (const auto &table_key : table) { + for (const auto &table_key : keycode_table) { if (table_key.Name == name) return table_key; } @@ -272,34 +264,43 @@ static const table_key &lookup_keyname(std::string_view name) auto wname = utf8_to_wide(name); if (wname.empty()) return invalid_key; - return lookup_keychar(wname[0]); + return lookupKeychar(wname[0]); } -static const table_key &lookup_scancode(const u32 scancode) +const KeyPress::table_key &KeyPress::lookupScancode(const u32 scancode) { auto key = RenderingEngine::get_raw_device()->getKeyFromScancode(scancode); return std::holds_alternative(key) ? - lookup_keykey(std::get(key)) : - lookup_keychar(std::get(key)); + lookupKeykey(std::get(key)) : + lookupKeychar(std::get(key)); } -static const table_key &lookup_scancode(const std::variant &scancode) +const KeyPress::table_key &KeyPress::lookupScancode() const { - return std::holds_alternative(scancode) ? - lookup_keykey(std::get(scancode)) : - lookup_scancode(std::get(scancode)); + switch (getType()) { + case KeyPress::KEYCODE_INPUT: + return lookupKeykey(std::get(scancode)); + case KeyPress::SCANCODE_INPUT: + return lookupScancode(std::get(scancode)); + default: + return invalid_key; + } } void KeyPress::loadFromKey(EKEY_CODE keycode, wchar_t keychar) { - scancode = RenderingEngine::get_raw_device()->getScancodeFromKey(Keycode(keycode, keychar)); + auto irr_scancode = RenderingEngine::get_raw_device()->getScancodeFromKey(Keycode(keycode, keychar)); + if (std::holds_alternative(irr_scancode)) + scancode.emplace(std::get(irr_scancode)); + else + scancode.emplace(std::get(irr_scancode)); } KeyPress::KeyPress(const std::string &name) { if (loadFromScancode(name)) return; - const auto &key = lookup_keyname(name); + const auto &key = lookupKeyname(name); loadFromKey(key.Key, key.Char); } @@ -326,7 +327,7 @@ std::string KeyPress::formatScancode() const std::string KeyPress::sym() const { - std::string name = lookup_scancode(scancode).Name; + std::string name = lookupScancode().Name; if (USE_SDL2 || name.empty()) if (auto newname = formatScancode(); !newname.empty()) return newname; @@ -335,7 +336,7 @@ std::string KeyPress::sym() const std::string KeyPress::name() const { - const auto &name = lookup_scancode(scancode).LangName; + const auto &name = lookupScancode().LangName; if (!name.empty()) return name; return formatScancode(); @@ -343,12 +344,12 @@ std::string KeyPress::name() const EKEY_CODE KeyPress::getKeycode() const { - return lookup_scancode(scancode).Key; + return lookupScancode().Key; } wchar_t KeyPress::getKeychar() const { - return lookup_scancode(scancode).Char; + return lookupScancode().Char; } bool KeyPress::loadFromScancode(const std::string &name) @@ -376,28 +377,51 @@ KeyPress KeyPress::getSpecialKey(const std::string &name) return key; } +KeyPress::operator bool() const +{ + switch (getType()) { + case KeyPress::SCANCODE_INPUT: + return std::get(scancode) != 0; + case KeyPress::KEYCODE_INPUT: + return Keycode::isValid(std::get(scancode)); + case KeyPress::GAME_ACTION_INPUT: + return std::get(scancode) < KeyType::INTERNAL_ENUM_COUNT; + default: + return false; + } +} + /* Key config */ // A simple cache for quicker lookup -static std::unordered_map g_key_setting_cache; +static std::unordered_map> g_key_setting_cache; -KeyPress getKeySetting(const std::string &settingname) +const std::vector &getKeySetting(const std::string &settingname) { auto n = g_key_setting_cache.find(settingname); if (n != g_key_setting_cache.end()) return n->second; - auto keysym = g_settings->get(settingname); + auto setting_value = g_settings->get(settingname); auto &ref = g_key_setting_cache[settingname]; - ref = KeyPress(keysym); - if (!keysym.empty() && !ref) { - warningstream << "Invalid key '" << keysym << "' for '" << settingname << "'." << std::endl; + for (const auto &keysym: str_split(setting_value, '|')) { + if (KeyPress kp = keysym) { + ref.push_back(kp); + } else { + warningstream << "Invalid key '" << keysym << "' for '" << settingname << "'." << std::endl; + } } return ref; } +bool keySettingHasMatch(const std::string &settingname, KeyPress kp) +{ + const auto &keylist = getKeySetting(settingname); + return std::find(keylist.begin(), keylist.end(), kp) != keylist.end(); +} + void clearKeyCache() { g_key_setting_cache.clear(); diff --git a/src/client/keycode.h b/src/client/keycode.h index a62e7822cd..08bf1e32cb 100644 --- a/src/client/keycode.h +++ b/src/client/keycode.h @@ -5,10 +5,12 @@ #pragma once #include "irrlichttypes.h" +#include "keys.h" #include #include #include #include +#include /* A key press, consisting of a scancode or a keycode. * This fits into 64 bits, so prefer passing this by value. @@ -16,12 +18,20 @@ class KeyPress { public: + enum InputType { + SCANCODE_INPUT, // Keyboard input (scancodes) + KEYCODE_INPUT, // (Deprecated) EKEY_CODE-based keyboard and mouse input + GAME_ACTION_INPUT, // GameKeyType input passed by touchscreen buttons + }; + KeyPress() = default; KeyPress(const std::string &name); KeyPress(const SEvent::SKeyInput &in); + KeyPress(GameKeyType key) : scancode(key) {} + // Get a string representation that is suitable for use in minetest.conf std::string sym() const; @@ -54,18 +64,32 @@ public: return scancode < o.scancode; } - // Check whether the keypress is valid - operator bool() const - { - return std::holds_alternative(scancode) ? - Keycode::isValid(std::get(scancode)) : - std::get(scancode) != 0; + InputType getType() const { + return static_cast(scancode.index()); } + // Check whether the keypress is valid + operator bool() const; + static KeyPress getSpecialKey(const std::string &name); private: - using value_type = std::variant; + struct table_key { // internal keycode lookup table + std::string Name; // An EKEY_CODE 'symbol' name as a string + EKEY_CODE Key; + wchar_t Char; // L'\0' means no character assigned + std::string LangName; // empty string means it doesn't have a human description + }; + static const table_key invalid_key; + static std::vector keycode_table; + static const table_key &lookupKeychar(wchar_t Char); + static const table_key &lookupKeykey(EKEY_CODE key); + static const table_key &lookupKeyname(std::string_view name); + static const table_key &lookupScancode(const u32 scancode); + const table_key &lookupScancode() const; + + using value_type = std::variant; + bool loadFromScancode(const std::string &name); void loadFromKey(EKEY_CODE keycode, wchar_t keychar); std::string formatScancode() const; @@ -92,7 +116,13 @@ struct std::hash #define RMBKey KeyPress::getSpecialKey("KEY_RBUTTON") // Key configuration getter -KeyPress getKeySetting(const std::string &settingname); +// Note that the reference may be invalidated by a next call to getKeySetting +// or a related function, so the value should either be used immediately or +// copied elsewhere before calling this again. +const std::vector &getKeySetting(const std::string &settingname); + +// Check whether the key setting includes a key. +bool keySettingHasMatch(const std::string &settingname, KeyPress kp); // Clear fast lookup cache void clearKeyCache(); diff --git a/src/gui/guiChatConsole.cpp b/src/gui/guiChatConsole.cpp index db90368cc3..64b9b18d48 100644 --- a/src/gui/guiChatConsole.cpp +++ b/src/gui/guiChatConsole.cpp @@ -436,7 +436,7 @@ bool GUIChatConsole::OnEvent(const SEvent& event) } // Key input - if (KeyPress(event.KeyInput) == getKeySetting("keymap_console")) { + if (keySettingHasMatch("keymap_console", event.KeyInput)) { closeConsole(); // inhibit open so the_game doesn't reopen immediately diff --git a/src/gui/guiFormSpecMenu.cpp b/src/gui/guiFormSpecMenu.cpp index 94e3dcb872..f3b53477af 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -3985,7 +3985,7 @@ bool GUIFormSpecMenu::preprocessEvent(const SEvent& event) if (event.EventType == EET_KEY_INPUT_EVENT) { KeyPress kp(event.KeyInput); if (kp == EscapeKey - || kp == getKeySetting("keymap_inventory") + || keySettingHasMatch("keymap_inventory", kp) || event.KeyInput.Key==KEY_RETURN) { gui::IGUIElement *focused = Environment->getFocus(); if (focused && isMyChild(focused) && @@ -4084,17 +4084,17 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event) } if (event.KeyInput.PressedDown && ( (kp == EscapeKey) || - ((m_client != NULL) && (kp == getKeySetting("keymap_inventory"))))) { + ((m_client != NULL) && (keySettingHasMatch("keymap_inventory", kp))))) { tryClose(); return true; } if (m_client != NULL && event.KeyInput.PressedDown && - (kp == getKeySetting("keymap_screenshot"))) { + (keySettingHasMatch("keymap_screenshot", kp))) { m_client->makeScreenshot(); } - if (event.KeyInput.PressedDown && kp == getKeySetting("keymap_toggle_debug")) { + if (event.KeyInput.PressedDown && keySettingHasMatch("keymap_toggle_debug", kp)) { if (!m_client || m_client->checkPrivilege("debug")) m_show_debug = !m_show_debug; } diff --git a/src/gui/touchcontrols.cpp b/src/gui/touchcontrols.cpp index 6a3aeb108b..ec3c9e7282 100644 --- a/src/gui/touchcontrols.cpp +++ b/src/gui/touchcontrols.cpp @@ -31,16 +31,13 @@ TouchControls *g_touchcontrols; -void TouchControls::emitKeyboardEvent(KeyPress key, bool pressed) +void TouchControls::emitGameKeyEvent(GameKeyType key, bool pressed) { SEvent e{}; - e.EventType = EET_KEY_INPUT_EVENT; - e.KeyInput.Key = key.getKeycode(); - e.KeyInput.Control = false; - e.KeyInput.Shift = false; - e.KeyInput.Char = key.getKeychar(); - e.KeyInput.SystemKeyCode = key.getScancode(); - e.KeyInput.PressedDown = pressed; + e.EventType = EET_USER_EVENT; + e.UserEvent.code = USER_EVENT_GAME_KEY; + e.UserEvent.UserData1 = static_cast(key); + e.UserEvent.UserData2 = pressed; m_receiver->OnEvent(e); } @@ -55,10 +52,10 @@ void TouchControls::loadButtonTexture(IGUIImage *gui_button, const std::string & void TouchControls::buttonEmitAction(button_info &btn, bool action) { - if (!btn.keypress) + if (!btn.game_key) return; - emitKeyboardEvent(btn.keypress, action); + emitGameKeyEvent(btn.game_key, action); if (action) { if (btn.toggleable == button_info::FIRST_TEXTURE) { @@ -142,81 +139,48 @@ bool TouchControls::buttonsStep(std::vector &buttons, float dtime) return has_pointers; } -static std::string id_to_setting(touch_gui_button_id id) +static GameKeyType id_to_action(touch_gui_button_id id) { - std::string key = ""; switch (id) { + case exit_id: + return KeyType::ESC; case dig_id: - key = "dig"; - break; + return KeyType::DIG; case place_id: - key = "place"; - break; + return KeyType::PLACE; case jump_id: - key = "jump"; - break; + return KeyType::JUMP; case sneak_id: - key = "sneak"; - break; + return KeyType::SNEAK; case zoom_id: - key = "zoom"; - break; + return KeyType::ZOOM; case aux1_id: - key = "aux1"; - break; + return KeyType::AUX1; case fly_id: - key = "freemove"; - break; + return KeyType::FREEMOVE; case noclip_id: - key = "noclip"; - break; + return KeyType::NOCLIP; case fast_id: - key = "fastmove"; - break; + return KeyType::FASTMOVE; case debug_id: - key = "toggle_debug"; - break; + return KeyType::TOGGLE_DEBUG; case camera_id: - key = "camera_mode"; - break; + return KeyType::CAMERA_MODE; case range_id: - key = "rangeselect"; - break; + return KeyType::RANGESELECT; case minimap_id: - key = "minimap"; - break; + return KeyType::MINIMAP; case toggle_chat_id: - key = "toggle_chat"; - break; + return KeyType::TOGGLE_CHAT; case chat_id: - key = "chat"; - break; + return KeyType::CHAT; case inventory_id: - key = "inventory"; - break; + return KeyType::INVENTORY; case drop_id: - key = "drop"; - break; + return KeyType::DROP; default: - break; + return KeyType::INTERNAL_ENUM_COUNT; } - return key.empty() ? key : "keymap_" + key; -} - -static KeyPress id_to_keypress(touch_gui_button_id id) -{ - // ESC isn't part of the keymap. - if (id == exit_id) - return EscapeKey; - - auto setting_name = id_to_setting(id); - - assert(!setting_name.empty()); - auto kp = getKeySetting(setting_name); - if (!kp) - warningstream << "TouchControls: Unbound or invalid key for " - << setting_name << ", hiding button." << std::endl; - return kp; } @@ -239,11 +203,6 @@ TouchControls::TouchControls(IrrlichtDevice *device, ISimpleTextureSource *tsrc) readSettings(); for (auto name : setting_names) g_settings->registerChangedCallback(name, settingChangedCallback, this); - - // Also update layout when keybindings change (e.g. for convertibles) - for (u8 id = 0; id < touch_gui_button_id_END; id++) - if (auto name = id_to_setting((touch_gui_button_id)id); !name.empty()) - g_settings->registerChangedCallback(name, settingChangedCallback, this); } void TouchControls::settingChangedCallback(const std::string &name, void *data) @@ -376,7 +335,7 @@ bool TouchControls::mayAddButton(touch_gui_button_id id) assert(ButtonLayout::isButtonValid(id)); assert(ButtonLayout::isButtonAllowed(id)); // The overflow button doesn't need a keycode to be valid. - return id == overflow_id || id_to_keypress(id); + return id == overflow_id || id_to_action(id) < KeyType::INTERNAL_ENUM_COUNT; } void TouchControls::addButton(std::vector &buttons, touch_gui_button_id id, @@ -388,7 +347,7 @@ void TouchControls::addButton(std::vector &buttons, touch_gui_butto button_info &btn = buttons.emplace_back(); btn.id = id; - btn.keypress = id_to_keypress(id); + btn.game_key = id_to_action(id); btn.gui_button = grab_gui_element(btn_gui_button); } @@ -655,10 +614,10 @@ void TouchControls::translateEvent(const SEvent &event) void TouchControls::applyJoystickStatus() { if (m_joystick_triggers_aux1) { - auto key = id_to_keypress(aux1_id); - emitKeyboardEvent(key, false); + auto key = id_to_action(aux1_id); + emitGameKeyEvent(key, false); if (m_joystick_status_aux1) - emitKeyboardEvent(key, true); + emitGameKeyEvent(key, true); } } @@ -764,11 +723,11 @@ void TouchControls::releaseAll() // Release those manually too since the change initiated by // handleReleaseEvent will only be applied later by applyContextControls. if (m_dig_pressed) { - emitKeyboardEvent(id_to_keypress(dig_id), false); + emitGameKeyEvent(id_to_action(dig_id), false); m_dig_pressed = false; } if (m_place_pressed) { - emitKeyboardEvent(id_to_keypress(place_id), false); + emitGameKeyEvent(id_to_action(place_id), false); m_place_pressed = false; } } @@ -852,20 +811,20 @@ void TouchControls::applyContextControls(const TouchInteractionMode &mode) target_place_pressed |= now < m_place_pressed_until; if (target_dig_pressed && !m_dig_pressed) { - emitKeyboardEvent(id_to_keypress(dig_id), true); + emitGameKeyEvent(id_to_action(dig_id), true); m_dig_pressed = true; } else if (!target_dig_pressed && m_dig_pressed) { - emitKeyboardEvent(id_to_keypress(dig_id), false); + emitGameKeyEvent(id_to_action(dig_id), false); m_dig_pressed = false; } if (target_place_pressed && !m_place_pressed) { - emitKeyboardEvent(id_to_keypress(place_id), true); + emitGameKeyEvent(id_to_action(place_id), true); m_place_pressed = true; } else if (!target_place_pressed && m_place_pressed) { - emitKeyboardEvent(id_to_keypress(place_id), false); + emitGameKeyEvent(id_to_action(place_id), false); m_place_pressed = false; } } diff --git a/src/gui/touchcontrols.h b/src/gui/touchcontrols.h index 8e8e93f569..36a1279088 100644 --- a/src/gui/touchcontrols.h +++ b/src/gui/touchcontrols.h @@ -16,7 +16,7 @@ #include "itemdef.h" #include "touchscreenlayout.h" #include "util/basic_macros.h" -#include "client/keycode.h" +#include "client/keys.h" class IrrlichtDevice; namespace gui @@ -53,7 +53,7 @@ struct button_info { touch_gui_button_id id; float repeat_counter; - KeyPress keypress; + GameKeyType game_key; std::vector pointer_ids; std::shared_ptr gui_button = nullptr; @@ -198,7 +198,7 @@ private: // for its buttons. We only want static image display, not interactivity, // from Irrlicht. - void emitKeyboardEvent(KeyPress keycode, bool pressed); + void emitGameKeyEvent(GameKeyType, bool pressed); void loadButtonTexture(IGUIImage *gui_button, const std::string &path); void buttonEmitAction(button_info &btn, bool action); diff --git a/src/script/lua_api/l_menu_common.cpp b/src/script/lua_api/l_menu_common.cpp index 428b902c91..3a512fcd35 100644 --- a/src/script/lua_api/l_menu_common.cpp +++ b/src/script/lua_api/l_menu_common.cpp @@ -42,6 +42,14 @@ int ModApiMenuCommon::l_normalize_keycode(lua_State *L) return 1; } +int ModApiMenuCommon::l_get_keycode_name(lua_State *L) +{ + auto keystr = luaL_checkstring(L, 1); + auto name = KeyPress(keystr).name(); + lua_pushstring(L, name.empty() ? "" : gettext(name.c_str())); + return 1; +} + void ModApiMenuCommon::Initialize(lua_State *L, int top) { @@ -49,6 +57,7 @@ void ModApiMenuCommon::Initialize(lua_State *L, int top) API_FCT(get_active_driver); API_FCT(irrlicht_device_supports_touch); API_FCT(normalize_keycode); + API_FCT(get_keycode_name); } diff --git a/src/script/lua_api/l_menu_common.h b/src/script/lua_api/l_menu_common.h index 2c1756fa5d..b0ea9cb5ef 100644 --- a/src/script/lua_api/l_menu_common.h +++ b/src/script/lua_api/l_menu_common.h @@ -14,6 +14,7 @@ private: static int l_get_active_driver(lua_State *L); static int l_irrlicht_device_supports_touch(lua_State *L); static int l_normalize_keycode(lua_State *L); + static int l_get_keycode_name(lua_State *L); public: static void Initialize(lua_State *L, int top);