Do not trigger "HotkeyPress" events when releasing a key.
Follows d0a42f2f00
.
Because only the most specific hotkeys can be active at any time,
releasing a key may require re-activating less specific hotkeys.
There were two issues with this behaviour:
- It was buggy, as it only checked one active key, when any still active
key can trigger hotkeys.
- "HotkeyPress" and "HotkeyDown" events where sent, as if the hotkey was
pressed, which was unexpected for most code/users (it is unusual to have
a "Press" event on key release).
This fixes these issues by "silently" re-triggering the hotkeys in such
a case. It also makes it easier for JS code to use "hotkeyPress" instead
of "hotkeyDown" for non-continuous behaviour.
Accepted By: nani
Fixes #6123
Refs #6064 (fixes the problem, but not the code weirdness)
Differential Revision: https://code.wildfiregames.com/D3766
This was SVN commit r25169.
This commit is contained in:
parent
457c538780
commit
4b46c09222
@ -780,12 +780,12 @@ function handleInputAfterGui(ev)
|
||||
|
||||
if (ev.hotkey == "session.highlightguarding")
|
||||
{
|
||||
g_ShowGuarding = (ev.type == "hotkeydown");
|
||||
g_ShowGuarding = (ev.type == "hotkeypress");
|
||||
updateAdditionalHighlight();
|
||||
}
|
||||
else if (ev.hotkey == "session.highlightguarded")
|
||||
{
|
||||
g_ShowGuarded = (ev.type == "hotkeydown");
|
||||
g_ShowGuarded = (ev.type == "hotkeypress");
|
||||
updateAdditionalHighlight();
|
||||
}
|
||||
|
||||
@ -828,7 +828,7 @@ function handleInputAfterGui(ev)
|
||||
}
|
||||
break;
|
||||
|
||||
case "hotkeydown":
|
||||
case "hotkeypress":
|
||||
if (ev.hotkey.indexOf("selection.group.") == 0)
|
||||
{
|
||||
let now = Date.now();
|
||||
|
@ -48,8 +48,11 @@ template<> void ScriptInterface::ToJSVal<SDL_Event_>(const ScriptRequest& rq, JS
|
||||
case SDL_MOUSEBUTTONDOWN: typeName = "mousebuttondown"; break;
|
||||
case SDL_MOUSEBUTTONUP: typeName = "mousebuttonup"; break;
|
||||
case SDL_QUIT: typeName = "quit"; break;
|
||||
case SDL_HOTKEYPRESS: typeName = "hotkeypress"; break;
|
||||
case SDL_HOTKEYDOWN: typeName = "hotkeydown"; break;
|
||||
case SDL_HOTKEYUP: typeName = "hotkeyup"; break;
|
||||
case SDL_HOTKEYPRESS_SILENT: typeName = "hotkeypresssilent"; break;
|
||||
case SDL_HOTKEYUP_SILENT: typeName = "hotkeyupsilent"; break;
|
||||
default: typeName = "(unknown)"; break;
|
||||
}
|
||||
|
||||
@ -111,8 +114,11 @@ template<> void ScriptInterface::ToJSVal<SDL_Event_>(const ScriptRequest& rq, JS
|
||||
SET(obj, "clicks", (int)val.ev.button.clicks);
|
||||
break;
|
||||
}
|
||||
case SDL_HOTKEYPRESS:
|
||||
case SDL_HOTKEYDOWN:
|
||||
case SDL_HOTKEYUP:
|
||||
case SDL_HOTKEYPRESS_SILENT:
|
||||
case SDL_HOTKEYUP_SILENT:
|
||||
{
|
||||
SET(obj, "hotkey", static_cast<const char*>(val.ev.user.data1));
|
||||
break;
|
||||
|
@ -116,7 +116,7 @@ public:
|
||||
{
|
||||
// Load up a fake test hotkey when pressing 'a'.
|
||||
const char* test_hotkey_name = "hotkey.test";
|
||||
configDB->SetValueString(CFG_USER, test_hotkey_name, "A");
|
||||
configDB->SetValueString(CFG_SYSTEM, test_hotkey_name, "A");
|
||||
LoadHotkeys(*configDB);
|
||||
|
||||
// Load up a test page.
|
||||
@ -189,6 +189,7 @@ public:
|
||||
ScriptInterface::FromJSVal(prq, js_hotkey_pressed_value, hotkey_pressed_value);
|
||||
TS_ASSERT_EQUALS(hotkey_pressed_value, false);
|
||||
|
||||
configDB->RemoveValue(CFG_SYSTEM, test_hotkey_name);
|
||||
UnloadHotkeys();
|
||||
}
|
||||
};
|
||||
|
@ -539,14 +539,14 @@ void InitInput()
|
||||
|
||||
in_add_handler(CProfileViewer::InputThunk);
|
||||
|
||||
in_add_handler(conInputHandler);
|
||||
|
||||
in_add_handler(HotkeyInputHandler);
|
||||
|
||||
// gui_handler needs to be registered after (i.e. called before!) the
|
||||
// hotkey handler so that input boxes can be typed in without
|
||||
// setting off hotkeys.
|
||||
in_add_handler(gui_handler);
|
||||
// Likewise for the console.
|
||||
in_add_handler(conInputHandler);
|
||||
|
||||
in_add_handler(touch_input_handler);
|
||||
|
||||
|
@ -31,12 +31,33 @@
|
||||
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;
|
||||
std::unordered_map<std::string, bool> g_HotkeyStatus;
|
||||
|
||||
struct PressedHotkey
|
||||
{
|
||||
PressedHotkey(const SHotkeyMapping* m, bool t) : mapping(m), retriggered(t) {};
|
||||
// NB: this points to one of g_HotkeyMap's mappings. It works because that std::unordered_map is stable once constructed.
|
||||
const SHotkeyMapping* mapping;
|
||||
// Whether the hotkey was triggered by a key release (silences "press" and "up" events).
|
||||
bool retriggered;
|
||||
};
|
||||
|
||||
struct ReleasedHotkey
|
||||
{
|
||||
ReleasedHotkey(const char* n, bool t) : name(n), wasRetriggered(t) {};
|
||||
const char* name;
|
||||
bool wasRetriggered;
|
||||
};
|
||||
|
||||
// List of currently pressed hotkeys. This is used to quickly reset hotkeys.
|
||||
// This is an unsorted vector because there will generally be very few elements,
|
||||
// so it's presumably faster than std::set.
|
||||
std::vector<PressedHotkey> pressedHotkeys;
|
||||
|
||||
// List of active keys relevant for hotkeys.
|
||||
std::vector<SDL_Scancode_> activeScancodes;
|
||||
}
|
||||
|
||||
static_assert(std::is_integral<std::underlying_type<SDL_Scancode>::type>::value, "SDL_Scancode is not an integral enum.");
|
||||
@ -133,9 +154,9 @@ bool isPressed(const SKey& key)
|
||||
|
||||
InReaction HotkeyStateChange(const SDL_Event_* ev)
|
||||
{
|
||||
if (ev->ev.type == SDL_HOTKEYPRESS)
|
||||
if (ev->ev.type == SDL_HOTKEYPRESS || ev->ev.type == SDL_HOTKEYPRESS_SILENT)
|
||||
g_HotkeyStatus[static_cast<const char*>(ev->ev.user.data1)] = true;
|
||||
else if (ev->ev.type == SDL_HOTKEYUP)
|
||||
else if (ev->ev.type == SDL_HOTKEYUP || ev->ev.type == SDL_HOTKEYUP_SILENT)
|
||||
g_HotkeyStatus[static_cast<const char*>(ev->ev.user.data1)] = false;
|
||||
return IN_PASS;
|
||||
}
|
||||
@ -227,68 +248,67 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
|
||||
if (g_HotkeyMap.find(scancode) == g_HotkeyMap.end())
|
||||
return (IN_PASS);
|
||||
|
||||
// Inhibit the dispatch of hotkey events caused by real keys (not fake mouse button
|
||||
// events) while the console is up.
|
||||
bool isReleasedKey = ev->ev.type == SDL_KEYUP || ev->ev.type == SDL_MOUSEBUTTONUP;
|
||||
std::vector<SDL_Scancode_>::iterator it = std::find(activeScancodes.begin(), activeScancodes.end(), scancode);
|
||||
// This prevents duplicates, assuming we might end up in a weird state - feels safer with input.
|
||||
if (isReleasedKey && it != activeScancodes.end())
|
||||
activeScancodes.erase(it);
|
||||
else if (!isReleasedKey && it == activeScancodes.end())
|
||||
activeScancodes.emplace_back(scancode);
|
||||
|
||||
bool consoleCapture = false;
|
||||
/**
|
||||
* Hotkey behaviour spec (see also tests):
|
||||
* - If both 'F' and 'Ctrl+F' are hotkeys, and Ctrl & F keys are down, then the more specific one only is fired ('Ctrl+F' here).
|
||||
* - If 'Ctrl+F' and 'Ctrl+A' are both hotkeys, both may fire simulatenously (respectively without Ctrl).
|
||||
* - However, per the first point, 'Ctrl+Shift+F' would fire alone in that situation.
|
||||
* - "Press" is sent once, when the hotkey is initially triggered.
|
||||
* - "Up" is sent once, when the hotkey is released or superseded by a more specific hotkey.
|
||||
* - "Down" is sent repeatedly, and is also sent alongside the inital "Press".
|
||||
* - As a special case (see below), "Down" is not sent alongside "PressSilent".
|
||||
* - If 'Ctrl+F' is active, and 'Ctrl' is released, 'F' must become active again.
|
||||
* - However, the "Press" event is _not_ fired. Instead, "PressSilent" is.
|
||||
* - Likewise, once 'F' is released, the "Up" event will be a "UpSilent".
|
||||
* (the reason is that it is unexpected to trigger a press on key release).
|
||||
* - Hotkeys are allowed to fire with extra keys (e.g. Ctrl+F+A still triggers 'Ctrl+F').
|
||||
* - If 'F' and 'Ctrl+F' trigger the same hotkey, adding 'Ctrl' _and_ releasing 'Ctrl' will trigger new 'Press' events.
|
||||
* The "Up" event is only sent when both Ctrl & F are released.
|
||||
* - This is somewhat unexpected/buggy, but it makes the implementation easier and is easily avoidable for players.
|
||||
* Note that mouse buttons/wheel inputs can fire hotkeys, in combinations with keys.
|
||||
* ...Yes, this is all surprisingly complex.
|
||||
*/
|
||||
|
||||
if (g_Console && g_Console->IsActive() && scancode < SDL_NUM_SCANCODES)
|
||||
consoleCapture = true;
|
||||
std::vector<ReleasedHotkey> releasedHotkeys;
|
||||
std::vector<PressedHotkey> newPressedHotkeys;
|
||||
|
||||
// Here's an interesting bit:
|
||||
// If you have an event bound to, say, 'F', and another to, say, 'Ctrl+F', pressing
|
||||
// 'F' while control is down would normally fire off both.
|
||||
std::set<SDL_Scancode_> triggers;
|
||||
if (!isReleasedKey)
|
||||
triggers.insert(scancode);
|
||||
else
|
||||
// If the key is released, we need to check all less precise hotkeys again, to see if we should retrigger some.
|
||||
for (SDL_Scancode_ code : activeScancodes)
|
||||
triggers.insert(code);
|
||||
|
||||
// To avoid this, set the modifier keys for /all/ events this key would trigger
|
||||
// (Ctrl, for example, is both group-save and bookmark-save)
|
||||
// but only send a HotkeyPress/HotkeyDown event for the event with bindings most precisely
|
||||
// matching the conditions (i.e. the event with the highest number of auxiliary
|
||||
// keys, providing they're all down)
|
||||
|
||||
// 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 SHotkeyMapping*> newPressedHotkeys;
|
||||
std::vector<const char*> releasedHotkeys;
|
||||
// Now check if we need to trigger new hotkeys / retrigger hotkeys.
|
||||
// We'll need the match-level and the keys in play to release currently pressed hotkeys.
|
||||
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)
|
||||
for (SDL_Scancode_ code : triggers)
|
||||
for (const SHotkeyMapping& hotkey : g_HotkeyMap[code])
|
||||
{
|
||||
if (g_HotkeyStatus[hotkey.name])
|
||||
// Ensure no duplications in the new list.
|
||||
if (std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
|
||||
[&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.name; }) != newPressedHotkeys.end())
|
||||
continue;
|
||||
|
||||
bool accept = true;
|
||||
for (const SKey& k : hotkey.requires)
|
||||
{
|
||||
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;
|
||||
}
|
||||
accept = isPressed(k);
|
||||
if (!accept)
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check for no unpermitted keys
|
||||
bool accept = true;
|
||||
for (const SKey& k : hotkey.requires)
|
||||
{
|
||||
accept = isPressed(k);
|
||||
if (!accept)
|
||||
break;
|
||||
}
|
||||
continue;
|
||||
|
||||
if (accept && !(consoleCapture && hotkey.name != "console.toggle"))
|
||||
{
|
||||
// Check if this is an equally precise or more precise match
|
||||
if (hotkey.requires.size() + 1 >= closestMapMatch)
|
||||
{
|
||||
@ -299,78 +319,95 @@ InReaction HotkeyInputHandler(const SDL_Event_* ev)
|
||||
newPressedHotkeys.clear();
|
||||
closestMapMatch = hotkey.requires.size() + 1;
|
||||
}
|
||||
newPressedHotkeys.push_back(&hotkey);
|
||||
newPressedHotkeys.emplace_back(&hotkey, isReleasedKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we need to release hotkeys.
|
||||
for (PressedHotkey& hotkey : pressedHotkeys)
|
||||
{
|
||||
bool addingAnew = std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
|
||||
[&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.mapping->name; }) != newPressedHotkeys.end();
|
||||
|
||||
// Update the triggered status to match our current state.
|
||||
if (addingAnew)
|
||||
std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
|
||||
[&hotkey](const PressedHotkey& v){ return v.mapping->name == hotkey.mapping->name; })->retriggered = hotkey.retriggered;
|
||||
// If the already-pressed hotkey has a lower specificity than the new hotkey(s), de-activate it.
|
||||
else if (hotkey.mapping->requires.size() + 1 < closestMapMatch)
|
||||
{
|
||||
releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), hotkey.retriggered);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check that the hotkey still matches all active keys.
|
||||
bool accept = isPressed(hotkey.mapping->primary);
|
||||
if (accept)
|
||||
for (const SKey& k : hotkey.mapping->requires)
|
||||
{
|
||||
accept = isPressed(k);
|
||||
if (!accept)
|
||||
break;
|
||||
}
|
||||
if (!accept && !addingAnew)
|
||||
releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), hotkey.retriggered);
|
||||
else if (accept)
|
||||
{
|
||||
// If this hotkey has higher specificity than the new hotkeys we wanted to trigger/retrigger,
|
||||
// then discard this new addition(s). This works because at any given time, all hotkeys
|
||||
// active must have the same specificity.
|
||||
if (hotkey.mapping->requires.size() + 1 > closestMapMatch)
|
||||
{
|
||||
closestMapMatch = hotkey.mapping->requires.size() + 1;
|
||||
newPressedHotkeys.clear();
|
||||
newPressedHotkeys.emplace_back(hotkey.mapping, hotkey.retriggered);
|
||||
}
|
||||
else if (!addingAnew)
|
||||
newPressedHotkeys.emplace_back(hotkey.mapping, hotkey.retriggered);
|
||||
}
|
||||
}
|
||||
|
||||
// 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 (std::find_if(newPressedHotkeys.begin(), newPressedHotkeys.end(),
|
||||
[&hotkey](const SHotkeyMapping* v){ return v->name == hotkey->name; }) != newPressedHotkeys.end())
|
||||
continue;
|
||||
else if (hotkey->requires.size() + 1 < closestMapMatch)
|
||||
releasedHotkeys.push_back(hotkey->name.c_str());
|
||||
else
|
||||
{
|
||||
// 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 PressedHotkey& hotkey : pressedHotkeys)
|
||||
releasedHotkeys.emplace_back(hotkey.mapping->name.c_str(), false);
|
||||
|
||||
for (const SHotkeyMapping* hotkey : pressedHotkeys)
|
||||
for (const PressedHotkey& 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*>(hotkey->name.c_str());
|
||||
hotkeyPressNotification.ev.type = hotkey.retriggered ? SDL_HOTKEYPRESS_SILENT : SDL_HOTKEYPRESS;
|
||||
hotkeyPressNotification.ev.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
|
||||
in_push_priority_event(&hotkeyPressNotification);
|
||||
}
|
||||
|
||||
// Send a HotkeyDown event on every key, mouseButton and mouseWheel event.
|
||||
// The exception is on the first retriggering: hotkeys may fire transiently
|
||||
// while a user lifts fingers off multi-key hotkeys, and listeners to "hotkeydown"
|
||||
// generally don't expect that to trigger then.
|
||||
// (It might be better to check for HotkeyIsPressed, however).
|
||||
// For keys the event is repeated depending on hardware and OS configured interval.
|
||||
// On linux, modifier keys (shift, alt, ctrl) are not repeated, see https://github.com/SFML/SFML/issues/122.
|
||||
if (ev->ev.key.repeat == 0 && hotkey.retriggered)
|
||||
continue;
|
||||
SDL_Event_ hotkeyDownNotification;
|
||||
hotkeyDownNotification.ev.type = SDL_HOTKEYDOWN;
|
||||
hotkeyDownNotification.ev.user.data1 = const_cast<char*>(hotkey->name.c_str());
|
||||
hotkeyDownNotification.ev.user.data1 = const_cast<char*>(hotkey.mapping->name.c_str());
|
||||
in_push_priority_event(&hotkeyDownNotification);
|
||||
}
|
||||
|
||||
for (const char* hotkeyName : releasedHotkeys)
|
||||
for (const ReleasedHotkey& hotkey : releasedHotkeys)
|
||||
{
|
||||
SDL_Event_ hotkeyNotification;
|
||||
hotkeyNotification.ev.type = SDL_HOTKEYUP;
|
||||
hotkeyNotification.ev.user.data1 = const_cast<char*>(hotkeyName);
|
||||
hotkeyNotification.ev.type = hotkey.wasRetriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP;
|
||||
hotkeyNotification.ev.user.data1 = const_cast<char*>(hotkey.name);
|
||||
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;
|
||||
}
|
||||
|
||||
|
@ -46,6 +46,8 @@ const uint SDL_USEREVENT_ = 0x8000;
|
||||
const uint SDL_HOTKEYPRESS = SDL_USEREVENT_;
|
||||
const uint SDL_HOTKEYDOWN = SDL_USEREVENT_ + 1;
|
||||
const uint SDL_HOTKEYUP = SDL_USEREVENT_ + 2;
|
||||
const uint SDL_HOTKEYPRESS_SILENT = SDL_USEREVENT_ + 3;
|
||||
const uint SDL_HOTKEYUP_SILENT = SDL_USEREVENT_ + 4;
|
||||
|
||||
constexpr SDL_Scancode_ UNUSED_HOTKEY_CODE = 0; // == SDL_SCANCODE_UNKNOWN
|
||||
|
||||
@ -71,9 +73,6 @@ typedef std::vector<SHotkeyMapping> KeyMapping;
|
||||
// multiple times.)
|
||||
extern std::unordered_map<SDL_Scancode_, KeyMapping> g_HotkeyMap;
|
||||
|
||||
// The current pressed status of hotkeys
|
||||
extern std::unordered_map<std::string, bool> g_HotkeyStatus;
|
||||
|
||||
class CConfigDB;
|
||||
extern void LoadHotkeys(CConfigDB& configDB);
|
||||
extern void UnloadHotkeys();
|
||||
|
@ -164,6 +164,7 @@ JS::Value GetConflicts(const ScriptRequest& rq, JS::HandleValue combination)
|
||||
|
||||
void JSI_Hotkey::RegisterScriptFunctions(const ScriptRequest& rq)
|
||||
{
|
||||
ScriptFunction::Register<&HotkeyIsPressed>(rq, "HotkeyIsPressed");
|
||||
ScriptFunction::Register<&GetHotkeyMap>(rq, "GetHotkeyMap");
|
||||
ScriptFunction::Register<&GetScancodeKeyNames>(rq, "GetScancodeKeyNames");
|
||||
ScriptFunction::Register<&ReloadHotkeys>(rq, "ReloadHotkeys");
|
||||
|
@ -1,4 +1,4 @@
|
||||
/* Copyright (C) 2020 Wildfire Games.
|
||||
/* Copyright (C) 2021 Wildfire Games.
|
||||
* This file is part of 0 A.D.
|
||||
*
|
||||
* 0 A.D. is free software: you can redistribute it and/or modify
|
||||
@ -88,11 +88,6 @@ JS::Value LoadMapSettings(const ScriptInterface& scriptInterface, const VfsPath&
|
||||
return settings;
|
||||
}
|
||||
|
||||
bool HotkeyIsPressed_(const std::string& hotkeyName)
|
||||
{
|
||||
return HotkeyIsPressed(hotkeyName);
|
||||
}
|
||||
|
||||
// This value is recalculated once a frame. We take special care to
|
||||
// filter it, so it is both accurate and free of jitter.
|
||||
int GetFps()
|
||||
@ -134,7 +129,6 @@ void RegisterScriptFunctions(const ScriptRequest& rq)
|
||||
ScriptFunction::Register<&GetSystemUsername>(rq, "GetSystemUsername");
|
||||
ScriptFunction::Register<&GetMatchID>(rq, "GetMatchID");
|
||||
ScriptFunction::Register<&LoadMapSettings>(rq, "LoadMapSettings");
|
||||
ScriptFunction::Register<&HotkeyIsPressed_>(rq, "HotkeyIsPressed");
|
||||
ScriptFunction::Register<&GetFps>(rq, "GetFPS");
|
||||
ScriptFunction::Register<&GetTextWidth>(rq, "GetTextWidth");
|
||||
ScriptFunction::Register<&CalculateMD5>(rq, "CalculateMD5");
|
||||
|
@ -32,6 +32,9 @@
|
||||
class TestHotkey : public CxxTest::TestSuite
|
||||
{
|
||||
CConfigDB* configDB;
|
||||
// Stores whether one of these was sent in the last fakeInput call.
|
||||
bool hotkeyPress = false;
|
||||
bool hotkeyUp = false;
|
||||
|
||||
private:
|
||||
|
||||
@ -43,8 +46,14 @@ private:
|
||||
ev.ev.key.keysym.scancode = SDL_GetScancodeFromName(key);
|
||||
GlobalsInputHandler(&ev);
|
||||
HotkeyInputHandler(&ev);
|
||||
hotkeyPress = false;
|
||||
hotkeyUp = false;
|
||||
while(in_poll_priority_event(&ev))
|
||||
{
|
||||
hotkeyUp |= ev.ev.type == SDL_HOTKEYUP;
|
||||
hotkeyPress |= ev.ev.type == SDL_HOTKEYPRESS;
|
||||
HotkeyStateChange(&ev);
|
||||
}
|
||||
}
|
||||
|
||||
public:
|
||||
@ -87,12 +96,14 @@ public:
|
||||
* Simple check.
|
||||
*/
|
||||
fakeInput("A", true);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, 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(hotkeyUp, true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
@ -105,19 +116,27 @@ public:
|
||||
*/
|
||||
fakeInput("A", true);
|
||||
fakeInput("B", true);
|
||||
// HotkeyUp is true - A is released.
|
||||
TS_ASSERT_EQUALS(hotkeyUp, true);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, 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);
|
||||
// A is silently retriggered - no Press
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true);
|
||||
|
||||
fakeInput("A", false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
|
||||
fakeInput("B", true);
|
||||
fakeInput("A", true);
|
||||
// Activating the more precise hotkey AB untriggers "A"
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
@ -133,6 +152,9 @@ public:
|
||||
fakeInput("A", true);
|
||||
fakeInput("B", true);
|
||||
fakeInput("B", false);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, true);
|
||||
// The "A" is retriggered silently.
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
@ -144,16 +166,66 @@ public:
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
}
|
||||
|
||||
void test_double_combination()
|
||||
{
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B");
|
||||
configDB->SetValueList(CFG_SYSTEM, "hotkey.D", { "D", "E" });
|
||||
configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg");
|
||||
configDB->Reload(CFG_SYSTEM);
|
||||
|
||||
UnloadHotkeys();
|
||||
LoadHotkeys(*configDB);
|
||||
|
||||
// Bit of a special case > Both D and E trigger the same hotkey.
|
||||
// In that case, any key change that still gets the hotkey triggered
|
||||
// will re-trigger a "press", and on the final release, the "Up" is sent.
|
||||
fakeInput("D", true);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
fakeInput("E", true);
|
||||
// Changing from one hotkey to another more specific combination of the same hotkey keeps it active
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
fakeInput("D", false);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
fakeInput("E", false);
|
||||
// Likewise going the other way.
|
||||
TS_ASSERT_EQUALS(hotkeyUp, true);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
|
||||
// Check that silent triggering works even in that case.
|
||||
fakeInput("D", true);
|
||||
fakeInput("E", true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
fakeInput("A", true);
|
||||
fakeInput("B", true);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
fakeInput("B", false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
fakeInput("E", false);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
// Note: as a consequence of the special case here - repressing E won't trigger a "press".
|
||||
fakeInput("E", true);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
fakeInput("E", false);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
fakeInput("D", false);
|
||||
TS_ASSERT_EQUALS(hotkeyUp, false);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
}
|
||||
|
||||
void test_quirk()
|
||||
@ -162,6 +234,7 @@ public:
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.AB", "A+B");
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.ABC", "A+B+C");
|
||||
configDB->SetValueList(CFG_SYSTEM, "hotkey.D", { "D", "D+E" });
|
||||
configDB->SetValueString(CFG_SYSTEM, "hotkey.E", "E+C");
|
||||
configDB->WriteFile(CFG_SYSTEM, "config/conf.cfg");
|
||||
configDB->Reload(CFG_SYSTEM);
|
||||
|
||||
@ -170,9 +243,9 @@ public:
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Further, hotkeys with the same # of keys are allowed to trigger at the same time.
|
||||
* This is required to make e.g. 'up' and 'left' scroll up-left when both are active.
|
||||
* It would be nice to extend this to 'non-conflicting hotkeys', but that's quickly far more complex.
|
||||
*/
|
||||
fakeInput("A", true);
|
||||
fakeInput("D", true);
|
||||
@ -181,6 +254,7 @@ public:
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false);
|
||||
|
||||
fakeInput("C", true);
|
||||
// A+D+C likewise.
|
||||
@ -188,27 +262,56 @@ public:
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false);
|
||||
|
||||
fakeInput("B", true);
|
||||
// Here D is inactivated because it's lower-specificity than A+B+C (with D being ignored).
|
||||
// A+B+C is a hotkey, more specific than A and D - both deactivated.
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false);
|
||||
|
||||
fakeInput("E", true);
|
||||
// D+E is still less specific than A+B+C - nothing changes.
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false);
|
||||
|
||||
fakeInput("B", false);
|
||||
// E & D activated - D+E and E+C have the same specificity.
|
||||
// The triggering is silent as it's from a key release.
|
||||
TS_ASSERT_EQUALS(hotkeyUp, true);
|
||||
TS_ASSERT_EQUALS(hotkeyPress, false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), true);
|
||||
|
||||
fakeInput("E", false);
|
||||
// A and D again.
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("A"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), 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("AB"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("ABC"), false);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("D"), true);
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false);
|
||||
|
||||
fakeInput("D", 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);
|
||||
|
||||
TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false);
|
||||
UnloadHotkeys();
|
||||
}
|
||||
};
|
||||
|
Loading…
Reference in New Issue
Block a user