1
0
mirror of https://github.com/luanti-org/luanti.git synced 2026-01-11 11:45:25 +01:00

Implement secondary keybindings (#16803)

Co-authored-by: Lars Müller <34514239+appgurueu@users.noreply.github.com>
Co-authored-by: sfan5 <sfan5@live.de>
Co-authored-by: SmallJoker <SmallJoker@users.noreply.github.com>
This commit is contained in:
y5nw
2026-01-05 22:34:25 +01:00
committed by GitHub
parent 42906dbe54
commit 07b8268d57
9 changed files with 144 additions and 45 deletions

View File

@@ -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

View File

@@ -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)

View File

@@ -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<KeyPress>{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<GameKeyType>(i));
for (auto key: keybindings[i]) {
listenForKey(key, static_cast<GameKeyType>(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) {

View File

@@ -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<KeyPress, KeyType::INTERNAL_ENUM_COUNT> keybindings;
std::array<std::vector<KeyPress>, KeyType::INTERNAL_ENUM_COUNT> keybindings;
s32 mouse_wheel = 0;

View File

@@ -7,6 +7,7 @@
#include "settings.h"
#include "log.h"
#include "renderingengine.h"
#include "util/basic_macros.h"
#include "util/string.h"
#include <unordered_map>
#include <vector>
@@ -365,23 +366,32 @@ KeyPress KeyPress::getSpecialKey(const std::string &name)
*/
// A simple cache for quicker lookup
static std::unordered_map<std::string, KeyPress> g_key_setting_cache;
static std::unordered_map<std::string, std::vector<KeyPress>> g_key_setting_cache;
KeyPress getKeySetting(const std::string &settingname)
const std::vector<KeyPress> &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();

View File

@@ -9,6 +9,7 @@
#include <IEventReceiver.h>
#include <string>
#include <variant>
#include <vector>
/* 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<KeyPress>
#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<KeyPress> &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();

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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];
}