Browse Source

Change Keymapper to OptionCategory/OptionEntry

pull/3833/head
obligaron 4 years ago committed by Anders Jenbo
parent
commit
419fe7b7ec
  1. 1
      Source/CMakeLists.txt
  2. 3
      Source/control.cpp
  3. 133
      Source/controls/keymapper.cpp
  4. 69
      Source/controls/keymapper.hpp
  5. 137
      Source/diablo.cpp
  6. 2
      Source/diablo.h
  7. 1
      Source/loadsave.cpp
  8. 204
      Source/options.cpp
  9. 59
      Source/options.h
  10. 17
      Source/panels/spell_list.cpp

1
Source/CMakeLists.txt

@ -81,7 +81,6 @@ set(libdevilutionx_SRCS
controls/menu_controls.cpp controls/menu_controls.cpp
controls/modifier_hints.cpp controls/modifier_hints.cpp
controls/plrctrls.cpp controls/plrctrls.cpp
controls/keymapper.cpp
engine/animationinfo.cpp engine/animationinfo.cpp
engine/demomode.cpp engine/demomode.cpp
engine/load_cel.cpp engine/load_cel.cpp

3
Source/control.cpp

@ -14,7 +14,6 @@
#include "DiabloUI/art.h" #include "DiabloUI/art.h"
#include "DiabloUI/art_draw.h" #include "DiabloUI/art_draw.h"
#include "automap.h" #include "automap.h"
#include "controls/keymapper.hpp"
#include "cursor.h" #include "cursor.h"
#include "engine/cel_sprite.hpp" #include "engine/cel_sprite.hpp"
#include "engine/load_cel.hpp" #include "engine/load_cel.hpp"
@ -93,8 +92,6 @@ const Rectangle &GetRightPanel()
return RightPanel; return RightPanel;
} }
extern std::array<Keymapper::ActionIndex, 4> quickSpellActionIndexes;
/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */ /** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */
Rectangle ChrBtnsRect[4] = { Rectangle ChrBtnsRect[4] = {
{ { 137, 138 }, { 41, 22 } }, { { 137, 138 }, { 41, 22 } },

133
Source/controls/keymapper.cpp

@ -1,133 +0,0 @@
#include "keymapper.hpp"
#include <cassert>
#include <fmt/format.h>
#include <utility>
#include <SDL.h>
#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<char, 64> 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

69
Source/controls/keymapper.hpp

@ -1,69 +0,0 @@
#pragma once
#include <cstddef>
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
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<void()> action)
: name(name)
, defaultKey(defaultKey)
, action(action)
, enable([] { return true; })
{
}
Action(const std::string &name, int defaultKey, std::function<void()> action, std::function<bool()> enable)
: name(name)
, defaultKey(defaultKey)
, action(action)
, enable(enable)
{
}
void operator()() const
{
action();
}
private:
std::string name;
int defaultKey;
std::function<void()> action;
std::function<bool()> 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<Action> actions;
std::unordered_map<int, std::reference_wrapper<Action>> keyIDToAction;
std::unordered_map<int, std::string> keyIDToKeyName;
std::unordered_map<std::string, int> keyNameToKeyID;
};
} // namespace devilution

137
Source/diablo.cpp

@ -19,7 +19,6 @@
#include "debug.h" #include "debug.h"
#endif #endif
#include "DiabloUI/diabloui.h" #include "DiabloUI/diabloui.h"
#include "controls/keymapper.hpp"
#include "controls/plrctrls.h" #include "controls/plrctrls.h"
#include "controls/touch/gamepad.h" #include "controls/touch/gamepad.h"
#include "controls/touch/renderers.h" #include "controls/touch/renderers.h"
@ -105,8 +104,6 @@ bool gbQuietMode = false;
clicktype sgbMouseDown; clicktype sgbMouseDown;
uint16_t gnTickDelay = 50; uint16_t gnTickDelay = 50;
char gszProductName[64] = "DevilutionX vUnknown"; char gszProductName[64] = "DevilutionX vUnknown";
Keymapper keymapper;
std::array<Keymapper::ActionIndex, 4> quickSpellActionIndexes;
#ifdef _DEBUG #ifdef _DEBUG
bool DebugDisableNetworkTimeout = false; bool DebugDisableNetworkTimeout = false;
@ -443,7 +440,7 @@ void PressKey(int vkey)
if (sgnTimeoutCurs != CURSOR_NONE) { if (sgnTimeoutCurs != CURSOR_NONE) {
return; return;
} }
keymapper.KeyPressed(vkey); sgOptions.Keymapper.KeyPressed(vkey);
if (vkey == DVL_VK_RETURN) { if (vkey == DVL_VK_RETURN) {
if (GetAsyncKeyState(DVL_VK_MENU)) if (GetAsyncKeyState(DVL_VK_MENU))
sgOptions.Graphics.fullscreen.SetValue(!IsFullScreen()); sgOptions.Graphics.fullscreen.SetValue(!IsFullScreen());
@ -475,7 +472,7 @@ void PressKey(int vkey)
return; return;
} }
keymapper.KeyPressed(vkey); sgOptions.Keymapper.KeyPressed(vkey);
if (vkey == DVL_VK_RETURN) { if (vkey == DVL_VK_RETURN) {
if (GetAsyncKeyState(DVL_VK_MENU)) { if (GetAsyncKeyState(DVL_VK_MENU)) {
@ -1388,15 +1385,18 @@ bool IsPlayerDead()
void InitKeymapActions() void InitKeymapActions()
{ {
keymapper.AddAction({ sgOptions.Keymapper.AddAction(
"Help", "Help",
N_("Help"),
N_("Open Help Screen."),
DVL_VK_F1, DVL_VK_F1,
HelpKeyPressed, HelpKeyPressed,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
});
for (int i = 0; i < 4; ++i) { for (int i = 0; i < 4; ++i) {
quickSpellActionIndexes[i] = keymapper.AddAction({ sgOptions.Keymapper.AddAction(
std::string("QuickSpell") + std::to_string(i + 1), "QuickSpell{}",
N_("Quick spell {}"),
N_("Hotkey for skill or spell."),
DVL_VK_F5 + i, DVL_VK_F5 + i,
[i]() { [i]() {
if (spselflag) { if (spselflag) {
@ -1409,68 +1409,81 @@ void InitKeymapActions()
QuickCast(i); QuickCast(i);
}, },
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); },
}); i + 1);
} }
for (int i = 0; i < 4; ++i) { for (int i = 0; i < 4; ++i) {
keymapper.AddAction({ sgOptions.Keymapper.AddAction(
QuickMessages[i].key, "QuickMessage{}",
N_("Quick Message {}"),
N_("Use Quick Message in chat."),
DVL_VK_F9 + i, DVL_VK_F9 + i,
[i]() { DiabloHotkeyMsg(i); }, [i]() { DiabloHotkeyMsg(i); },
}); [] { return true; },
i + 1);
} }
keymapper.AddAction({ sgOptions.Keymapper.AddAction(
"DecreaseGamma", "DecreaseGamma",
N_("Decrease Gamma"),
N_("Reduce screen brightness."),
'G', 'G',
DecreaseGamma, DecreaseGamma,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"IncreaseGamma", "IncreaseGamma",
N_("Increase Gamma"),
N_("Increase screen brightness."),
'F', 'F',
IncreaseGamma, IncreaseGamma,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"Inventory", "Inventory",
N_("Inventory"),
N_("Open Inventory screen."),
'I', 'I',
InventoryKeyPressed, InventoryKeyPressed,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"Character", "Character",
N_("Character"),
N_("Open Character screen."),
'C', 'C',
CharacterSheetKeyPressed, CharacterSheetKeyPressed,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"QuestLog", "QuestLog",
N_("Quest log"),
N_("Open Quest log."),
'Q', 'Q',
QuestLogKeyPressed, QuestLogKeyPressed,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"Zoom", "Zoom",
N_("Zoom"),
N_("Zoom Game Screen."),
'Z', 'Z',
[] { [] {
zoomflag = !zoomflag; zoomflag = !zoomflag;
CalcViewportGeometry(); CalcViewportGeometry();
}, },
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"DisplaySpells", "DisplaySpells",
N_("Speedbook"),
N_("Open Speedbook."),
'S', 'S',
DisplaySpellsKeyPressed, DisplaySpellsKeyPressed,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"SpellBook", "SpellBook",
N_("Spellbook"),
N_("Open Spellbook."),
'B', 'B',
SpellBookKeyPressed, SpellBookKeyPressed,
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"GameInfo", "GameInfo",
N_("Game info"),
N_("Displays game infos."),
'V', 'V',
[] { [] {
char pszStr[MAX_SEND_STR_LEN]; char pszStr[MAX_SEND_STR_LEN];
@ -1485,11 +1498,12 @@ void InitKeymapActions()
sizeof(pszStr)); sizeof(pszStr));
NetSendCmdString(1 << MyPlayerId, pszStr); NetSendCmdString(1 << MyPlayerId, pszStr);
}, },
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
});
for (int i = 0; i < 8; ++i) { for (int i = 0; i < 8; ++i) {
keymapper.AddAction({ sgOptions.Keymapper.AddAction(
std::string("BeltItem") + std::to_string(i + 1), "BeltItem{}",
N_("Belt item {}"),
N_("Use Belt item."),
'1' + i, '1' + i,
[i] { [i] {
auto &myPlayer = Players[MyPlayerId]; auto &myPlayer = Players[MyPlayerId];
@ -1498,40 +1512,45 @@ void InitKeymapActions()
} }
}, },
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); },
}); i + 1);
} }
keymapper.AddAction({ sgOptions.Keymapper.AddAction(
"QuickSave", "QuickSave",
N_("Quick save"),
N_("Saves the game."),
DVL_VK_F2, DVL_VK_F2,
[] { gamemenu_save_game(false); }, [] { gamemenu_save_game(false); },
[&]() { return !gbIsMultiplayer && !IsPlayerDead(); }, [&]() { return !gbIsMultiplayer && !IsPlayerDead(); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"QuickLoad", "QuickLoad",
N_("Quick load"),
N_("Loads the game."),
DVL_VK_F3, DVL_VK_F3,
[] { gamemenu_load_game(false); }, [] { gamemenu_load_game(false); },
[&]() { return !gbIsMultiplayer && gbValidSaveFile && stextflag == STORE_NONE; }, [&]() { return !gbIsMultiplayer && gbValidSaveFile && stextflag == STORE_NONE; });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"QuitGame", "QuitGame",
N_("Quit game"),
N_("Closes the game."),
DVL_VK_INVALID, DVL_VK_INVALID,
[] { gamemenu_quit_game(false); }, [] { gamemenu_quit_game(false); });
}); sgOptions.Keymapper.AddAction(
keymapper.AddAction({
"StopHero", "StopHero",
N_("Stop hero"),
N_("Stops walking and cancel pending actions."),
DVL_VK_INVALID, DVL_VK_INVALID,
[] { Players[MyPlayerId].Stop(); }, [] { Players[MyPlayerId].Stop(); },
[&]() { return !IsPlayerDead(); }, [&]() { return !IsPlayerDead(); });
});
#ifdef _DEBUG #ifdef _DEBUG
keymapper.AddAction({ sgOptions.Keymapper.AddAction(
"DebugToggle", "DebugToggle",
"Debug toggle",
"Programming is like magic.",
'X', 'X',
[] { [] {
DebugToggle = !DebugToggle; DebugToggle = !DebugToggle;
}, },
[&]() { return true; }, [&]() { return true; });
});
#endif #endif
} }

2
Source/diablo.h

@ -7,7 +7,6 @@
#include <cstdint> #include <cstdint>
#include "controls/keymapper.hpp"
#ifdef _DEBUG #ifdef _DEBUG
#include "monstdat.h" #include "monstdat.h"
#endif #endif
@ -102,7 +101,6 @@ void diablo_color_cyc_logic();
/* rdata */ /* rdata */
extern Keymapper keymapper;
#ifdef _DEBUG #ifdef _DEBUG
extern bool DebugDisableNetworkTimeout; extern bool DebugDisableNetworkTimeout;
#endif #endif

1
Source/loadsave.cpp

@ -7,6 +7,7 @@
#include <climits> #include <climits>
#include <cstring> #include <cstring>
#include <unordered_map>
#include <SDL.h> #include <SDL.h>

204
Source/options.cpp

@ -32,6 +32,7 @@
#define SI_SUPPORT_IOSTREAMS #define SI_SUPPORT_IOSTREAMS
#include <SimpleIni.h> #include <SimpleIni.h>
#include "control.h"
#include "diablo.h" #include "diablo.h"
#include "discord/discord.h" #include "discord/discord.h"
#include "engine/demomode.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); 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<std::string> &stringValues) bool GetIniStringVector(const char *sectionName, const char *keyName, std::vector<std::string> &stringValues)
{ {
std::list<CSimpleIni::Entry> values; std::list<CSimpleIni::Entry> values;
@ -197,6 +209,13 @@ void SetIniValue(const char *keyname, const char *valuename, float value)
GetIni().SetDoubleValue(keyname, valuename, value, nullptr, true); 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<std::string> &stringValues) void SetIniValue(const char *keyname, const char *valuename, const std::vector<std::string> &stringValues)
{ {
IniChangedChecker changedChecker(keyname, valuename); IniChangedChecker changedChecker(keyname, valuename);
@ -307,25 +326,6 @@ void OptionAudioChanged()
} // namespace } // 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 */ /** Game options */
Options sgOptions; Options sgOptions;
bool sbWasOptionsLoaded = false; bool sbWasOptionsLoaded = false;
@ -365,8 +365,6 @@ void LoadOptions()
sgOptions.Controller.bRearTouch = GetIniBool("Controller", "Enable Rear Touchpad", true); sgOptions.Controller.bRearTouch = GetIniBool("Controller", "Enable Rear Touchpad", true);
#endif #endif
keymapper.Load();
if (demo::IsRunning()) if (demo::IsRunning())
demo::OverrideOptions(); demo::OverrideOptions();
@ -409,8 +407,6 @@ void SaveOptions()
SetIniValue("Controller", "Enable Rear Touchpad", sgOptions.Controller.bRearTouch); SetIniValue("Controller", "Enable Rear Touchpad", sgOptions.Controller.bRearTouch);
#endif #endif
keymapper.Save();
SaveIni(); SaveIni();
} }
@ -1132,4 +1128,166 @@ std::vector<OptionEntryBase *> 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<OptionEntryBase *> KeymapperOptions::GetEntries()
{
std::vector<OptionEntryBase *> 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<void()> action, std::function<bool()> 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<char, 64> 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<void()> action, std::function<bool()> enable, int index)
{
actions.push_back(std::unique_ptr<Action>(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 } // namespace devilution

59
Source/options.h

@ -1,6 +1,7 @@
#pragma once #pragma once
#include <cstdint> #include <cstdint>
#include <unordered_map>
#include <SDL_version.h> #include <SDL_version.h>
@ -42,6 +43,7 @@ enum class ScalingQuality {
enum class OptionEntryType { enum class OptionEntryType {
Boolean, Boolean,
List, List,
Key,
}; };
enum class OptionEntryFlags { enum class OptionEntryFlags {
@ -75,7 +77,7 @@ public:
, description(description) , description(description)
{ {
} }
[[nodiscard]] string_view GetName() const; [[nodiscard]] virtual string_view GetName() const;
[[nodiscard]] string_view GetDescription() const; [[nodiscard]] string_view GetDescription() const;
[[nodiscard]] virtual OptionEntryType GetType() const = 0; [[nodiscard]] virtual OptionEntryType GetType() const = 0;
[[nodiscard]] OptionEntryFlags GetFlags() const; [[nodiscard]] OptionEntryFlags GetFlags() const;
@ -519,6 +521,56 @@ struct LanguageOptions : OptionCategoryBase {
OptionEntryLanguageCode code; 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<void()> action, std::function<bool()> enable, int index);
int defaultKey;
std::function<void()> action;
std::function<bool()> enable;
int boundKey = DVL_VK_INVALID;
int dynamicIndex;
std::string dynamicKey;
mutable std::string dynamicName;
friend struct KeymapperOptions;
};
KeymapperOptions();
std::vector<OptionEntryBase *> GetEntries() override;
void AddAction(
string_view key, string_view name, string_view description, int defaultKey,
std::function<void()> action, std::function<bool()> enable = [] { return true; }, int index = -1);
void KeyPressed(int key) const;
string_view KeyNameForAction(string_view actionName) const;
private:
std::vector<std::unique_ptr<Action>> actions;
std::unordered_map<int, std::reference_wrapper<Action>> keyIDToAction;
std::unordered_map<int, std::string> keyIDToKeyName;
std::unordered_map<std::string, int> keyNameToKeyID;
};
struct Options { struct Options {
StartUpOptions StartUp; StartUpOptions StartUp;
DiabloOptions Diablo; DiabloOptions Diablo;
@ -530,6 +582,7 @@ struct Options {
NetworkOptions Network; NetworkOptions Network;
ChatOptions Chat; ChatOptions Chat;
LanguageOptions Language; LanguageOptions Language;
KeymapperOptions Keymapper;
[[nodiscard]] std::vector<OptionCategoryBase *> GetCategories() [[nodiscard]] std::vector<OptionCategoryBase *> GetCategories()
{ {
@ -544,13 +597,11 @@ struct Options {
&Network, &Network,
&Chat, &Chat,
&Language, &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 DVL_API_FOR_TEST Options sgOptions;
extern bool sbWasOptionsLoaded; extern bool sbWasOptionsLoaded;

17
Source/panels/spell_list.cpp

@ -3,10 +3,10 @@
#include <fmt/format.h> #include <fmt/format.h>
#include "control.h" #include "control.h"
#include "controls/keymapper.hpp"
#include "engine.h" #include "engine.h"
#include "engine/render/text_render.hpp" #include "engine/render/text_render.hpp"
#include "inv_iterators.hpp" #include "inv_iterators.hpp"
#include "options.h"
#include "palette.h" #include "palette.h"
#include "panels/spell_icons.hpp" #include "panels/spell_icons.hpp"
#include "player.h" #include "player.h"
@ -17,8 +17,6 @@
namespace devilution { namespace devilution {
extern std::array<Keymapper::ActionIndex, 4> quickSpellActionIndexes;
namespace { namespace {
void PrintSBookSpellType(const Surface &out, Point position, const std::string &text, uint8_t rectColorIndex) 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); 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 // 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 // Draw a drop shadow below and to the left of the text
DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack); DrawString(out, text, position + Displacement { -1, 1 }, UiFlags::ColorBlack);
@ -83,13 +81,14 @@ bool GetSpellListSelection(spell_id &pSpell, spell_type &pSplType)
return false; return false;
} }
std::optional<std::string> GetHotkeyName(spell_id spellId, spell_type spellType) std::optional<string_view> GetHotkeyName(spell_id spellId, spell_type spellType)
{ {
auto &myPlayer = Players[MyPlayerId]; auto &myPlayer = Players[MyPlayerId];
for (int t = 0; t < 4; t++) { for (int t = 0; t < 4; t++) {
if (myPlayer._pSplHotKey[t] != spellId || myPlayer._pSplTHotKey[t] != spellType) if (myPlayer._pSplHotKey[t] != spellId || myPlayer._pSplTHotKey[t] != spellType)
continue; continue;
return keymapper.KeyNameForAction(quickSpellActionIndexes[t]); auto quickSpellActionKey = fmt::format("QuickSpell{}", t + 1);
return sgOptions.Keymapper.KeyNameForAction(quickSpellActionKey);
} }
return {}; return {};
} }
@ -117,7 +116,7 @@ void DrawSpell(const Surface &out)
const Point position { PANEL_X + 565, PANEL_Y + 119 }; const Point position { PANEL_X + 565, PANEL_Y + 119 };
DrawSpellCel(out, position, nCel); DrawSpellCel(out, position, nCel);
std::optional<std::string> hotkeyName = GetHotkeyName(spl, myPlayer._pRSplType); std::optional<string_view> hotkeyName = GetHotkeyName(spl, myPlayer._pRSplType);
if (hotkeyName) if (hotkeyName)
PrintSBookHotkey(out, position, *hotkeyName); PrintSBookHotkey(out, position, *hotkeyName);
} }
@ -146,7 +145,7 @@ void DrawSpellList(const Surface &out)
SetSpellTrans(transType); SetSpellTrans(transType);
DrawSpellCel(out, spellListItem.location, SpellITbl[static_cast<size_t>(spellId)]); DrawSpellCel(out, spellListItem.location, SpellITbl[static_cast<size_t>(spellId)]);
std::optional<std::string> hotkeyName = GetHotkeyName(spellId, spellListItem.type); std::optional<string_view> hotkeyName = GetHotkeyName(spellId, spellListItem.type);
if (hotkeyName) if (hotkeyName)
PrintSBookHotkey(out, spellListItem.location, *hotkeyName); PrintSBookHotkey(out, spellListItem.location, *hotkeyName);

Loading…
Cancel
Save