#include "pch.h" #include "centralized_kb_hook.h" #include #include #include #include namespace CentralizedKeyboardHook { struct HotkeyDescriptor { Hotkey hotkey; std::wstring moduleName; std::function action; bool operator<(const HotkeyDescriptor& other) const { return hotkey < other.hotkey; }; }; std::multiset hotkeyDescriptors; std::mutex mutex; HHOOK hHook{}; // To store information about handling pressed keys. struct PressedKeyDescriptor { DWORD virtualKey; // Virtual Key code of the key we're keeping track of. std::wstring moduleName; std::function action; UINT_PTR idTimer; // Timer ID for calling SET_TIMER with. UINT millisecondsToPress; // How much time the key must be pressed. bool operator<(const PressedKeyDescriptor& other) const { // We'll use the virtual key as the real key, since looking for a hit with the key is done in the more time sensitive path (low level keyboard hook). return virtualKey < other.virtualKey; }; }; std::multiset pressedKeyDescriptors; std::mutex pressedKeyMutex; // keep track of last pressed key, to detect repeated keys and if there are more keys pressed. const DWORD VK_DISABLED = CommonSharedConstants::VK_DISABLED; DWORD vkCodePressed = VK_DISABLED; // Save the runner window handle for registering timers. HWND runnerWindow; struct DestroyOnExit { ~DestroyOnExit() { Stop(); } } destroyOnExitObj; // Handle the pressed key proc void PressedKeyTimerProc( HWND hwnd, UINT /*message*/, UINT_PTR idTimer, DWORD /*dwTime*/) { std::multiset copy; { // Make a copy, to look for the action to call. std::unique_lock lock{ pressedKeyMutex }; copy = pressedKeyDescriptors; } for (const auto& it : copy) { if (it.idTimer == idTimer) { it.action(); } } KillTimer(hwnd, idTimer); } LRESULT CALLBACK KeyboardHookProc(_In_ int nCode, _In_ WPARAM wParam, _In_ LPARAM lParam) { if (nCode < 0) { return CallNextHookEx(hHook, nCode, wParam, lParam); } const auto& keyPressInfo = *reinterpret_cast(lParam); if (keyPressInfo.dwExtraInfo == PowertoyModuleIface::CENTRALIZED_KEYBOARD_HOOK_DONT_TRIGGER_FLAG) { // The new keystroke was generated from one of our actions. We should pass it along. return CallNextHookEx(hHook, nCode, wParam, lParam); } // Check if the keys are pressed. if (!pressedKeyDescriptors.empty()) { bool wasKeyPressed = vkCodePressed != VK_DISABLED; // Hold the lock for the shortest possible duration if ((wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)) { if (!wasKeyPressed) { // If no key was pressed before, let's start a timer to take into account this new key. std::unique_lock lock{ pressedKeyMutex }; PressedKeyDescriptor dummy{ .virtualKey = keyPressInfo.vkCode }; auto [it, last] = pressedKeyDescriptors.equal_range(dummy); for (; it != last; ++it) { SetTimer(runnerWindow, it->idTimer, it->millisecondsToPress, PressedKeyTimerProc); } } else if (vkCodePressed != keyPressInfo.vkCode) { // If a different key was pressed, let's clear the timers we have started for the previous key. std::unique_lock lock{ pressedKeyMutex }; PressedKeyDescriptor dummy{ .virtualKey = vkCodePressed }; auto [it, last] = pressedKeyDescriptors.equal_range(dummy); for (; it != last; ++it) { KillTimer(runnerWindow, it->idTimer); } } vkCodePressed = keyPressInfo.vkCode; } if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP) { std::unique_lock lock{ pressedKeyMutex }; PressedKeyDescriptor dummy{ .virtualKey = keyPressInfo.vkCode }; auto [it, last] = pressedKeyDescriptors.equal_range(dummy); for (; it != last; ++it) { KillTimer(runnerWindow, it->idTimer); } vkCodePressed = 0x100; } } if ((wParam != WM_KEYDOWN) && (wParam != WM_SYSKEYDOWN)) { return CallNextHookEx(hHook, nCode, wParam, lParam); } Hotkey hotkey{ .win = (GetAsyncKeyState(VK_LWIN) & 0x8000) || (GetAsyncKeyState(VK_RWIN) & 0x8000), .ctrl = static_cast(GetAsyncKeyState(VK_CONTROL) & 0x8000), .shift = static_cast(GetAsyncKeyState(VK_SHIFT) & 0x8000), .alt = static_cast(GetAsyncKeyState(VK_MENU) & 0x8000), .key = static_cast(keyPressInfo.vkCode) }; std::function action; { // Hold the lock for the shortest possible duration std::unique_lock lock{ mutex }; HotkeyDescriptor dummy{ .hotkey = hotkey }; auto it = hotkeyDescriptors.find(dummy); if (it != hotkeyDescriptors.end()) { action = it->action; } } if (action) { if (action()) { // After invoking the hotkey send a dummy key to prevent Start Menu from activating INPUT dummyEvent[1] = {}; dummyEvent[0].type = INPUT_KEYBOARD; dummyEvent[0].ki.wVk = 0xFF; dummyEvent[0].ki.dwFlags = KEYEVENTF_KEYUP; SendInput(1, dummyEvent, sizeof(INPUT)); // Swallow the key press return 1; } } return CallNextHookEx(hHook, nCode, wParam, lParam); } void SetHotkeyAction(const std::wstring& moduleName, const Hotkey& hotkey, std::function&& action) noexcept { Logger::trace(L"Register hotkey action for {}", moduleName); std::unique_lock lock{ mutex }; hotkeyDescriptors.insert({ .hotkey = hotkey, .moduleName = moduleName, .action = std::move(action) }); } void AddPressedKeyAction(const std::wstring& moduleName, const DWORD vk, const UINT milliseconds, std::function&& action) noexcept { // Calculate a unique TimerID. auto hash = std::hash{}(moduleName); // Hash the module as the upper part of the timer ID. const UINT upperId = hash & 0xFFFF; const UINT lowerId = vk & 0xFFFF; // The key to press can be the lower ID. const UINT timerId = upperId << 16 | lowerId; std::unique_lock lock{ pressedKeyMutex }; pressedKeyDescriptors.insert({ .virtualKey = vk, .moduleName = moduleName, .action = std::move(action), .idTimer = timerId, .millisecondsToPress = milliseconds }); } void ClearModuleHotkeys(const std::wstring& moduleName) noexcept { Logger::trace(L"UnRegister hotkey action for {}", moduleName); { std::unique_lock lock{ mutex }; auto it = hotkeyDescriptors.begin(); while (it != hotkeyDescriptors.end()) { if (it->moduleName == moduleName) { it = hotkeyDescriptors.erase(it); } else { ++it; } } } { std::unique_lock lock{ pressedKeyMutex }; auto it = pressedKeyDescriptors.begin(); while (it != pressedKeyDescriptors.end()) { if (it->moduleName == moduleName) { it = pressedKeyDescriptors.erase(it); } else { ++it; } } } } void Start() noexcept { #if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED) const bool hook_disabled = IsDebuggerPresent(); #else const bool hook_disabled = false; #endif if (!hook_disabled) { if (!hHook) { hHook = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardHookProc, NULL, NULL); if (!hHook) { DWORD errorCode = GetLastError(); show_last_error_message(L"SetWindowsHookEx", errorCode, L"centralized_kb_hook"); } } } } void Stop() noexcept { if (hHook && UnhookWindowsHookEx(hHook)) { hHook = NULL; } } void RegisterWindow(HWND hwnd) noexcept { runnerWindow = hwnd; } }