From 419fe7b7ecbe396f2fc24aca3331dec7ca7c4850 Mon Sep 17 00:00:00 2001 From: obligaron Date: Sat, 18 Dec 2021 21:39:42 +0100 Subject: [PATCH] Change Keymapper to OptionCategory/OptionEntry --- Source/CMakeLists.txt | 1 - Source/control.cpp | 3 - Source/controls/keymapper.cpp | 133 ---------------------- Source/controls/keymapper.hpp | 69 ------------ Source/diablo.cpp | 137 +++++++++++++---------- Source/diablo.h | 2 - Source/loadsave.cpp | 1 + Source/options.cpp | 204 ++++++++++++++++++++++++++++++---- Source/options.h | 59 +++++++++- Source/panels/spell_list.cpp | 17 ++- 10 files changed, 323 insertions(+), 303 deletions(-) delete mode 100644 Source/controls/keymapper.cpp delete mode 100644 Source/controls/keymapper.hpp diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 32239e8fd..6850186f2 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -81,7 +81,6 @@ set(libdevilutionx_SRCS controls/menu_controls.cpp controls/modifier_hints.cpp controls/plrctrls.cpp - controls/keymapper.cpp engine/animationinfo.cpp engine/demomode.cpp engine/load_cel.cpp diff --git a/Source/control.cpp b/Source/control.cpp index b291e5174..315c27331 100644 --- a/Source/control.cpp +++ b/Source/control.cpp @@ -14,7 +14,6 @@ #include "DiabloUI/art.h" #include "DiabloUI/art_draw.h" #include "automap.h" -#include "controls/keymapper.hpp" #include "cursor.h" #include "engine/cel_sprite.hpp" #include "engine/load_cel.hpp" @@ -93,8 +92,6 @@ const Rectangle &GetRightPanel() return RightPanel; } -extern std::array quickSpellActionIndexes; - /** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ Rectangle ChrBtnsRect[4] = { { { 137, 138 }, { 41, 22 } }, diff --git a/Source/controls/keymapper.cpp b/Source/controls/keymapper.cpp deleted file mode 100644 index 7e71f5c5a..000000000 --- a/Source/controls/keymapper.cpp +++ /dev/null @@ -1,133 +0,0 @@ -#include "keymapper.hpp" - -#include -#include -#include - -#include - -#ifdef USE_SDL1 -#include "utils/sdl2_to_1_2_backports.h" -#endif - -#include "control.h" -#include "options.h" -#include "utils/log.hpp" - -namespace devilution { - -Keymapper::Keymapper() -{ - // Insert all supported keys: a-z, 0-9 and F1-F12. - keyIDToKeyName.reserve(('Z' - 'A' + 1) + ('9' - '0' + 1) + 12); - for (char c = 'A'; c <= 'Z'; ++c) { - keyIDToKeyName.emplace(c, std::string(1, c)); - } - for (char c = '0'; c <= '9'; ++c) { - keyIDToKeyName.emplace(c, std::string(1, c)); - } - for (int i = 0; i < 12; ++i) { - keyIDToKeyName.emplace(DVL_VK_F1 + i, fmt::format("F{}", i + 1)); - } - - keyNameToKeyID.reserve(keyIDToKeyName.size()); - for (const auto &kv : keyIDToKeyName) { - keyNameToKeyID.emplace(kv.second, kv.first); - } -} - -Keymapper::ActionIndex Keymapper::AddAction(const Action &action) -{ - actions.emplace_back(action); - - return actions.size() - 1; -} - -void Keymapper::KeyPressed(int key) const -{ - auto it = keyIDToAction.find(key); - if (it == keyIDToAction.end()) - return; // Ignore unmapped keys. - - const auto &action = it->second; - - // Check that the action can be triggered and that the chat textbox is not - // open. - if (!action.get().enable() || talkflag) - return; - - action(); -} - -std::string Keymapper::KeyNameForAction(ActionIndex actionIndex) const -{ - assert(actionIndex < actions.size()); - auto key = actions[actionIndex].key; - auto it = keyIDToKeyName.find(key); - assert(it != keyIDToKeyName.end()); - return it->second; -} - -void Keymapper::Save() const -{ - // Use the action vector to go though the actions to keep the same order. - for (const auto &action : actions) { - if (action.key == DVL_VK_INVALID) { - // Just add an empty config entry if the action is unbound. - SetIniValue("Keymapping", action.name.c_str(), ""); - continue; - } - - auto keyNameIt = keyIDToKeyName.find(action.key); - if (keyNameIt == keyIDToKeyName.end()) { - Log("Keymapper: no name found for key '{}'", action.key); - continue; - } - SetIniValue("Keymapping", action.name.c_str(), keyNameIt->second.c_str()); - } -} - -void Keymapper::Load() -{ - keyIDToAction.clear(); - - for (auto &action : actions) { - auto key = GetActionKey(action); - action.key = key; - if (key == DVL_VK_INVALID) { - // Skip if the action has no key bound to it. - continue; - } - // Store the key in action.key and in the map so we can save() the - // actions while keeping the same order as they have been added. - keyIDToAction.emplace(key, action); - } -} - -int Keymapper::GetActionKey(const Keymapper::Action &action) -{ - std::array result; - if (!GetIniValue("Keymapping", action.name.c_str(), result.data(), result.size())) - return action.defaultKey; // Return the default key if no key has been set. - - std::string key = result.data(); - if (key.empty()) - return DVL_VK_INVALID; - - auto keyIt = keyNameToKeyID.find(key); - if (keyIt == keyNameToKeyID.end()) { - // Return the default key if the key is unknown. - Log("Keymapper: unknown key '{}'", key); - return action.defaultKey; - } - - auto it = keyIDToAction.find(keyIt->second); - if (it != keyIDToAction.end()) { - // Warn about overwriting keys. - Log("Keymapper: key '{}' is already bound to action '{}', overwriting", key, it->second.get().name); - } - - return keyIt->second; -} - -} // namespace devilution diff --git a/Source/controls/keymapper.hpp b/Source/controls/keymapper.hpp deleted file mode 100644 index 8d1ae17ae..000000000 --- a/Source/controls/keymapper.hpp +++ /dev/null @@ -1,69 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -namespace devilution { - -/** The Keymapper maps keys to actions. */ -class Keymapper final { -public: - using ActionIndex = std::size_t; - - /** - * Action represents an action that can be triggered using a keyboard - * shortcut. - */ - class Action final { - public: - Action(const std::string &name, int defaultKey, std::function action) - : name(name) - , defaultKey(defaultKey) - , action(action) - , enable([] { return true; }) - { - } - Action(const std::string &name, int defaultKey, std::function action, std::function enable) - : name(name) - , defaultKey(defaultKey) - , action(action) - , enable(enable) - { - } - - void operator()() const - { - action(); - } - - private: - std::string name; - int defaultKey; - std::function action; - std::function enable; - int key {}; - - friend class Keymapper; - }; - - Keymapper(); - - ActionIndex AddAction(const Action &action); - void KeyPressed(int key) const; - std::string KeyNameForAction(ActionIndex actionIndex) const; - void Save() const; - void Load(); - -private: - int GetActionKey(const Action &action); - - std::vector actions; - std::unordered_map> keyIDToAction; - std::unordered_map keyIDToKeyName; - std::unordered_map keyNameToKeyID; -}; - -} // namespace devilution diff --git a/Source/diablo.cpp b/Source/diablo.cpp index c9189ab0e..cb3d91152 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -19,7 +19,6 @@ #include "debug.h" #endif #include "DiabloUI/diabloui.h" -#include "controls/keymapper.hpp" #include "controls/plrctrls.h" #include "controls/touch/gamepad.h" #include "controls/touch/renderers.h" @@ -105,8 +104,6 @@ bool gbQuietMode = false; clicktype sgbMouseDown; uint16_t gnTickDelay = 50; char gszProductName[64] = "DevilutionX vUnknown"; -Keymapper keymapper; -std::array quickSpellActionIndexes; #ifdef _DEBUG bool DebugDisableNetworkTimeout = false; @@ -443,7 +440,7 @@ void PressKey(int vkey) if (sgnTimeoutCurs != CURSOR_NONE) { return; } - keymapper.KeyPressed(vkey); + sgOptions.Keymapper.KeyPressed(vkey); if (vkey == DVL_VK_RETURN) { if (GetAsyncKeyState(DVL_VK_MENU)) sgOptions.Graphics.fullscreen.SetValue(!IsFullScreen()); @@ -475,7 +472,7 @@ void PressKey(int vkey) return; } - keymapper.KeyPressed(vkey); + sgOptions.Keymapper.KeyPressed(vkey); if (vkey == DVL_VK_RETURN) { if (GetAsyncKeyState(DVL_VK_MENU)) { @@ -1388,15 +1385,18 @@ bool IsPlayerDead() void InitKeymapActions() { - keymapper.AddAction({ + sgOptions.Keymapper.AddAction( "Help", + N_("Help"), + N_("Open Help Screen."), DVL_VK_F1, HelpKeyPressed, - [&]() { return !IsPlayerDead(); }, - }); + [&]() { return !IsPlayerDead(); }); for (int i = 0; i < 4; ++i) { - quickSpellActionIndexes[i] = keymapper.AddAction({ - std::string("QuickSpell") + std::to_string(i + 1), + sgOptions.Keymapper.AddAction( + "QuickSpell{}", + N_("Quick spell {}"), + N_("Hotkey for skill or spell."), DVL_VK_F5 + i, [i]() { if (spselflag) { @@ -1409,68 +1409,81 @@ void InitKeymapActions() QuickCast(i); }, [&]() { return !IsPlayerDead(); }, - }); + i + 1); } for (int i = 0; i < 4; ++i) { - keymapper.AddAction({ - QuickMessages[i].key, + sgOptions.Keymapper.AddAction( + "QuickMessage{}", + N_("Quick Message {}"), + N_("Use Quick Message in chat."), DVL_VK_F9 + i, [i]() { DiabloHotkeyMsg(i); }, - }); + [] { return true; }, + i + 1); } - keymapper.AddAction({ + sgOptions.Keymapper.AddAction( "DecreaseGamma", + N_("Decrease Gamma"), + N_("Reduce screen brightness."), 'G', DecreaseGamma, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "IncreaseGamma", + N_("Increase Gamma"), + N_("Increase screen brightness."), 'F', IncreaseGamma, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "Inventory", + N_("Inventory"), + N_("Open Inventory screen."), 'I', InventoryKeyPressed, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "Character", + N_("Character"), + N_("Open Character screen."), 'C', CharacterSheetKeyPressed, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "QuestLog", + N_("Quest log"), + N_("Open Quest log."), 'Q', QuestLogKeyPressed, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "Zoom", + N_("Zoom"), + N_("Zoom Game Screen."), 'Z', [] { zoomflag = !zoomflag; CalcViewportGeometry(); }, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "DisplaySpells", + N_("Speedbook"), + N_("Open Speedbook."), 'S', DisplaySpellsKeyPressed, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "SpellBook", + N_("Spellbook"), + N_("Open Spellbook."), 'B', SpellBookKeyPressed, - [&]() { return !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "GameInfo", + N_("Game info"), + N_("Displays game infos."), 'V', [] { char pszStr[MAX_SEND_STR_LEN]; @@ -1485,11 +1498,12 @@ void InitKeymapActions() sizeof(pszStr)); NetSendCmdString(1 << MyPlayerId, pszStr); }, - [&]() { return !IsPlayerDead(); }, - }); + [&]() { return !IsPlayerDead(); }); for (int i = 0; i < 8; ++i) { - keymapper.AddAction({ - std::string("BeltItem") + std::to_string(i + 1), + sgOptions.Keymapper.AddAction( + "BeltItem{}", + N_("Belt item {}"), + N_("Use Belt item."), '1' + i, [i] { auto &myPlayer = Players[MyPlayerId]; @@ -1498,40 +1512,45 @@ void InitKeymapActions() } }, [&]() { return !IsPlayerDead(); }, - }); + i + 1); } - keymapper.AddAction({ + sgOptions.Keymapper.AddAction( "QuickSave", + N_("Quick save"), + N_("Saves the game."), DVL_VK_F2, [] { gamemenu_save_game(false); }, - [&]() { return !gbIsMultiplayer && !IsPlayerDead(); }, - }); - keymapper.AddAction({ + [&]() { return !gbIsMultiplayer && !IsPlayerDead(); }); + sgOptions.Keymapper.AddAction( "QuickLoad", + N_("Quick load"), + N_("Loads the game."), DVL_VK_F3, [] { gamemenu_load_game(false); }, - [&]() { return !gbIsMultiplayer && gbValidSaveFile && stextflag == STORE_NONE; }, - }); - keymapper.AddAction({ + [&]() { return !gbIsMultiplayer && gbValidSaveFile && stextflag == STORE_NONE; }); + sgOptions.Keymapper.AddAction( "QuitGame", + N_("Quit game"), + N_("Closes the game."), DVL_VK_INVALID, - [] { gamemenu_quit_game(false); }, - }); - keymapper.AddAction({ + [] { gamemenu_quit_game(false); }); + sgOptions.Keymapper.AddAction( "StopHero", + N_("Stop hero"), + N_("Stops walking and cancel pending actions."), DVL_VK_INVALID, [] { Players[MyPlayerId].Stop(); }, - [&]() { return !IsPlayerDead(); }, - }); + [&]() { return !IsPlayerDead(); }); #ifdef _DEBUG - keymapper.AddAction({ + sgOptions.Keymapper.AddAction( "DebugToggle", + "Debug toggle", + "Programming is like magic.", 'X', [] { DebugToggle = !DebugToggle; }, - [&]() { return true; }, - }); + [&]() { return true; }); #endif } diff --git a/Source/diablo.h b/Source/diablo.h index 3d2a5672b..0aeb9da00 100644 --- a/Source/diablo.h +++ b/Source/diablo.h @@ -7,7 +7,6 @@ #include -#include "controls/keymapper.hpp" #ifdef _DEBUG #include "monstdat.h" #endif @@ -102,7 +101,6 @@ void diablo_color_cyc_logic(); /* rdata */ -extern Keymapper keymapper; #ifdef _DEBUG extern bool DebugDisableNetworkTimeout; #endif diff --git a/Source/loadsave.cpp b/Source/loadsave.cpp index 821aa32ab..479e90fbf 100644 --- a/Source/loadsave.cpp +++ b/Source/loadsave.cpp @@ -7,6 +7,7 @@ #include #include +#include #include diff --git a/Source/options.cpp b/Source/options.cpp index cf9bef666..489a88aaf 100644 --- a/Source/options.cpp +++ b/Source/options.cpp @@ -32,6 +32,7 @@ #define SI_SUPPORT_IOSTREAMS #include +#include "control.h" #include "diablo.h" #include "discord/discord.h" #include "engine/demomode.h" @@ -161,6 +162,17 @@ float GetIniFloat(const char *sectionName, const char *keyName, float defaultVal return (float)GetIni().GetDoubleValue(sectionName, keyName, defaultValue); } +bool GetIniValue(const char *sectionName, const char *keyName, char *string, int stringSize, const char *defaultString = "") +{ + const char *value = GetIni().GetValue(sectionName, keyName); + if (value == nullptr) { + CopyUtf8(string, defaultString, stringSize); + return false; + } + CopyUtf8(string, value, stringSize); + return true; +} + bool GetIniStringVector(const char *sectionName, const char *keyName, std::vector &stringValues) { std::list values; @@ -197,6 +209,13 @@ void SetIniValue(const char *keyname, const char *valuename, float value) GetIni().SetDoubleValue(keyname, valuename, value, nullptr, true); } +void SetIniValue(const char *sectionName, const char *keyName, const char *value) +{ + IniChangedChecker changedChecker(sectionName, keyName); + auto &ini = GetIni(); + ini.SetValue(sectionName, keyName, value, nullptr, true); +} + void SetIniValue(const char *keyname, const char *valuename, const std::vector &stringValues) { IniChangedChecker changedChecker(keyname, valuename); @@ -307,25 +326,6 @@ void OptionAudioChanged() } // namespace -void SetIniValue(const char *sectionName, const char *keyName, const char *value, int len) -{ - IniChangedChecker changedChecker(sectionName, keyName); - auto &ini = GetIni(); - std::string stringValue(value, len != 0 ? len : strlen(value)); - ini.SetValue(sectionName, keyName, stringValue.c_str(), nullptr, true); -} - -bool GetIniValue(const char *sectionName, const char *keyName, char *string, int stringSize, const char *defaultString) -{ - const char *value = GetIni().GetValue(sectionName, keyName); - if (value == nullptr) { - CopyUtf8(string, defaultString, stringSize); - return false; - } - CopyUtf8(string, value, stringSize); - return true; -} - /** Game options */ Options sgOptions; bool sbWasOptionsLoaded = false; @@ -365,8 +365,6 @@ void LoadOptions() sgOptions.Controller.bRearTouch = GetIniBool("Controller", "Enable Rear Touchpad", true); #endif - keymapper.Load(); - if (demo::IsRunning()) demo::OverrideOptions(); @@ -409,8 +407,6 @@ void SaveOptions() SetIniValue("Controller", "Enable Rear Touchpad", sgOptions.Controller.bRearTouch); #endif - keymapper.Save(); - SaveIni(); } @@ -1132,4 +1128,166 @@ std::vector LanguageOptions::GetEntries() }; } +KeymapperOptions::KeymapperOptions() + : OptionCategoryBase("Keymapping", N_("Keymapping"), N_("Keymapping Settings")) +{ + // Insert all supported keys: a-z, 0-9 and F1-F12. + keyIDToKeyName.reserve(('Z' - 'A' + 1) + ('9' - '0' + 1) + 12); + for (char c = 'A'; c <= 'Z'; ++c) { + keyIDToKeyName.emplace(c, std::string(1, c)); + } + for (char c = '0'; c <= '9'; ++c) { + keyIDToKeyName.emplace(c, std::string(1, c)); + } + for (int i = 0; i < 12; ++i) { + keyIDToKeyName.emplace(DVL_VK_F1 + i, fmt::format("F{}", i + 1)); + } + + keyNameToKeyID.reserve(keyIDToKeyName.size()); + for (const auto &kv : keyIDToKeyName) { + keyNameToKeyID.emplace(kv.second, kv.first); + } +} + +std::vector KeymapperOptions::GetEntries() +{ + std::vector entries; + for (auto &action : actions) { + entries.push_back(action.get()); + } + return entries; +} + +KeymapperOptions::Action::Action(string_view key, string_view name, string_view description, int defaultKey, std::function action, std::function enable, int index) + : OptionEntryBase(key, OptionEntryFlags::None, name, description) + , defaultKey(defaultKey) + , action(action) + , enable(enable) + , dynamicIndex(index) +{ + if (index >= 0) { + dynamicKey = fmt::format(key, index); + this->key = dynamicKey; + } +} + +string_view KeymapperOptions::Action::GetName() const +{ + if (dynamicIndex < 0) + return name; + dynamicName = fmt::format(_(name.data()), dynamicIndex); + return dynamicName; +} + +void KeymapperOptions::Action::LoadFromIni(string_view category) +{ + std::array result; + if (!GetIniValue(category.data(), key.data(), result.data(), result.size())) { + SetValue(defaultKey); + return; // Use the default key if no key has been set. + } + + std::string readKey = result.data(); + if (readKey.empty()) { + SetValue(DVL_VK_INVALID); + return; + } + + auto keyIt = sgOptions.Keymapper.keyNameToKeyID.find(readKey); + if (keyIt == sgOptions.Keymapper.keyNameToKeyID.end()) { + // Use the default key if the key is unknown. + Log("Keymapper: unknown key '{}'", readKey); + SetValue(defaultKey); + return; + } + + // Store the key in action.key and in the map so we can save() the + // actions while keeping the same order as they have been added. + SetValue(keyIt->second); +} +void KeymapperOptions::Action::SaveToIni(string_view category) const +{ + if (boundKey == DVL_VK_INVALID) { + // Just add an empty config entry if the action is unbound. + SetIniValue(category.data(), key.data(), ""); + } + auto keyNameIt = sgOptions.Keymapper.keyIDToKeyName.find(boundKey); + if (keyNameIt == sgOptions.Keymapper.keyIDToKeyName.end()) { + Log("Keymapper: no name found for key '{}'", key); + return; + } + SetIniValue(category.data(), key.data(), keyNameIt->second.c_str()); +} + +string_view KeymapperOptions::Action::GetValueDescription() const +{ + if (boundKey == DVL_VK_INVALID) + return ""; + auto keyNameIt = sgOptions.Keymapper.keyIDToKeyName.find(boundKey); + if (keyNameIt == sgOptions.Keymapper.keyIDToKeyName.end()) { + return ""; + } + return keyNameIt->second.c_str(); +} + +bool KeymapperOptions::Action::SetValue(int value) +{ + if (value != DVL_VK_INVALID && sgOptions.Keymapper.keyIDToKeyName.find(value) == sgOptions.Keymapper.keyIDToKeyName.end()) { + // Ignore invalid key values + return false; + } + + // Remove old key + if (boundKey != DVL_VK_INVALID) { + sgOptions.Keymapper.keyIDToAction.erase(boundKey); + boundKey = DVL_VK_INVALID; + } + + // Add new key + if (value != DVL_VK_INVALID) { + auto it = sgOptions.Keymapper.keyIDToAction.find(value); + if (it != sgOptions.Keymapper.keyIDToAction.end()) { + // Warn about overwriting keys. + Log("Keymapper: key '{}' is already bound to action '{}', overwriting", value, it->second.get().name); + it->second.get().boundKey = DVL_VK_INVALID; + } + + sgOptions.Keymapper.keyIDToAction.insert_or_assign(value, *this); + boundKey = value; + } + + return true; +} + +void KeymapperOptions::AddAction(string_view key, string_view name, string_view description, int defaultKey, std::function action, std::function enable, int index) +{ + actions.push_back(std::unique_ptr(new Action(key, name, description, defaultKey, action, enable, index))); +} + +void KeymapperOptions::KeyPressed(int key) const +{ + auto it = keyIDToAction.find(key); + if (it == keyIDToAction.end()) + return; // Ignore unmapped keys. + + const auto &action = it->second; + + // Check that the action can be triggered and that the chat textbox is not + // open. + if (!action.get().enable() || talkflag) + return; + + action.get().action(); +} + +string_view KeymapperOptions::KeyNameForAction(string_view actionName) const +{ + for (auto &action : actions) { + if (action->key == actionName && action->boundKey != DVL_VK_INVALID) { + return action->GetValueDescription(); + } + } + return ""; +} + } // namespace devilution diff --git a/Source/options.h b/Source/options.h index 7c0f60371..a4b5ac7b0 100644 --- a/Source/options.h +++ b/Source/options.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include @@ -42,6 +43,7 @@ enum class ScalingQuality { enum class OptionEntryType { Boolean, List, + Key, }; enum class OptionEntryFlags { @@ -75,7 +77,7 @@ public: , description(description) { } - [[nodiscard]] string_view GetName() const; + [[nodiscard]] virtual string_view GetName() const; [[nodiscard]] string_view GetDescription() const; [[nodiscard]] virtual OptionEntryType GetType() const = 0; [[nodiscard]] OptionEntryFlags GetFlags() const; @@ -519,6 +521,56 @@ struct LanguageOptions : OptionCategoryBase { OptionEntryLanguageCode code; }; +/** The Keymapper maps keys to actions. */ +struct KeymapperOptions : OptionCategoryBase { + /** + * Action represents an action that can be triggered using a keyboard + * shortcut. + */ + class Action final : public OptionEntryBase { + public: + [[nodiscard]] string_view GetName() const override; + [[nodiscard]] OptionEntryType GetType() const override + { + return OptionEntryType::Key; + } + + void LoadFromIni(string_view category) override; + void SaveToIni(string_view category) const override; + + [[nodiscard]] string_view GetValueDescription() const override; + + bool SetValue(int value); + + private: + Action(string_view key, string_view name, string_view description, int defaultKey, std::function action, std::function enable, int index); + int defaultKey; + std::function action; + std::function enable; + int boundKey = DVL_VK_INVALID; + int dynamicIndex; + std::string dynamicKey; + mutable std::string dynamicName; + + friend struct KeymapperOptions; + }; + + KeymapperOptions(); + std::vector GetEntries() override; + + void AddAction( + string_view key, string_view name, string_view description, int defaultKey, + std::function action, std::function enable = [] { return true; }, int index = -1); + void KeyPressed(int key) const; + string_view KeyNameForAction(string_view actionName) const; + +private: + std::vector> actions; + std::unordered_map> keyIDToAction; + std::unordered_map keyIDToKeyName; + std::unordered_map keyNameToKeyID; +}; + struct Options { StartUpOptions StartUp; DiabloOptions Diablo; @@ -530,6 +582,7 @@ struct Options { NetworkOptions Network; ChatOptions Chat; LanguageOptions Language; + KeymapperOptions Keymapper; [[nodiscard]] std::vector GetCategories() { @@ -544,13 +597,11 @@ struct Options { &Network, &Chat, &Language, + &Keymapper, }; } }; -bool GetIniValue(const char *sectionName, const char *keyName, char *string, int stringSize, const char *defaultString = ""); -void SetIniValue(const char *sectionName, const char *keyName, const char *value, int len = 0); - extern DVL_API_FOR_TEST Options sgOptions; extern bool sbWasOptionsLoaded; diff --git a/Source/panels/spell_list.cpp b/Source/panels/spell_list.cpp index 37318ed6c..ca8074ed1 100644 --- a/Source/panels/spell_list.cpp +++ b/Source/panels/spell_list.cpp @@ -3,10 +3,10 @@ #include #include "control.h" -#include "controls/keymapper.hpp" #include "engine.h" #include "engine/render/text_render.hpp" #include "inv_iterators.hpp" +#include "options.h" #include "palette.h" #include "panels/spell_icons.hpp" #include "player.h" @@ -17,8 +17,6 @@ namespace devilution { -extern std::array quickSpellActionIndexes; - namespace { void PrintSBookSpellType(const Surface &out, Point position, const std::string &text, uint8_t rectColorIndex) @@ -53,10 +51,10 @@ void PrintSBookSpellType(const Surface &out, Point position, const std::string & DrawString(out, text, position, UiFlags::ColorWhite); } -void PrintSBookHotkey(const Surface &out, Point position, const std::string &text) +void PrintSBookHotkey(const Surface &out, Point position, const string_view text) { // Align the hot key text with the top-right corner of the spell icon - position += Displacement { SPLICONLENGTH - (GetLineWidth(text.c_str()) + 5), 5 - SPLICONLENGTH }; + position += Displacement { SPLICONLENGTH - (GetLineWidth(text.data()) + 5), 5 - SPLICONLENGTH }; // Draw a drop shadow below and to the left of the text DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); @@ -83,13 +81,14 @@ bool GetSpellListSelection(spell_id &pSpell, spell_type &pSplType) return false; } -std::optional GetHotkeyName(spell_id spellId, spell_type spellType) +std::optional GetHotkeyName(spell_id spellId, spell_type spellType) { auto &myPlayer = Players[MyPlayerId]; for (int t = 0; t < 4; t++) { if (myPlayer._pSplHotKey[t] != spellId || myPlayer._pSplTHotKey[t] != spellType) continue; - return keymapper.KeyNameForAction(quickSpellActionIndexes[t]); + auto quickSpellActionKey = fmt::format("QuickSpell{}", t + 1); + return sgOptions.Keymapper.KeyNameForAction(quickSpellActionKey); } return {}; } @@ -117,7 +116,7 @@ void DrawSpell(const Surface &out) const Point position { PANEL_X + 565, PANEL_Y + 119 }; DrawSpellCel(out, position, nCel); - std::optional hotkeyName = GetHotkeyName(spl, myPlayer._pRSplType); + std::optional hotkeyName = GetHotkeyName(spl, myPlayer._pRSplType); if (hotkeyName) PrintSBookHotkey(out, position, *hotkeyName); } @@ -146,7 +145,7 @@ void DrawSpellList(const Surface &out) SetSpellTrans(transType); DrawSpellCel(out, spellListItem.location, SpellITbl[static_cast(spellId)]); - std::optional hotkeyName = GetHotkeyName(spellId, spellListItem.type); + std::optional hotkeyName = GetHotkeyName(spellId, spellListItem.type); if (hotkeyName) PrintSBookHotkey(out, spellListItem.location, *hotkeyName);