[Settings/Run] LowLevel Keyboard hooking for Hotkeys (#3825)

* [Launcher/Settings] Low Level Keyboard Hooks

* [Run] LowLevel Keyboard Hook for Hotkeys

* Prevent shortcuts from auto repeating when keeping the keys pressed down
This commit is contained in:
Tomas Agustin Raies
2020-06-11 12:59:36 -07:00
committed by GitHub
parent fa7e4cc817
commit 670033c4da
28 changed files with 584 additions and 546 deletions

View File

@@ -0,0 +1,133 @@
#include "pch.h"
#include "HotkeyManager.h"
using namespace interop;
HotkeyManager::HotkeyManager()
{
keyboardEventCallback = gcnew KeyboardEventCallback(this, &HotkeyManager::KeyboardEventProc);
isActiveCallback = gcnew IsActiveCallback(this, &HotkeyManager::IsActiveProc);
filterKeyboardCallback = gcnew FilterKeyboardEvent(this, &HotkeyManager::FilterKeyboardProc);
keyboardHook = gcnew KeyboardHook(
keyboardEventCallback,
isActiveCallback,
filterKeyboardCallback
);
hotkeys = gcnew Dictionary<HOTKEY_HANDLE, HotkeyCallback ^>();
pressedKeys = gcnew Hotkey();
keyboardHook->Start();
}
HotkeyManager::~HotkeyManager()
{
delete keyboardHook;
}
// When all Shortcut keys are pressed, fire the HotkeyCallback event.
void HotkeyManager::KeyboardEventProc(KeyboardEvent^ ev)
{
auto pressedKeysHandle = GetHotkeyHandle(pressedKeys);
if (hotkeys->ContainsKey(pressedKeysHandle))
{
hotkeys[pressedKeysHandle]->Invoke();
}
}
// Hotkeys are intended to be global, therefore they are always active no matter the
// context in which the keypress occurs.
bool HotkeyManager::IsActiveProc()
{
return true;
}
// KeyboardEvent callback is only fired for relevant key events.
bool HotkeyManager::FilterKeyboardProc(KeyboardEvent^ ev)
{
auto oldHandle = GetHotkeyHandle(pressedKeys);
// Updating the pressed keys here so we know if the keypress event
// should be propagated or not.
UpdatePressedKeys(ev);
auto pressedKeysHandle = GetHotkeyHandle(pressedKeys);
// Check if the hotkey matches the pressed keys, and check if the pressed keys aren't duplicate
// (there shouldn't be auto repeating hotkeys)
if (hotkeys->ContainsKey(pressedKeysHandle) && oldHandle != pressedKeysHandle)
{
return true;
}
return false;
}
// NOTE: Replaces old hotkey if one already present.
HOTKEY_HANDLE HotkeyManager::RegisterHotkey(Hotkey ^ hotkey, HotkeyCallback ^ callback)
{
auto handle = GetHotkeyHandle(hotkey);
hotkeys[handle] = callback;
return handle;
}
void HotkeyManager::UnregisterHotkey(HOTKEY_HANDLE handle)
{
hotkeys->Remove(handle);
}
HOTKEY_HANDLE HotkeyManager::GetHotkeyHandle(Hotkey ^ hotkey)
{
HOTKEY_HANDLE handle = hotkey->Key;
handle |= hotkey->Win << 8;
handle |= hotkey->Ctrl << 9;
handle |= hotkey->Shift << 10;
handle |= hotkey->Alt << 11;
return handle;
}
void HotkeyManager::UpdatePressedKey(DWORD code, bool replaceWith, unsigned char replaceWithKey)
{
switch (code)
{
case VK_LWIN:
case VK_RWIN:
pressedKeys->Win = replaceWith;
break;
case VK_CONTROL:
case VK_LCONTROL:
case VK_RCONTROL:
pressedKeys->Ctrl = replaceWith;
break;
case VK_SHIFT:
case VK_LSHIFT:
case VK_RSHIFT:
pressedKeys->Shift = replaceWith;
break;
case VK_MENU:
case VK_LMENU:
case VK_RMENU:
pressedKeys->Alt = replaceWith;
break;
default:
pressedKeys->Key = replaceWithKey;
break;
}
}
void HotkeyManager::UpdatePressedKeys(KeyboardEvent ^ ev)
{
switch (ev->message)
{
case WM_KEYDOWN:
case WM_SYSKEYDOWN:
{
UpdatePressedKey(ev->key, true, ev->key);
}
break;
case WM_KEYUP:
case WM_SYSKEYUP:
{
UpdatePressedKey(ev->key, false, 0);
}
break;
}
}

View File

@@ -0,0 +1,57 @@
#pragma once
#include <Windows.h>
#include "KeyboardHook.h"
namespace interop
{
public
ref struct Hotkey
{
bool Win;
bool Ctrl;
bool Shift;
bool Alt;
unsigned char Key;
Hotkey()
{
Win = false;
Ctrl = false;
Shift = false;
Alt = false;
Key = 0;
}
};
public
delegate void HotkeyCallback();
typedef unsigned short HOTKEY_HANDLE;
public
ref class HotkeyManager
{
public:
HotkeyManager();
~HotkeyManager();
HOTKEY_HANDLE RegisterHotkey(Hotkey ^ hotkey, HotkeyCallback ^ callback);
void UnregisterHotkey(HOTKEY_HANDLE handle);
private:
KeyboardHook ^ keyboardHook;
Dictionary<HOTKEY_HANDLE, HotkeyCallback ^> ^ hotkeys;
Hotkey ^ pressedKeys;
KeyboardEventCallback ^ keyboardEventCallback;
IsActiveCallback ^ isActiveCallback;
FilterKeyboardEvent ^ filterKeyboardCallback;
void KeyboardEventProc(KeyboardEvent ^ ev);
bool IsActiveProc();
bool FilterKeyboardProc(KeyboardEvent ^ ev);
HOTKEY_HANDLE GetHotkeyHandle(Hotkey ^ hotkey);
void UpdatePressedKeys(KeyboardEvent ^ ev);
void UpdatePressedKey(DWORD code, bool replaceWith, unsigned char replaceWithKey);
};
}

View File

@@ -0,0 +1,96 @@
#include "pch.h"
#include "KeyboardHook.h"
#include <exception>
#include <msclr\marshal.h>
#include <msclr\marshal_cppstd.h>
using namespace interop;
using namespace System::Runtime::InteropServices;
using namespace System;
using namespace System::Diagnostics;
KeyboardHook::KeyboardHook(
KeyboardEventCallback ^ keyboardEventCallback,
IsActiveCallback ^ isActiveCallback,
FilterKeyboardEvent ^ filterKeyboardEvent)
{
kbEventDispatch = gcnew Thread(gcnew ThreadStart(this, &KeyboardHook::DispatchProc));
queue = gcnew Queue<KeyboardEvent ^>();
this->keyboardEventCallback = keyboardEventCallback;
this->isActiveCallback = isActiveCallback;
this->filterKeyboardEvent = filterKeyboardEvent;
}
KeyboardHook::~KeyboardHook()
{
quit = true;
kbEventDispatch->Join();
// Unregister low level hook procedure
UnhookWindowsHookEx(hookHandle);
}
void KeyboardHook::DispatchProc()
{
Monitor::Enter(queue);
quit = false;
while (!quit)
{
if (queue->Count == 0)
{
Monitor::Wait(queue);
continue;
}
auto nextEv = queue->Dequeue();
// Release lock while callback is being invoked
Monitor::Exit(queue);
keyboardEventCallback->Invoke(nextEv);
// Re-aquire lock
Monitor::Enter(queue);
}
Monitor::Exit(queue);
}
void KeyboardHook::Start()
{
hookProc = gcnew HookProcDelegate(this, &KeyboardHook::HookProc);
Process ^ curProcess = Process::GetCurrentProcess();
ProcessModule ^ curModule = curProcess->MainModule;
// register low level hook procedure
hookHandle = SetWindowsHookEx(
WH_KEYBOARD_LL,
(HOOKPROC)(void*)Marshal::GetFunctionPointerForDelegate(hookProc),
0,
0);
if (hookHandle == nullptr)
{
throw std::exception("SetWindowsHookEx failed.");
}
kbEventDispatch->Start();
}
LRESULT CALLBACK KeyboardHook::HookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HC_ACTION && isActiveCallback->Invoke())
{
KeyboardEvent ^ ev = gcnew KeyboardEvent();
ev->message = wParam;
ev->key = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam)->vkCode;
if (filterKeyboardEvent != nullptr && !filterKeyboardEvent->Invoke(ev))
{
return CallNextHookEx(hookHandle, nCode, wParam, lParam);
}
Monitor::Enter(queue);
queue->Enqueue(ev);
Monitor::Pulse(queue);
Monitor::Exit(queue);
return 1;
}
return CallNextHookEx(hookHandle, nCode, wParam, lParam);
}

View File

@@ -0,0 +1,49 @@
#pragma once
using namespace System::Threading;
using namespace System::Collections::Generic;
namespace interop
{
public
ref struct KeyboardEvent
{
WPARAM message;
int key;
};
public
delegate void KeyboardEventCallback(KeyboardEvent ^ ev);
public
delegate bool IsActiveCallback();
public
delegate bool FilterKeyboardEvent(KeyboardEvent ^ ev);
public
ref class KeyboardHook
{
public:
KeyboardHook(
KeyboardEventCallback ^ keyboardEventCallback,
IsActiveCallback ^ isActiveCallback,
FilterKeyboardEvent ^ filterKeyboardEvent);
~KeyboardHook();
void Start();
private:
delegate LRESULT HookProcDelegate(int nCode, WPARAM wParam, LPARAM lParam);
Thread ^ kbEventDispatch;
Queue<KeyboardEvent ^> ^ queue;
KeyboardEventCallback ^ keyboardEventCallback;
IsActiveCallback ^ isActiveCallback;
FilterKeyboardEvent ^ filterKeyboardEvent;
bool quit;
HHOOK hookHandle;
HookProcDelegate ^ hookProc;
void DispatchProc();
LRESULT CALLBACK HookProc(int nCode, WPARAM wParam, LPARAM lParam);
};
}

View File

@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="..\..\..\installer\Version.props" />
<PropertyGroup>
@@ -124,13 +124,17 @@
<WriteLinesToFile File="Generated Files\AssemblyInfo.cpp" Lines="@(HeaderLines)" Overwrite="true" Encoding="Unicode" WriteOnlyWhenDifferent="true" />
</Target>
<ItemGroup>
<ClInclude Include="HotkeyManager.h" />
<ClInclude Include="interop.h" />
<ClInclude Include="KeyboardHook.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="Resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="Generated Files\AssemblyInfo.cpp" />
<ClCompile Include="HotkeyManager.cpp" />
<ClCompile Include="interop.cpp" />
<ClCompile Include="KeyboardHook.cpp" />
<ClCompile Include="pch.cpp">
<PrecompiledHeader Condition="'$(CIBuild)'!='true'">Create</PrecompiledHeader>
</ClCompile>

View File

@@ -24,6 +24,12 @@
<ClInclude Include="pch.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="KeyboardHook.h">
<Filter>Header Files</Filter>
</ClInclude>
<ClInclude Include="HotkeyManager.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<ClCompile Include="interop.cpp">
@@ -35,6 +41,12 @@
<ClCompile Include="Generated Files\AssemblyInfo.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="KeyboardHook.cpp">
<Filter>Source Files</Filter>
</ClCompile>
<ClCompile Include="HotkeyManager.cpp">
<Filter>Source Files</Filter>
</ClCompile>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="app.rc">

View File

@@ -7,6 +7,8 @@
#ifndef PCH_H
#define PCH_H
#define WIN32_LEAN_AND_MEAN
// add headers that you want to pre-compile here
#include <Windows.h>
#endif //PCH_H