Further Hotkey fixes.
12cceed3d9
broke meta-key releases. This fixes that.
Also fix a much older issue where pressing new keys would not release
less specific hotkeys.
Add tests.
Reported by: Imarok
Tested by: langbart
Fixes #5930
Fixes #5927
Differential Revision: https://code.wildfiregames.com/D3396
This was SVN commit r24675.
This commit is contained in:
parent
85a54b904e
commit
d0a42f2f00
@ -116,8 +116,8 @@ public:
|
||||
{
|
||||
// Load up a fake test hotkey when pressing 'a'.
|
||||
const char* test_hotkey_name = "hotkey.test";
|
||||
g_ConfigDB.SetValueString(CFG_USER, test_hotkey_name, "A");
|
||||
LoadHotkeys();
|
||||
configDB->SetValueString(CFG_USER, test_hotkey_name, "A");
|
||||
LoadHotkeys(*configDB);
|
||||
|
||||
// Load up a test page.
|
||||
const ScriptInterface& scriptInterface = *(g_GUI->GetScriptInterface());
|
||||
@ -189,5 +189,6 @@ public:
|
||||
ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value);
|
||||
TS_ASSERT_EQUALS(hotkey_pressed_value, false);
|
||||
|
||||
UnloadHotkeys();
|
||||
}
|
||||
};
|
||||
|
@ -499,7 +499,7 @@ static void InitPs(bool setup_gui, const CStrW& gui_page, ScriptInterface* srcSc
|
||||
// hotkeys
|
||||
{
|
||||
TIMER(L"ps_lang_hotkeys");
|
||||
LoadHotkeys();
|
||||
LoadHotkeys(g_ConfigDB);
|
||||
}
|
||||
|
||||
if (!setup_gui)
|
||||
|
@ -33,23 +33,31 @@ static bool unified[UNIFIED_LAST - UNIFIED_SHIFT];
|
||||
std::unordered_map<int, KeyMapping> g_HotkeyMap;
|
||||
std::unordered_map<std::string, bool> g_HotkeyStatus;
|
||||
|
||||
namespace {
|
||||
// List of currently pressed hotkeys. This is used to quickly reset hotkeys.
|
||||
// NB: this points to one of g_HotkeyMap's mappings. It works because that map is stable once constructed.
|
||||
std::vector<const SHotkeyMapping*> pressedHotkeys;
|
||||
}
|
||||
|
||||
static_assert(std::is_integral<std::underlying_type<SDL_Scancode>::type>::value, "SDL_Scancode is not an integral enum.");
|
||||
static_assert(SDL_USEREVENT_ == SDL_USEREVENT, "SDL_USEREVENT_ is not the same type as the real SDL_USEREVENT");
|
||||
static_assert(UNUSED_HOTKEY_CODE == SDL_SCANCODE_UNKNOWN);
|
||||
|
||||
// Look up each key binding in the config file and set the mappings for
|
||||
// all key combinations that trigger it.
|
||||
static void LoadConfigBindings()
|
||||
static void LoadConfigBindings(CConfigDB& configDB)
|
||||
{
|
||||
for (const std::pair<const CStr, CConfigValueSet>& configPair : g_ConfigDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey."))
|
||||
for (const std::pair<const CStr, CConfigValueSet>& configPair : configDB.GetValuesWithPrefix(CFG_COMMAND, "hotkey."))
|
||||
{
|
||||
std::string hotkeyName = configPair.first.substr(7); // strip the "hotkey." prefix
|
||||
|
||||
if (configPair.second.empty())
|
||||
// "unused" is kept or the A23->24 migration, this can likely be removed in A25.
|
||||
if (configPair.second.empty() || (configPair.second.size() == 1 && configPair.second.front() == "unused"))
|
||||
{
|
||||
// Unused hotkeys must still be registered in the map to appear in the hotkey editor.
|
||||
SHotkeyMapping unusedCode;
|
||||
unusedCode.name = hotkeyName;
|
||||
unusedCode.primary = SKey{ UNUSED_HOTKEY_CODE };
|
||||
g_HotkeyMap[UNUSED_HOTKEY_CODE].push_back(unusedCode);
|
||||
continue;
|
||||
}
|
||||
@ -82,6 +90,7 @@ static void LoadConfigBindings()
|
||||
SHotkeyMapping bindCode;
|
||||
|
||||
bindCode.name = hotkeyName;
|
||||
bindCode.primary = SKey{ itKey->code };
|
||||
|
||||
for (itKey2 = keyCombination.begin(); itKey2 != keyCombination.end(); ++itKey2)
|
||||
if (itKey != itKey2) // Push any auxiliary keys
|
||||
@ -93,13 +102,15 @@ static void LoadConfigBindings()
|
||||
}
|
||||
}
|
||||
|
||||
void LoadHotkeys()
|
||||
void LoadHotkeys(CConfigDB& configDB)
|
||||
{
|
||||
LoadConfigBindings();
|
||||
pressedHotkeys.clear();
|
||||
LoadConfigBindings(configDB);
|
||||
}
|
||||
|
||||
void UnloadHotkeys()
|
||||
{
|
||||
pressedHotkeys.clear();
|
||||
g_HotkeyMap.clear();
|
||||
g_HotkeyStatus.clear();
|
||||
}
|
||||
@ -234,14 +245,39 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
|
||||
// matching the conditions (i.e. the event with the highest number of auxiliary
|
||||
// keys, providing they're all down)
|
||||
|
||||
bool typeKeyDown = ( ev->ev.type == SDL_KEYDOWN ) || ( ev->ev.type == SDL_MOUSEBUTTONDOWN ) || (ev->ev.type == SDL_MOUSEWHEEL);
|
||||
// Furthermore, we need to support non-conflicting hotkeys triggering at the same time.
|
||||
// This is much more complex code than you might expect. A refactoring could be used.
|
||||
|
||||
std::vector<const char*> pressedHotkeys;
|
||||
std::vector<const SHotkeyMapping*> newPressedHotkeys;
|
||||
std::vector<const char*> releasedHotkeys;
|
||||
size_t closestMapMatch = 0;
|
||||
|
||||
bool release = (ev->ev.type == SDL_KEYUP) || (ev->ev.type == SDL_MOUSEBUTTONUP);
|
||||
|
||||
SKey retrigger = { UNUSED_HOTKEY_CODE };
|
||||
for (const SHotkeyMapping& hotkey : g_HotkeyMap[scancode])
|
||||
{
|
||||
// If the key is being released, any active hotkey is released.
|
||||
if (release)
|
||||
{
|
||||
if (g_HotkeyStatus[hotkey.name])
|
||||
{
|
||||
releasedHotkeys.push_back(hotkey.name.c_str());
|
||||
|
||||
// If we are releasing a key, we possibly need to retrigger less precise hotkeys
|
||||
// (e.g. 'Ctrl + D', if releasing D, we need to retrigger Ctrl hotkeys).
|
||||
// To do this simply, we'll just re-trigger any of the additional required key.
|
||||
if (!hotkey.requires.empty() && retrigger.code == UNUSED_HOTKEY_CODE)
|
||||
for (const SKey& k : hotkey.requires)
|
||||
if (isPressed(k))
|
||||
{
|
||||
retrigger.code = hotkey.requires.front().code;
|
||||
break;
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for no unpermitted keys
|
||||
bool accept = true;
|
||||
for (const SKey& k : hotkey.requires)
|
||||
@ -260,26 +296,49 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
|
||||
if (hotkey.requires.size() + 1 > closestMapMatch)
|
||||
{
|
||||
// Throw away the old less-precise matches
|
||||
pressedHotkeys.clear();
|
||||
releasedHotkeys.clear();
|
||||
newPressedHotkeys.clear();
|
||||
closestMapMatch = hotkey.requires.size() + 1;
|
||||
}
|
||||
if (typeKeyDown)
|
||||
pressedHotkeys.push_back(hotkey.name.c_str());
|
||||
else
|
||||
releasedHotkeys.push_back(hotkey.name.c_str());
|
||||
newPressedHotkeys.push_back(&hotkey);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const char* hotkeyName : pressedHotkeys)
|
||||
// If this is a new key, check if we need to unset any previous hotkey.
|
||||
// NB: this uses unsorted vectors because there are usually very few elements to go through
|
||||
// (and thus it is presumably faster than std::set).
|
||||
if ((ev->ev.type == SDL_KEYDOWN) || (ev->ev.type == SDL_MOUSEBUTTONDOWN))
|
||||
for (const SHotkeyMapping* hotkey : pressedHotkeys)
|
||||
{
|
||||
if (hotkey->requires.size() + 1 < closestMapMatch)
|
||||
releasedHotkeys.push_back(hotkey->name.c_str());
|
||||
else if (std::find(newPressedHotkeys.begin(), newPressedHotkeys.end(), hotkey) == newPressedHotkeys.end())
|
||||
{
|
||||
// We need to check that all 'keys' are still pressed (because of mouse buttons).
|
||||
if (!isPressed(hotkey->primary))
|
||||
continue;
|
||||
for (const SKey& key : hotkey->requires)
|
||||
if (!isPressed(key))
|
||||
continue;
|
||||
newPressedHotkeys.push_back(hotkey);
|
||||
}
|
||||
}
|
||||
|
||||
pressedHotkeys.swap(newPressedHotkeys);
|
||||
|
||||
// Mouse wheel events are released instantly.
|
||||
if (ev->ev.type == SDL_MOUSEWHEEL)
|
||||
for (const SHotkeyMapping* hotkey : pressedHotkeys)
|
||||
releasedHotkeys.push_back(hotkey->name.c_str());
|
||||
|
||||
for (const SHotkeyMapping* hotkey : pressedHotkeys)
|
||||
{
|
||||
// Send a KeyPress event when a hotkey is pressed initially and on mouseButton and mouseWheel events.
|
||||
if (ev->ev.type != SDL_KEYDOWN || ev->ev.key.repeat == 0)
|
||||
{
|
||||
SDL_Event_ hotkeyPressNotification;
|
||||
hotkeyPressNotification.ev.type = SDL_HOTKEYPRESS;
|
||||
hotkeyPressNotification.ev.user.data1 = const_cast<char*>(hotkeyName);
|
||||
hotkeyPressNotification.ev.user.data1 = const_cast<char*>(hotkey->name.c_str());
|
||||
in_push_priority_event(&hotkeyPressNotification);
|
||||
}
|
||||
|
||||
@ -288,7 +347,7 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
|
||||
// On linux, modifier keys (shift, alt, ctrl) are not repeated, see https://github.com/SFML/SFML/issues/122.
|
||||
SDL_Event_ hotkeyDownNotification;
|
||||
hotkeyDownNotification.ev.type = SDL_HOTKEYDOWN;
|
||||
hotkeyDownNotification.ev.user.data1 = const_cast<char*>(hotkeyName);
|
||||
hotkeyDownNotification.ev.user.data1 = const_cast<char*>(hotkey->name.c_str());
|
||||
in_push_priority_event(&hotkeyDownNotification);
|
||||
}
|
||||
|
||||
@ -300,6 +359,15 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
|
||||
in_push_priority_event(&hotkeyNotification);
|
||||
}
|
||||
|
||||
if (retrigger.code != UNUSED_HOTKEY_CODE)
|
||||
{
|
||||
SDL_Event_ phantomKey;
|
||||
phantomKey.ev.type = SDL_KEYDOWN;
|
||||
phantomKey.ev.key.repeat = 0;
|
||||
phantomKey.ev.key.keysym.scancode = static_cast<SDL_Scancode>(retrigger.code);
|
||||
HotkeyInputHandler(&phantomKey);
|
||||
}
|
||||
|
||||
return IN_PASS;
|
||||
}
|
||||
|
||||
|
@ -60,6 +60,7 @@ struct SKey
|
||||
struct SHotkeyMapping
|
||||
{
|
||||
CStr name; // name of the hotkey
|
||||
SKey primary; // the primary key
|
||||
std::vector<SKey> requires; // list of non-primary keys that must also be active
|
||||
};
|
||||
|
||||
@ -73,7 +74,8 @@ extern std::unordered_map<SDL_Scancode_, KeyMapping> g_HotkeyMap;
|
||||
// The current pressed status of hotkeys
|
||||
extern std::unordered_map<std::string, bool> g_HotkeyStatus;
|
||||
|
||||
extern void LoadHotkeys();
|
||||
class CConfigDB;
|
||||
extern void LoadHotkeys(CConfigDB& configDB);
|
||||
extern void UnloadHotkeys();
|
||||
|
||||
extern InReaction HotkeyStateChange(const SDL_Event_* ev);
|
||||
|
@ -21,6 +21,7 @@
|
||||
|
||||
#include "lib/external_libraries/libsdl.h"
|
||||
#include "ps/CLogger.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/Hotkey.h"
|
||||
#include "ps/KeyName.h"
|
||||
#include "scriptinterface/ScriptConversions.h"
|
||||
@ -115,7 +116,7 @@ JS::Value GetScancodeKeyNames(ScriptInterface::CmptPrivate* pCmptPrivate)
|
||||
void ReloadHotkeys(ScriptInterface::CmptPrivate* UNUSED(pCmptPrivate))
|
||||
{
|
||||
UnloadHotkeys();
|
||||
LoadHotkeys();
|
||||
LoadHotkeys(g_ConfigDB);
|
||||
}
|
||||
|
||||
JS::Value GetConflicts(ScriptInterface::CmptPrivate* pCmptPrivate, JS::HandleValue combination)
|
||||
|
181
source/ps/tests/test_Hotkeys.h
Normal file
181
source/ps/tests/test_Hotkeys.h
Normal file
@ -0,0 +1,181 @@
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
*
|
||||
* Permission is hereby granted, free of charge, to any person obtaining
|
||||
* a copy of this software and associated documentation files (the
|
||||
* "Software"), to deal in the Software without restriction, including
|
||||
* without limitation the rights to use, copy, modify, merge, publish,
|
||||
* distribute, sublicense, and/or sell copies of the Software, and to
|
||||
* permit persons to whom the Software is furnished to do so, subject to
|
||||
* the following conditions:
|
||||
*
|
||||
* The above copyright notice and this permission notice shall be included
|
||||
* in all copies or substantial portions of the Software.
|
||||
*
|
||||
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
||||
* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
|
||||
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
|
||||
#include "lib/self_test.h"
|
||||
|
||||
#include "lib/external_libraries/libsdl.h"
|
||||
#include "ps/Hotkey.h"
|
||||
#include "ps/ConfigDB.h"
|
||||
#include "ps/Globals.h"
|
||||
#include "ps/Filesystem.h"
|
||||
|
||||
|
||||
class TestHotkey : public CxxTest::TestSuite
|
||||
{
|
||||
CConfigDB* configDB;
|
||||
|
||||
private:
|
||||
|
||||
void fakeInput(const char* key, bool keyDown)
|
||||
{
|
||||
SDL_Event_ ev;
|
||||
ev.ev.type = keyDown ? SDL_KEYDOWN : SDL_KEYUP;
|
||||
ev.ev.key.repeat = 0;
|
||||
ev.ev.key.keysym.scancode = SDL_GetScancodeFromName(key);
|
||||
GlobalsInputHandler(&ev);
|
||||
HotkeyInputHandler(&ev);
|
||||
while(in_poll_priority_event(&ev))
|
||||
HotkeyStateChange(&ev);
|
||||
}
|
||||
|
||||
public:
|
||||
void setUp()
|
||||
{
|
||||
g_VFS = CreateVfs();
|
||||
TS_ASSERT_OK(g_VFS->Mount(L"config", DataDir()/"_testconfig"));
|
||||
TS_ASSERT_OK(g_VFS->Mount(L"cache", DataDir()/"_testcache"));
|
||||
|
||||
configDB = new CConfigDB;
|
||||
}
|
||||
|
||||
void tearDown()
|
||||
{
|
||||
delete configDB;
|
||||
g_VFS.reset();
|
||||
DeleteDirectory(DataDir()/"_testcache");
|
||||
DeleteDirectory(DataDir()/"_testconfig");
|
||||
}
|
||||
|
||||
void test_Hotkeys()
|
||||
{
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.A", "A");
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B");
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.ABC", "A+B+C");
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.D", "D");
|
||||
configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg");
|
||||
configDB->Reload(CFG_SYSTEM);
|
||||
|
||||
UnloadHotkeys();
|
||||
LoadHotkeys(*configDB);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
/**
|
||||
* Simple check.
|
||||
*/
|
||||
fakeInput("A", true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
fakeInput("A", false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
/**
|
||||
* Hotkey combinations:
|
||||
* - The most precise match only is selected
|
||||
* - Order does not matter.
|
||||
*/
|
||||
fakeInput("A", true);
|
||||
fakeInput("B", true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
fakeInput("A", false);
|
||||
fakeInput("B", false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
|
||||
fakeInput("B", true);
|
||||
fakeInput("A", true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
fakeInput("A", false);
|
||||
fakeInput("B", false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
fakeInput("A", true);
|
||||
fakeInput("B", true);
|
||||
fakeInput("B", false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
/**
|
||||
* Quirk of the implementation: hotkeys are allowed to fire with too many keys.
|
||||
* Further, hotkeys of the same specificity (i.e. same # of required keys)
|
||||
* are allowed to fire at the same time if they don't conflict.
|
||||
* This is required so that e.g. up+left scrolls both up and left at the same time.
|
||||
*/
|
||||
fakeInput("A", true);
|
||||
fakeInput("D", true);
|
||||
// A+D isn't a hotkey; both A and D are active.
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
|
||||
fakeInput("C", true);
|
||||
// A+D+C likewise.
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
|
||||
fakeInput("B", true);
|
||||
// Here D is inactivated because it's lower-specificity than A+B+C (with D being ignored).
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
fakeInput("A", false);
|
||||
fakeInput("B", false);
|
||||
fakeInput("C", false);
|
||||
fakeInput("D", false);
|
||||
|
||||
fakeInput("B", true);
|
||||
fakeInput("D", true);
|
||||
fakeInput("A", true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
UnloadHotkeys();
|
||||
}
|
||||
};
|
Loading…
Reference in New Issue
Block a user