Index: binaries/data/mods/public/gui/session/input.js =================================================================== --- binaries/data/mods/public/gui/session/input.js +++ binaries/data/mods/public/gui/session/input.js @@ -780,12 +780,12 @@ 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 @@ } break; - case "hotkeydown": + case "hotkeypress": if (ev.hotkey.indexOf("selection.group.") == 0) { let now = Date.now(); Index: source/gui/Scripting/GuiScriptConversions.cpp =================================================================== --- source/gui/Scripting/GuiScriptConversions.cpp +++ source/gui/Scripting/GuiScriptConversions.cpp @@ -48,8 +48,11 @@ 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 @@ 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(val.ev.user.data1)); break; Index: source/gui/tests/test_GuiManager.h =================================================================== --- source/gui/tests/test_GuiManager.h +++ source/gui/tests/test_GuiManager.h @@ -116,7 +116,7 @@ { // 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 @@ 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(); } }; Index: source/ps/GameSetup/GameSetup.cpp =================================================================== --- source/ps/GameSetup/GameSetup.cpp +++ source/ps/GameSetup/GameSetup.cpp @@ -539,14 +539,14 @@ 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); Index: source/ps/Hotkey.h =================================================================== --- source/ps/Hotkey.h +++ source/ps/Hotkey.h @@ -46,6 +46,8 @@ 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 @@ // multiple times.) extern std::unordered_map g_HotkeyMap; -// The current pressed status of hotkeys -extern std::unordered_map g_HotkeyStatus; - class CConfigDB; extern void LoadHotkeys(CConfigDB& configDB); extern void UnloadHotkeys(); Index: source/ps/Hotkey.cpp =================================================================== --- source/ps/Hotkey.cpp +++ source/ps/Hotkey.cpp @@ -31,12 +31,33 @@ static bool unified[UNIFIED_LAST - UNIFIED_SHIFT]; std::unordered_map g_HotkeyMap; -std::unordered_map 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 pressedHotkeys; + std::unordered_map 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 pressedHotkeys; + + // List of active keys relevant for hotkeys. + std::vector activeScancodes; } static_assert(std::is_integral::type>::value, "SDL_Scancode is not an integral enum."); @@ -133,9 +154,9 @@ 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(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(ev->ev.user.data1)] = false; return IN_PASS; } @@ -227,68 +248,67 @@ 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::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 releasedHotkeys; + std::vector 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 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 newPressedHotkeys; - std::vector 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 @@ 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(hotkey->name.c_str()); + hotkeyPressNotification.ev.type = hotkey.retriggered ? SDL_HOTKEYPRESS_SILENT : SDL_HOTKEYPRESS; + hotkeyPressNotification.ev.user.data1 = const_cast(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(hotkey->name.c_str()); + hotkeyDownNotification.ev.user.data1 = const_cast(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(hotkeyName); + hotkeyNotification.ev.type = hotkey.wasRetriggered ? SDL_HOTKEYUP_SILENT : SDL_HOTKEYUP; + hotkeyNotification.ev.user.data1 = const_cast(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(retrigger.code); - HotkeyInputHandler(&phantomKey); - } - return IN_PASS; } Index: source/ps/scripting/JSInterface_Hotkey.cpp =================================================================== --- source/ps/scripting/JSInterface_Hotkey.cpp +++ source/ps/scripting/JSInterface_Hotkey.cpp @@ -164,6 +164,7 @@ 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"); Index: source/ps/scripting/JSInterface_Main.cpp =================================================================== --- source/ps/scripting/JSInterface_Main.cpp +++ source/ps/scripting/JSInterface_Main.cpp @@ -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 @@ 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 @@ 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"); Index: source/ps/tests/test_Hotkeys.h =================================================================== --- source/ps/tests/test_Hotkeys.h +++ source/ps/tests/test_Hotkeys.h @@ -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 @@ 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 @@ * 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 @@ */ 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 @@ 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 @@ 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); + 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); - // Likewise going the other way. + 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 @@ 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 @@ /** * 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 @@ 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 @@ 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("A", false); fakeInput("B", false); - fakeInput("C", 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); + 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"), 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"), false); - + TS_ASSERT_EQUALS(HotkeyIsPressed("E"), false); UnloadHotkeys(); } };