KBM - Set up tests for keyboard hook remapping logic (#4004)

* Add test proj, refactor proj with filters, and move single remap function to a separate header

* Moved all methods to header files

* remove more unused commented code

* Reverted sln file

* Fixed sln file

* Added interface wrapping SendInput calls

* fixed formatting

* Created test mock class

* Added keyboard input logic

* Fixed compilation errors and added nuget reference to CppWinRT

* Added tests for single key remapping

* Refactored code for adding shortcut remap tests

* Separated test classes

* Fixed tests in release mode

* Added more tests

* Resolved comments
This commit is contained in:
Arjun Balgovind
2020-06-11 13:07:46 -07:00
committed by GitHub
parent 670033c4da
commit 071ea1dc97
30 changed files with 1866 additions and 66 deletions

View File

@@ -0,0 +1,14 @@
#include "pch.h"
#include "Input.h"
// Function to simulate input
UINT Input::SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize)
{
return SendInput(cInputs, pInputs, cbSize);
}
// Function to get the state of a particular key
bool Input::GetVirtualKeyState(int key)
{
return (GetAsyncKeyState(key) & 0x8000);
}

View File

@@ -0,0 +1,14 @@
#pragma once
#include <keyboardmanager/common/InputInterface.h>
// Class used to wrap keyboard input library methods
class Input :
public InputInterface
{
public:
// Function to simulate input
UINT SendVirtualInput(UINT cInputs, LPINPUT pInputs, int cbSize);
// Function to get the state of a particular key
bool GetVirtualKeyState(int key);
};

View File

@@ -4,7 +4,7 @@
namespace KeyboardEventHandlers
{
// Function to a handle a single key remap
intptr_t HandleSingleKeyRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
__declspec(dllexport) intptr_t HandleSingleKeyRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
{
// Check if the key event was generated by KeyboardManager to avoid remapping events generated by us.
if (!(data->lParam->dwExtraInfo & CommonSharedConstants::KEYBOARDMANAGER_INJECTED_FLAG))
@@ -42,7 +42,7 @@ namespace KeyboardEventHandlers
}
lock.unlock();
UINT res = SendInput(key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput(key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
return 1;
}
@@ -52,7 +52,7 @@ namespace KeyboardEventHandlers
}
// Function to a change a key's behavior from toggle to modifier
intptr_t HandleSingleKeyToggleToModEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
__declspec(dllexport) intptr_t HandleSingleKeyToggleToModEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
{
// Check if the key event was generated by KeyboardManager to avoid remapping events generated by us.
if (!(data->lParam->dwExtraInfo & CommonSharedConstants::KEYBOARDMANAGER_INJECTED_FLAG))
@@ -82,7 +82,7 @@ namespace KeyboardEventHandlers
KeyboardManagerHelper::SetKeyEvent(keyEventList, 1, INPUT_KEYBOARD, (WORD)data->lParam->vkCode, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SINGLEKEY_FLAG);
lock.unlock();
UINT res = SendInput(key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput(key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
// Reset the long press flag when the key has been lifted.
@@ -101,7 +101,7 @@ namespace KeyboardEventHandlers
}
// Function to a handle a shortcut remap
intptr_t HandleShortcutRemapEvent(LowlevelKeyboardEvent* data, std::map<Shortcut, RemapShortcut>& reMap, std::mutex& map_mutex) noexcept
__declspec(dllexport) intptr_t HandleShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, std::map<Shortcut, RemapShortcut>& reMap, std::mutex& map_mutex) noexcept
{
// The mutex should be unlocked before SendInput is called to avoid re-entry into the same mutex. More details can be found at https://github.com/microsoft/PowerToys/pull/1789#issuecomment-607555837
std::unique_lock<std::mutex> lock(map_mutex);
@@ -130,12 +130,12 @@ namespace KeyboardEventHandlers
const size_t dest_size = it.second.targetShortcut.Size();
// If the shortcut has been pressed down
if (!it.second.isShortcutInvoked && it.first.CheckModifiersKeyboardState())
if (!it.second.isShortcutInvoked && it.first.CheckModifiersKeyboardState(ii))
{
if (data->lParam->vkCode == it.first.GetActionKey() && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN))
{
// Check if any other keys have been pressed apart from the shortcut. If true, then check for the next shortcut
if (!it.first.IsKeyboardStateClearExceptShortcut())
if (!it.first.IsKeyboardStateClearExceptShortcut(ii))
{
continue;
}
@@ -144,11 +144,11 @@ namespace KeyboardEventHandlers
LPINPUT keyEventList;
// Remember which win key was pressed initially
if (GetAsyncKeyState(VK_RWIN) & 0x8000)
if (ii.GetVirtualKeyState(VK_RWIN))
{
it.second.winKeyInvoked = ModifierKey::Right;
}
else if (GetAsyncKeyState(VK_LWIN) & 0x8000)
else if (ii.GetVirtualKeyState(VK_LWIN))
{
it.second.winKeyInvoked = ModifierKey::Left;
}
@@ -248,7 +248,7 @@ namespace KeyboardEventHandlers
it.second.isShortcutInvoked = true;
lock.unlock();
UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
return 1;
}
@@ -351,14 +351,14 @@ namespace KeyboardEventHandlers
// key count can be 0 if both shortcuts have same modifiers and the action key is not held down. delete will throw an error if keyEventList is empty
if (key_count > 0)
{
UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
}
return 1;
}
// The system will see the modifiers of the new shortcut as being held down because of the shortcut remap
if (it.second.targetShortcut.CheckModifiersKeyboardState())
if (it.second.targetShortcut.CheckModifiersKeyboardState(ii))
{
// Case 2: If the original shortcut is still held down the keyboard will get a key down message of the action key in the original shortcut and the new shortcut's modifiers will be held down (keys held down send repeated keydown messages)
if (data->lParam->vkCode == it.first.GetActionKey() && (data->wParam == WM_KEYDOWN || data->wParam == WM_SYSKEYDOWN))
@@ -370,7 +370,7 @@ namespace KeyboardEventHandlers
it.second.isShortcutInvoked = true;
lock.unlock();
UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
return 1;
}
@@ -385,7 +385,7 @@ namespace KeyboardEventHandlers
it.second.isShortcutInvoked = true;
lock.unlock();
UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
return 1;
}
@@ -547,7 +547,7 @@ namespace KeyboardEventHandlers
it.second.isShortcutInvoked = false;
it.second.winKeyInvoked = ModifierKey::Disabled;
lock.unlock();
UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
return 1;
}
@@ -565,12 +565,12 @@ namespace KeyboardEventHandlers
}
// Function to a handle an os-level shortcut remap
intptr_t HandleOSLevelShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
__declspec(dllexport) intptr_t HandleOSLevelShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
{
// Check if the key event was generated by KeyboardManager to avoid remapping events generated by us.
if (data->lParam->dwExtraInfo != KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG)
{
bool result = HandleShortcutRemapEvent(data, keyboardManagerState.osLevelShortcutReMap, keyboardManagerState.osLevelShortcutReMap_mutex);
bool result = HandleShortcutRemapEvent(ii, data, keyboardManagerState.osLevelShortcutReMap, keyboardManagerState.osLevelShortcutReMap_mutex);
return result;
}
@@ -578,7 +578,7 @@ namespace KeyboardEventHandlers
}
// Function to a handle an app-specific shortcut remap
intptr_t HandleAppSpecificShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
__declspec(dllexport) intptr_t HandleAppSpecificShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept
{
// Check if the key event was generated by KeyboardManager to avoid remapping events generated by us.
if (data->lParam->dwExtraInfo != KeyboardManagerConstants::KEYBOARDMANAGER_SHORTCUT_FLAG)
@@ -594,7 +594,7 @@ namespace KeyboardEventHandlers
if (it != keyboardManagerState.appSpecificShortcutReMap.end())
{
lock.unlock();
bool result = HandleShortcutRemapEvent(data, keyboardManagerState.appSpecificShortcutReMap[process_name], keyboardManagerState.appSpecificShortcutReMap_mutex);
bool result = HandleShortcutRemapEvent(ii, data, keyboardManagerState.appSpecificShortcutReMap[process_name], keyboardManagerState.appSpecificShortcutReMap_mutex);
return result;
}
}
@@ -603,7 +603,7 @@ namespace KeyboardEventHandlers
}
// Function to ensure Num Lock state does not change when it is suppressed by the low level hook
void SetNumLockToPreviousState()
void SetNumLockToPreviousState(InputInterface& ii)
{
// Num Lock's key state is applied before it is intercepted by low level keyboard hooks, so we have to manually set back the state when we suppress the key. This is done by sending an additional key up, key down set of messages.
// We need 2 key events because after Num Lock is suppressed, key up to release num lock key and key down to revert the num lock state
@@ -614,7 +614,7 @@ namespace KeyboardEventHandlers
// Use the shortcut flag to ensure these are not intercepted by any remapped keys or shortcuts
KeyboardManagerHelper::SetKeyEvent(keyEventList, 0, INPUT_KEYBOARD, VK_NUMLOCK, KEYEVENTF_KEYUP, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
KeyboardManagerHelper::SetKeyEvent(keyEventList, 1, INPUT_KEYBOARD, VK_NUMLOCK, 0, KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG);
UINT res = SendInput((UINT)key_count, keyEventList, sizeof(INPUT));
UINT res = ii.SendVirtualInput((UINT)key_count, keyEventList, sizeof(INPUT));
delete[] keyEventList;
}
}

View File

@@ -1,24 +1,25 @@
#pragma once
#include <keyboardmanager/common/KeyboardManagerState.h>
#include <keyboardmanager/common/KeyboardManagerConstants.h>
#include <keyboardmanager/common/InputInterface.h>
namespace KeyboardEventHandlers
{
// Function to a handle a single key remap
intptr_t HandleSingleKeyRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
__declspec(dllexport) intptr_t HandleSingleKeyRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
// Function to a change a key's behavior from toggle to modifier
intptr_t HandleSingleKeyToggleToModEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
__declspec(dllexport) intptr_t HandleSingleKeyToggleToModEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
// Function to a handle a shortcut remap
intptr_t HandleShortcutRemapEvent(LowlevelKeyboardEvent* data, std::map<Shortcut, RemapShortcut>& reMap, std::mutex& map_mutex) noexcept;
__declspec(dllexport) intptr_t HandleShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, std::map<Shortcut, RemapShortcut>& reMap, std::mutex& map_mutex) noexcept;
// Function to a handle an os-level shortcut remap
intptr_t HandleOSLevelShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
__declspec(dllexport) intptr_t HandleOSLevelShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
// Function to a handle an app-specific shortcut remap
intptr_t HandleAppSpecificShortcutRemapEvent(LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
__declspec(dllexport) intptr_t HandleAppSpecificShortcutRemapEvent(InputInterface& ii, LowlevelKeyboardEvent* data, KeyboardManagerState& keyboardManagerState) noexcept;
// Function to ensure Num Lock state does not change when it is suppressed by the low level hook
void SetNumLockToPreviousState();
void SetNumLockToPreviousState(InputInterface& ii);
};

View File

@@ -1,5 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.props')" />
<ItemGroup Label="ProjectConfigurations">
<ProjectConfiguration Include="Debug|x64">
<Configuration>Debug</Configuration>
@@ -114,12 +115,14 @@
</ClCompile>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="Input.h" />
<ClInclude Include="KeyboardEventHandlers.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="dllmain.cpp" />
<ClCompile Include="Input.cpp" />
<ClCompile Include="KeyboardEventHandlers.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(CIBuild)'!='true'">Create</PrecompiledHeader>
@@ -142,7 +145,18 @@
<ItemGroup>
<Image Include="Keyboard.ico" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.targets')" />
</ImportGroup>
<Target Name="EnsureNuGetPackageBuildImports" BeforeTargets="PrepareForBuild">
<PropertyGroup>
<ErrorText>This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.</ErrorText>
</PropertyGroup>
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.200602.3\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -10,6 +10,9 @@
<ClCompile Include="pch.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="Input.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ClInclude Include="KeyboardEventHandlers.h">
@@ -21,6 +24,9 @@
<ClInclude Include="resource.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="Input.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<Filter Include="Source Files">
@@ -46,4 +52,7 @@
<Filter>Resource Files</Filter>
</ResourceCompile>
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
</ItemGroup>
</Project>

View File

@@ -14,6 +14,7 @@
#include <common/settings_helpers.h>
#include <keyboardmanager/common/trace.h>
#include "KeyboardEventHandlers.h"
#include "Input.h"
extern "C" IMAGE_DOS_HEADER __ImageBase;
@@ -56,6 +57,9 @@ private:
// Variable which stores all the state information to be shared between the UI and back-end
KeyboardManagerState keyboardManagerState;
// Object of class which implements InputInterface. Required for calling library functions while enabling testing
Input inputHandler;
public:
// Constructor
KeyboardManager()
@@ -286,7 +290,7 @@ public:
// Reset Num Lock whenever a NumLock key down event is suppressed since Num Lock key state change occurs before it is intercepted by low level hooks
if (event.lParam->vkCode == VK_NUMLOCK && (event.wParam == WM_KEYDOWN || event.wParam == WM_SYSKEYDOWN) && event.lParam->dwExtraInfo != KeyboardManagerConstants::KEYBOARDMANAGER_SUPPRESS_FLAG)
{
KeyboardEventHandlers::SetNumLockToPreviousState();
KeyboardEventHandlers::SetNumLockToPreviousState(keyboardmanager_object_ptr->inputHandler);
}
return 1;
}
@@ -347,7 +351,7 @@ public:
}
// Remap a key
intptr_t SingleKeyRemapResult = KeyboardEventHandlers::HandleSingleKeyRemapEvent(data, keyboardManagerState);
intptr_t SingleKeyRemapResult = KeyboardEventHandlers::HandleSingleKeyRemapEvent(inputHandler, data, keyboardManagerState);
// Single key remaps have priority. If a key is remapped, only the remapped version should be visible to the shortcuts and hence the event should be suppressed here.
if (SingleKeyRemapResult == 1)
@@ -367,10 +371,10 @@ public:
}
//// Remap a key to behave like a modifier instead of a toggle
//intptr_t SingleKeyToggleToModResult = KeyboardEventHandlers::HandleSingleKeyToggleToModEvent(data, keyboardManagerState);
//intptr_t SingleKeyToggleToModResult = KeyboardEventHandlers::HandleSingleKeyToggleToModEvent(inputHandler, data, keyboardManagerState);
//// Handle an app-specific shortcut remapping
//intptr_t AppSpecificShortcutRemapResult = KeyboardEventHandlers::HandleAppSpecificShortcutRemapEvent(data, keyboardManagerState);
//intptr_t AppSpecificShortcutRemapResult = KeyboardEventHandlers::HandleAppSpecificShortcutRemapEvent(inputHandler, data, keyboardManagerState);
//// If an app-specific shortcut is remapped then the os-level shortcut remapping should be suppressed.
//if (AppSpecificShortcutRemapResult == 1)
@@ -379,7 +383,7 @@ public:
//}
// Handle an os-level shortcut remapping
intptr_t OSLevelShortcutRemapResult = KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent(data, keyboardManagerState);
intptr_t OSLevelShortcutRemapResult = KeyboardEventHandlers::HandleOSLevelShortcutRemapEvent(inputHandler, data, keyboardManagerState);
// If any of the supported types of remappings took place, then suppress the key event
if ((SingleKeyRemapResult + OSLevelShortcutRemapResult) > 0)

View File

@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="Microsoft.Windows.CppWinRT" version="2.0.200602.3" targetFramework="native" />
</packages>