[PowerAccent] Move low-level keyboard hook to c++ (#20190)

* Move llkeyboardhook to c++

* expect.txt

* Address PR comment - Resolve namespaces

* Address PR comments - CallNextHook and correct char selection

* Also unpress the letter key

Co-authored-by: Jaime Bernardo <jaime@janeasystems.com>
This commit is contained in:
Stefan Markovic
2022-09-09 13:27:56 +02:00
committed by GitHub
parent 7f8c5c9f0c
commit 7b0f97597d
25 changed files with 760 additions and 548 deletions

View File

@@ -0,0 +1,207 @@
#include "pch.h"
#include "KeyboardListener.h"
#include "KeyboardListener.g.cpp"
#include <common/logger/logger.h>
#include <common/utils/logger_helper.h>
#include <common/utils/winapi_error.h>
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
{
KeyboardListener::KeyboardListener() :
m_toolbarVisible(false), m_triggeredWithSpace(false)
{
s_instance = this;
LoggerHelpers::init_logger(L"PowerAccent", L"PowerAccentKeyboardService", "PowerAccent");
}
void KeyboardListener::InitHook()
{
#if defined(DISABLE_LOWLEVEL_HOOKS_WHEN_DEBUGGED)
const bool hook_disabled = IsDebuggerPresent();
#else
const bool hook_disabled = false;
#endif
if (!hook_disabled)
{
s_llKeyboardHook = SetWindowsHookEx(WH_KEYBOARD_LL, LowLevelKeyboardProc, GetModuleHandle(NULL), NULL);
if (!s_llKeyboardHook)
{
DWORD errorCode = GetLastError();
show_last_error_message(L"SetWindowsHookEx", errorCode, L"PowerToys - PowerAccent");
auto errorMessage = get_last_error_message(errorCode);
Logger::error(errorMessage.has_value() ? errorMessage.value() : L"");
}
}
}
void KeyboardListener::UnInitHook()
{
if (s_llKeyboardHook)
{
if (UnhookWindowsHookEx(s_llKeyboardHook))
{
s_llKeyboardHook = nullptr;
}
}
}
void KeyboardListener::SetShowToolbarEvent(ShowToolbar showToolbarEvent)
{
m_showToolbarCb = [trigger = std::move(showToolbarEvent)](LetterKey key) {
trigger(key);
};
}
void KeyboardListener::SetHideToolbarEvent(HideToolbar hideToolbarEvent)
{
m_hideToolbarCb = [trigger = std::move(hideToolbarEvent)](InputType inputType) {
trigger(inputType);
};
}
void KeyboardListener::SetNextCharEvent(NextChar nextCharEvent)
{
m_nextCharCb = [trigger = std::move(nextCharEvent)](TriggerKey triggerKey) {
trigger(triggerKey);
};
}
void KeyboardListener::UpdateActivationKey(int32_t activationKey)
{
m_settings.activationKey = static_cast<PowerAccentActivationKey>(activationKey);
}
void KeyboardListener::UpdateInputTime(int32_t inputTime)
{
m_settings.inputTime = std::chrono::milliseconds(inputTime);
}
bool KeyboardListener::OnKeyDown(KBDLLHOOKSTRUCT info) noexcept
{
if (std::find(std::begin(letters), end(letters), static_cast<LetterKey>(info.vkCode)) != end(letters))
{
m_stopwatch.reset();
letterPressed = static_cast<LetterKey>(info.vkCode);
}
UINT triggerPressed = 0;
if (letterPressed != LetterKey::None)
{
if (std::find(std::begin(triggers), end(triggers), static_cast<TriggerKey>(info.vkCode)) != end(triggers))
{
triggerPressed = info.vkCode;
if ((triggerPressed == VK_SPACE && m_settings.activationKey == PowerAccentActivationKey::LeftRightArrow) ||
((triggerPressed == VK_LEFT || triggerPressed == VK_RIGHT) && m_settings.activationKey == PowerAccentActivationKey::Space))
{
triggerPressed = 0;
Logger::info(L"Reset trigger key");
}
}
}
if (!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed)
{
Logger::info(L"Show toolbar. Letter: %d, Trigger: %d", letterPressed, triggerPressed);
// Keep track if it was triggered with space so that it can be typed on false starts.
m_triggeredWithSpace = triggerPressed == VK_SPACE;
m_toolbarVisible = true;
m_showToolbarCb(letterPressed);
}
if (m_toolbarVisible && triggerPressed)
{
if (triggerPressed == VK_LEFT)
{
Logger::info(L"Next toolbar position - left");
m_nextCharCb(TriggerKey::Left);
}
else if (triggerPressed == VK_RIGHT)
{
Logger::info(L"Next toolbar position - right");
m_nextCharCb(TriggerKey::Right);
}
else if (triggerPressed == VK_SPACE)
{
Logger::info(L"Next toolbar position - space");
m_nextCharCb(TriggerKey::Space);
}
return true;
}
return false;
}
bool KeyboardListener::OnKeyUp(KBDLLHOOKSTRUCT info) noexcept
{
if (std::find(std::begin(letters), end(letters), static_cast<LetterKey>(info.vkCode)) != end(letters))
{
letterPressed = LetterKey::None;
if (m_toolbarVisible)
{
if (m_stopwatch.elapsed() < m_settings.inputTime)
{
Logger::info(L"Activation too fast. Do nothing.");
// False start, we should output the space if it was the trigger.
if (m_triggeredWithSpace)
{
m_hideToolbarCb(InputType::Space);
}
else
{
m_hideToolbarCb(InputType::None);
}
m_toolbarVisible = false;
return true;
}
Logger::info(L"Hide toolbar event and input char");
m_hideToolbarCb(InputType::Char);
m_toolbarVisible = false;
}
}
return false;
}
LRESULT KeyboardListener::LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
{
if (nCode == HC_ACTION && s_instance != nullptr)
{
KBDLLHOOKSTRUCT* key = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
switch (wParam)
{
case WM_KEYDOWN:
{
if (s_instance->OnKeyDown(*key))
{
return true;
}
}
break;
case WM_KEYUP:
{
if (s_instance->OnKeyUp(*key))
{
return true;
}
}
break;
}
}
return CallNextHookEx(NULL, nCode, wParam, lParam);
}
}
}

View File

@@ -0,0 +1,75 @@
#pragma once
#include "KeyboardListener.g.h"
#include <spdlog/stopwatch.h>
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
{
enum PowerAccentActivationKey
{
LeftRightArrow,
Space,
Both,
};
struct PowerAccentSettings
{
PowerAccentActivationKey activationKey{ PowerAccentActivationKey::Both };
std::chrono::milliseconds inputTime{ 200 };
};
struct KeyboardListener : KeyboardListenerT<KeyboardListener>
{
using LetterKey = winrt::PowerToys::PowerAccentKeyboardService::LetterKey;
using TriggerKey = winrt::PowerToys::PowerAccentKeyboardService::TriggerKey;
using InputType = winrt::PowerToys::PowerAccentKeyboardService::InputType;
KeyboardListener();
void KeyboardListener::InitHook();
void KeyboardListener::UnInitHook();
void SetShowToolbarEvent(ShowToolbar showToolbarEvent);
void SetHideToolbarEvent(HideToolbar hideToolbarEvent);
void SetNextCharEvent(NextChar NextCharEvent);
void UpdateActivationKey(int32_t activationKey);
void UpdateInputTime(int32_t inputTime);
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
private:
bool OnKeyDown(KBDLLHOOKSTRUCT info) noexcept;
bool OnKeyUp(KBDLLHOOKSTRUCT info) noexcept;
static inline KeyboardListener* s_instance;
HHOOK s_llKeyboardHook = nullptr;
bool m_toolbarVisible;
PowerAccentSettings m_settings;
std::function<void(LetterKey)> m_showToolbarCb;
std::function<void(InputType)> m_hideToolbarCb;
std::function<void(TriggerKey)> m_nextCharCb;
bool m_triggeredWithSpace;
spdlog::stopwatch m_stopwatch;
static inline const std::vector<LetterKey> letters = { LetterKey::VK_A,
LetterKey::VK_C,
LetterKey::VK_E,
LetterKey::VK_I,
LetterKey::VK_N,
LetterKey::VK_O,
LetterKey::VK_S,
LetterKey::VK_U,
LetterKey::VK_Y };
LetterKey letterPressed{};
static inline const std::vector<TriggerKey> triggers = { TriggerKey::Right, TriggerKey::Left, TriggerKey::Space };
};
}
namespace winrt::PowerToys::PowerAccentKeyboardService::factory_implementation
{
struct KeyboardListener : KeyboardListenerT<KeyboardListener, implementation::KeyboardListener>
{
};
}

View File

@@ -0,0 +1,48 @@
namespace PowerToys
{
namespace PowerAccentKeyboardService
{
enum LetterKey
{
None = 0x00,
VK_A = 0x41,
VK_C = 0x43,
VK_E = 0x45,
VK_I = 0x49,
VK_N = 0x4E,
VK_O = 0x4F,
VK_S = 0x53,
VK_U = 0x55,
VK_Y = 0x59
};
enum TriggerKey
{
Right = 0x27, // VK_RIGHT
Left = 0x25, // VK_LEFT
Space = 0x20 // VK_SPACE
};
enum InputType
{
None,
Space,
Char
};
[version(1.0), uuid(37197089-5438-4479-af57-30ab3f3c8be4)] delegate void ShowToolbar(LetterKey key);
[version(1.0), uuid(8eb79d6b-1826-424f-9fbc-af21ae19725e)] delegate void HideToolbar(InputType inputType);
[version(1.0), uuid(db72d45c-a5a2-446f-bdc1-506e9121764a)] delegate void NextChar(TriggerKey inputSpace);
[default_interface] runtimeclass KeyboardListener {
KeyboardListener();
void InitHook();
void UnInitHook();
void SetShowToolbarEvent(event ShowToolbar showToolbarEvent);
void SetHideToolbarEvent(event HideToolbar hideToolbarEvent);
void SetNextCharEvent(event NextChar nextCharEvent);
void UpdateActivationKey(Int32 activationKey);
void UpdateInputTime(Int32 inputTime);
}
}
}

View File

@@ -0,0 +1,3 @@
EXPORTS
DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE

View File

@@ -0,0 +1,40 @@
#include <windows.h>
#include "resource.h"
#include "../../../common/version/version.h"
#define APSTUDIO_READONLY_SYMBOLS
#include "winres.h"
#undef APSTUDIO_READONLY_SYMBOLS
1 VERSIONINFO
FILEVERSION FILE_VERSION
PRODUCTVERSION PRODUCT_VERSION
FILEFLAGSMASK VS_FFI_FILEFLAGSMASK
#ifdef _DEBUG
FILEFLAGS VS_FF_DEBUG
#else
FILEFLAGS 0x0L
#endif
FILEOS VOS_NT_WINDOWS32
FILETYPE VFT_DLL
FILESUBTYPE VFT2_UNKNOWN
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0" // US English (0x0409), Unicode (0x04B0) charset
BEGIN
VALUE "CompanyName", COMPANY_NAME
VALUE "FileDescription", FILE_DESCRIPTION
VALUE "FileVersion", FILE_VERSION_STRING
VALUE "InternalName", INTERNAL_NAME
VALUE "LegalCopyright", COPYRIGHT_NOTE
VALUE "OriginalFilename", ORIGINAL_FILENAME
VALUE "ProductName", PRODUCT_NAME
VALUE "ProductVersion", PRODUCT_VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200 // US English (0x0409), Unicode (1200) charset
END
END

View File

@@ -0,0 +1,139 @@
<?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.220418.1\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" />
<PropertyGroup Label="Globals">
<CppWinRTOptimized>true</CppWinRTOptimized>
<CppWinRTRootNamespaceAutoMerge>true</CppWinRTRootNamespaceAutoMerge>
<CppWinRTGenerateWindowsMetadata>true</CppWinRTGenerateWindowsMetadata>
<MinimalCoreWin>true</MinimalCoreWin>
<ProjectGuid>{c97d9a5d-206c-454e-997e-009e227d7f02}</ProjectGuid>
<ProjectName>PowerAccentKeyboardService</ProjectName>
<RootNamespace>PowerToys.PowerAccentKeyboardService</RootNamespace>
<DefaultLanguage>en-US</DefaultLanguage>
<MinimumVisualStudioVersion>14.0</MinimumVisualStudioVersion>
<AppContainerApplication>false</AppContainerApplication>
<AppxPackage>false</AppxPackage>
<ApplicationType>Windows Store</ApplicationType>
<ApplicationTypeRevision>10.0</ApplicationTypeRevision>
<WindowsTargetPlatformVersion Condition=" '$(WindowsTargetPlatformVersion)' == '' ">10.0.19041.0</WindowsTargetPlatformVersion>
<WindowsTargetPlatformMinVersion>10.0.17134.0</WindowsTargetPlatformMinVersion>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
<PropertyGroup Label="Configuration">
<ConfigurationType>DynamicLibrary</ConfigurationType>
<PlatformToolset>v143</PlatformToolset>
<CharacterSet>Unicode</CharacterSet>
<GenerateManifest>false</GenerateManifest>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
<UseDebugLibraries>true</UseDebugLibraries>
<LinkIncremental>true</LinkIncremental>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
<UseDebugLibraries>false</UseDebugLibraries>
<WholeProgramOptimization>true</WholeProgramOptimization>
<LinkIncremental>false</LinkIncremental>
</PropertyGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
<ImportGroup Label="ExtensionSettings">
</ImportGroup>
<ImportGroup Label="Shared">
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
</ImportGroup>
<ImportGroup Label="PropertySheets">
<Import Project="PropertySheet.props" />
</ImportGroup>
<PropertyGroup Label="UserMacros" />
<PropertyGroup>
<TargetName>PowerToys.PowerAccentKeyboardService</TargetName>
<OutDir>$(SolutionDir)$(Platform)\$(Configuration)\modules\PowerAccent\</OutDir>
</PropertyGroup>
<ItemDefinitionGroup>
<ClCompile>
<PrecompiledHeader>Use</PrecompiledHeader>
<PrecompiledHeaderFile>pch.h</PrecompiledHeaderFile>
<PrecompiledHeaderOutputFile>$(IntDir)pch.pch</PrecompiledHeaderOutputFile>
<WarningLevel>Level4</WarningLevel>
<AdditionalOptions>%(AdditionalOptions) /bigobj</AdditionalOptions>
<!--Temporarily disable cppwinrt heap enforcement to work around xaml compiler generated std::shared_ptr use -->
<AdditionalOptions Condition="'$(CppWinRTHeapEnforcement)'==''">/DWINRT_NO_MAKE_DETECTION %(AdditionalOptions)</AdditionalOptions>
<PreprocessorDefinitions>_WINRT_DLL;WINRT_LEAN_AND_MEAN;%(PreprocessorDefinitions)</PreprocessorDefinitions>
<AdditionalIncludeDirectories>../../..;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
<AdditionalUsingDirectories>$(WindowsSDK_WindowsMetadata);$(AdditionalUsingDirectories)</AdditionalUsingDirectories>
</ClCompile>
<Link>
<SubSystem>Console</SubSystem>
<GenerateWindowsMetadata>false</GenerateWindowsMetadata>
<ModuleDefinitionFile>PowerAccentKeyboardService.def</ModuleDefinitionFile>
<AdditionalDependencies>Shell32.lib;%(AdditionalDependencies)</AdditionalDependencies>
</Link>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Debug'">
<ClCompile>
<PreprocessorDefinitions>_DEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
</ItemDefinitionGroup>
<ItemDefinitionGroup Condition="'$(Configuration)'=='Release'">
<ClCompile>
<PreprocessorDefinitions>NDEBUG;%(PreprocessorDefinitions)</PreprocessorDefinitions>
</ClCompile>
<Link>
<EnableCOMDATFolding>true</EnableCOMDATFolding>
<OptimizeReferences>true</OptimizeReferences>
</Link>
</ItemDefinitionGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="KeyboardListener.h">
<DependentUpon>KeyboardListener.idl</DependentUpon>
</ClInclude>
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp">
<PrecompiledHeader>Create</PrecompiledHeader>
</ClCompile>
<ClCompile Include="KeyboardListener.cpp">
<DependentUpon>KeyboardListener.idl</DependentUpon>
</ClCompile>
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
<Midl Include="KeyboardListener.idl" />
</ItemGroup>
<ItemGroup>
<None Include="packages.config" />
<None Include="PowerAccentKeyboardService.def" />
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\common\logger\logger.vcxproj">
<Project>{d9b8fc84-322a-4f9f-bbb9-20915c47ddfd}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\SettingsAPI\SettingsAPI.vcxproj">
<Project>{6955446d-23f7-4023-9bb3-8657f904af99}</Project>
</ProjectReference>
<ProjectReference Include="..\..\..\common\version\version.vcxproj">
<Project>{cc6e41ac-8174-4e8a-8d22-85dd7f4851df}</Project>
</ProjectReference>
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerAccentKeyboardService.rc" />
</ItemGroup>
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
<Import Project="..\..\..\..\deps\spdlog.props" />
<ImportGroup Label="ExtensionTargets">
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\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.220418.1\build\native\Microsoft.Windows.CppWinRT.props')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.props'))" />
<Error Condition="!Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets')" Text="$([System.String]::Format('$(ErrorText)', '..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.220418.1\build\native\Microsoft.Windows.CppWinRT.targets'))" />
</Target>
</Project>

View File

@@ -0,0 +1,35 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="15.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ItemGroup>
<Filter Include="Resources">
<UniqueIdentifier>accd3aa8-1ba0-4223-9bbe-0c431709210b</UniqueIdentifier>
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tga;tiff;tif;png;wav;mfcribbon-ms</Extensions>
</Filter>
<Filter Include="Generated Files">
<UniqueIdentifier>{926ab91d-31b4-48c3-b9a4-e681349f27f0}</UniqueIdentifier>
</Filter>
</ItemGroup>
<ItemGroup>
<ClCompile Include="pch.cpp" />
<ClCompile Include="$(GeneratedFilesDir)module.g.cpp" />
</ItemGroup>
<ItemGroup>
<ClInclude Include="pch.h" />
<ClInclude Include="resource.h" />
</ItemGroup>
<ItemGroup>
<Midl Include="KeyboardListener.idl" />
</ItemGroup>
<ItemGroup>
<None Include="PowerAccentKeyboardService.def" />
<None Include="packages.config" />
</ItemGroup>
<ItemGroup>
<None Include="PropertySheet.props" />
</ItemGroup>
<ItemGroup>
<ResourceCompile Include="PowerAccentKeyboardService.rc">
<Filter>Resources</Filter>
</ResourceCompile>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,16 @@
<?xml version="1.0" encoding="utf-8"?>
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<ImportGroup Label="PropertySheets" />
<PropertyGroup Label="UserMacros" />
<!--
To customize common C++/WinRT project properties:
* right-click the project node
* expand the Common Properties item
* select the C++/WinRT property page
For more advanced scenarios, and complete documentation, please see:
https://github.com/Microsoft/cppwinrt/tree/master/nuget
-->
<PropertyGroup />
<ItemDefinitionGroup />
</Project>

View File

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

View File

@@ -0,0 +1 @@
#include "pch.h"

View File

@@ -0,0 +1,4 @@
#pragma once
#include <unknwn.h>
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>

View File

@@ -0,0 +1,13 @@
//{{NO_DEPENDENCIES}}
// Microsoft Visual C++ generated include file.
// Used by PowerToys.MeasureToolCore.rc
//////////////////////////////
// Non-localizable
#define FILE_DESCRIPTION "PowerToys PowerAccentKeyboardService"
#define INTERNAL_NAME "PowerToys.PowerAccentKeyboardService"
#define ORIGINAL_FILENAME "PowerToys.PowerAccentKeyboardService.dll"
// Non-localizable
//////////////////////////////