diff --git a/builtin/common/settings/components.lua b/builtin/common/settings/components.lua index ace373a4ca..41e3db089f 100644 --- a/builtin/common/settings/components.lua +++ b/builtin/common/settings/components.lua @@ -447,11 +447,30 @@ local function make_noise_params(setting) } end +local function has_keybinding_conflict(t1, t2) + for _, v1 in pairs(t1) do + for _, v2 in pairs(t2) do + if core.are_keycodes_equal(v1, v2) then + return true + end + end + end + return false +end + +local function get_key_setting(name) + return core.settings:get(name):split("|") +end + +-- Setting names where an empty field shall be shown to assign new keybindings. +local key_add_empty = {} + function make.key(setting) local btn_bind = "bind_" .. setting.name local btn_clear = "unbind_" .. setting.name + local btn_add = "add_" .. setting.name local function add_conflict_warnings(fs, height) - local value = core.settings:get(setting.name) + local value = get_key_setting(setting.name) if value == "" then return height end @@ -464,7 +483,7 @@ function make.key(setting) 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 + has_keybinding_conflict(get_key_setting(o.name), value) then local is_current_close_world = setting.name == "keymap_close_world" local is_other_close_world = o.name == "keymap_close_world" @@ -481,40 +500,80 @@ function make.key(setting) end return height end + + local add_empty = key_add_empty[setting.name] + key_add_empty[setting.name] = nil + return { info_text = setting.comment, setting = setting, spacing = 0.1, get_formspec = function(self, avail_w) - local current_value = core.settings:get(setting.name) or "" + local value_string = core.settings:get(setting.name) or "" local default_value = setting.default or "" - self.resettable = core.settings:has(setting.name) and (current_value ~= default_value) - local btn_bind_width = math.max(2.5, avail_w / 2) - local value = core.settings:get(setting.name) + self.resettable = core.settings:has(setting.name) and (value_string ~= default_value) + local value_width = math.max(2.5, avail_w / 2) + local value = get_key_setting(setting.name) 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")), } - local height = 0.8 + + local function add_keybinding_row(idx) + local btn_bind_width = value_width - 1.6 + local has_value = value[idx] + local y = (idx - 1) * 0.8 + if not has_value then + btn_bind_width = idx == 1 and value_width or (value_width - 0.8) + end + table.insert(fs, ("button_key[%f,%f;%f,0.8;%s_%d;%s]"):format( + value_width, y, btn_bind_width, + btn_bind, idx, core.formspec_escape(value[idx] or ""))) + if has_value then + table.insert(fs, ("image_button[%f,%f;0.8,0.8;%s;%s_%d;]"):format( + avail_w - 1.6, y, + core.formspec_escape(defaulttexturedir .. "clear.png"), + btn_clear, idx)) + table.insert(fs, ("tooltip[%s_%d;%s]"):format(btn_clear, idx, + fgettext("Remove keybinding"))) + end + end + + local height = #value * 0.8 + for i = 1, #value do + add_keybinding_row(i) + end + if add_empty or #value == 0 then + add_keybinding_row(#value+1) + height = height + 0.8 + else + table.insert(fs, ("image_button[%f,%f;0.8,0.8;%s;%s;]"):format( + avail_w - 0.8, height - 0.8, + core.formspec_escape(defaulttexturedir .. "plus.png"), btn_add)) + table.insert(fs, ("tooltip[%s;%s]"):format(btn_add, fgettext("Add keybinding"))) + end + height = add_conflict_warnings(fs, height) return table.concat(fs), height end, on_submit = function(self, fields) - 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, "") + if fields[btn_add] then + key_add_empty[setting.name] = true return true end + local value = get_key_setting(setting.name) + for i = 1, #value + 1 do + if fields[("%s_%d"):format(btn_bind, i)] then + value[i] = fields[("%s_%d"):format(btn_bind, i)] + core.settings:set(setting.name, table.concat(value, "|")) + return true + elseif fields[("%s_%d"):format(btn_clear, i)] then + table.remove(value, i) + core.settings:set(setting.name, table.concat(value, "|")) + return true + end + end end, } end diff --git a/builtin/mainmenu/dlg_rebind_keys.lua b/builtin/mainmenu/dlg_rebind_keys.lua index ec4d1357fd..c5e909a244 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 -- normalize keybinding with the "|" keychar (<5.12) + 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/src/client/inputhandler.cpp b/src/client/inputhandler.cpp index 3add475cb3..fc59c6a937 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,9 @@ 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)); + for (auto key: keybindings[i]) { + listenForKey(key, static_cast(i)); + } } } @@ -85,13 +87,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 +109,15 @@ void MyEventReceiver::setKeyDown(GameKeyType action, bool is_down) } } +bool MyEventReceiver::checkKeyDown(GameKeyType action) const +{ + for (const 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 +147,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(); @@ -151,7 +160,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) { diff --git a/src/client/inputhandler.h b/src/client/inputhandler.h index dc401991ac..d3697b0d4c 100644 --- a/src/client/inputhandler.h +++ b/src/client/inputhandler.h @@ -96,13 +96,14 @@ private: bool setKeyDown(KeyPress keyCode, bool is_down); void setKeyDown(GameKeyType action, bool is_down); + bool checkKeyDown(GameKeyType action) const; /* 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 1692c82dae..9a2a579ecc 100644 --- a/src/client/keycode.cpp +++ b/src/client/keycode.cpp @@ -7,6 +7,7 @@ #include "settings.h" #include "log.h" #include "renderingengine.h" +#include "util/basic_macros.h" #include "util/string.h" #include #include @@ -365,23 +366,32 @@ KeyPress KeyPress::getSpecialKey(const std::string &name) */ // 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 CONTAINS(keylist, kp); +} + void clearKeyCache() { g_key_setting_cache.clear(); diff --git a/src/client/keycode.h b/src/client/keycode.h index a62e7822cd..23608938f1 100644 --- a/src/client/keycode.h +++ b/src/client/keycode.h @@ -9,6 +9,7 @@ #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. @@ -92,7 +93,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 c6a8aa0df5..29dee1c9b7 100644 --- a/src/gui/guiChatConsole.cpp +++ b/src/gui/guiChatConsole.cpp @@ -435,7 +435,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 6194fefc96..ae73bc07da 100644 --- a/src/gui/guiFormSpecMenu.cpp +++ b/src/gui/guiFormSpecMenu.cpp @@ -3979,7 +3979,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) && @@ -4078,13 +4078,13 @@ 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 (event.KeyInput.PressedDown && - (kp == getKeySetting("keymap_screenshot"))) { + (keySettingHasMatch("keymap_screenshot", kp))) { if (m_client) { m_client->makeScreenshot(); } else if (m_text_dst) { // in main menu @@ -4092,7 +4092,7 @@ bool GUIFormSpecMenu::OnEvent(const SEvent& event) } } - 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 d783c1fb15..e9effe4e1e 100644 --- a/src/gui/touchcontrols.cpp +++ b/src/gui/touchcontrols.cpp @@ -209,11 +209,13 @@ static KeyPress id_to_keypress(touch_gui_button_id id) auto setting_name = id_to_setting(id); assert(!setting_name.empty()); - auto kp = getKeySetting(setting_name); - if (!kp) + const auto &keylist = getKeySetting(setting_name); + if (keylist.empty()) { warningstream << "TouchControls: Unbound or invalid key for " << setting_name << ", hiding button." << std::endl; - return kp; + return KeyPress(); + } + return keylist[0]; }