Change Keymapper to OptionCategory/OptionEntry
This commit is contained in:
parent
901661880e
commit
419fe7b7ec
10 changed files with 323 additions and 303 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Keymapper::ActionIndex, 4> quickSpellActionIndexes;
|
||||
|
||||
/** Maps from attribute_id to the rectangle on screen used for attribute increment buttons. */
|
||||
Rectangle ChrBtnsRect[4] = {
|
||||
{ { 137, 138 }, { 41, 22 } },
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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<Keymapper::ActionIndex, 4> 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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,6 @@
|
|||
|
||||
#include <cstdint>
|
||||
|
||||
#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
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
#include <climits>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <SDL.h>
|
||||
|
||||
|
|
|
|||
|
|
@ -32,6 +32,7 @@
|
|||
#define SI_SUPPORT_IOSTREAMS
|
||||
#include <SimpleIni.h>
|
||||
|
||||
#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<std::string> &stringValues)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
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<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
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
|
||||
#include <SDL_version.h>
|
||||
|
||||
|
|
@ -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<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 {
|
||||
StartUpOptions StartUp;
|
||||
DiabloOptions Diablo;
|
||||
|
|
@ -530,6 +582,7 @@ struct Options {
|
|||
NetworkOptions Network;
|
||||
ChatOptions Chat;
|
||||
LanguageOptions Language;
|
||||
KeymapperOptions Keymapper;
|
||||
|
||||
[[nodiscard]] std::vector<OptionCategoryBase *> 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;
|
||||
|
||||
|
|
|
|||
|
|
@ -3,10 +3,10 @@
|
|||
#include <fmt/format.h>
|
||||
|
||||
#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<Keymapper::ActionIndex, 4> 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<std::string> GetHotkeyName(spell_id spellId, spell_type spellType)
|
||||
std::optional<string_view> 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<std::string> hotkeyName = GetHotkeyName(spl, myPlayer._pRSplType);
|
||||
std::optional<string_view> 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<size_t>(spellId)]);
|
||||
|
||||
std::optional<std::string> hotkeyName = GetHotkeyName(spellId, spellListItem.type);
|
||||
std::optional<string_view> hotkeyName = GetHotkeyName(spellId, spellListItem.type);
|
||||
|
||||
if (hotkeyName)
|
||||
PrintSBookHotkey(out, spellListItem.location, *hotkeyName);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue