[AOT] Low level keyboard hook (#18643)

* low level keyboard hook for always on top

* fix for message loop

* fix spellcheck

* refactoring and cleanup

* spellcheck
This commit is contained in:
Davide Giacometti 2022-06-23 16:29:53 +02:00 committed by GitHub
parent 2159e2722e
commit 301f26aca1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 221 additions and 6 deletions

View File

@ -35,6 +35,7 @@ ALarger
alekhyareddy
alignas
ALLAPPS
ALLINPUT
Alloc
ALLOWUNDO
ALPHATYPE
@ -1096,6 +1097,7 @@ LINQTo
Linux
listview
lld
LLKH
llkhf
Llvm
lmcons

View File

@ -34,6 +34,9 @@ namespace CommonSharedConstants
// Path to the event used by Awake
const wchar_t AWAKE_EXIT_EVENT[] = L"Local\\PowerToysAwakeExitEvent-c0d5e305-35fc-4fb5-83ec-f6070cfaf7fe";
// Path to the event used by AlwaysOnTop
const wchar_t ALWAYS_ON_TOP_PIN_EVENT[] = L"Local\\AlwaysOnTopPinEvent-892e0aa2-cfa8-4cc4-b196-ddeb32314ce8";
// Max DWORD for key code to disable keys.
const DWORD VK_DISABLED = 0x100;

View File

@ -9,6 +9,7 @@
#include <common/utils/process_path.h>
#include <WinHookEventIDs.h>
#include <interop/shared_constants.h>
namespace NonLocalizable
{
@ -23,9 +24,10 @@ bool isExcluded(HWND window)
return find_app_name_in_path(processPath, AlwaysOnTopSettings::settings().excludedApps);
}
AlwaysOnTop::AlwaysOnTop() :
AlwaysOnTop::AlwaysOnTop(bool useLLKH) :
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps}),
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase))
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase)),
m_useCentralizedLLKH(useLLKH)
{
s_instance = this;
DPIAware::EnableDPIAwarenessForThisProcess();
@ -38,6 +40,8 @@ AlwaysOnTop::AlwaysOnTop() :
AlwaysOnTopSettings::instance().LoadSettings();
RegisterHotkey();
RegisterLLKH();
SubscribeToEvents();
StartTrackingTopmostWindows();
}
@ -50,6 +54,13 @@ AlwaysOnTop::AlwaysOnTop() :
AlwaysOnTop::~AlwaysOnTop()
{
if (m_hPinEvent)
{
SetEvent(m_hPinEvent);
m_thread.join();
CloseHandle(m_hPinEvent);
}
CleanUp();
}
@ -240,10 +251,57 @@ bool AlwaysOnTop::AssignBorder(HWND window)
void AlwaysOnTop::RegisterHotkey() const
{
if (m_useCentralizedLLKH)
{
return;
}
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::Pin));
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
}
void AlwaysOnTop::RegisterLLKH()
{
if (!m_useCentralizedLLKH)
{
return;
}
m_hPinEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
if (!m_hPinEvent)
{
Logger::warn(L"Failed to create pinEvent. {}", get_last_error_or_default(GetLastError()));
return;
}
m_thread = std::thread([this]() {
MSG msg;
while (1)
{
DWORD dwEvt = MsgWaitForMultipleObjects(1, &m_hPinEvent, false, INFINITE, QS_ALLINPUT);
switch (dwEvt)
{
case WAIT_OBJECT_0:
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw);
}
break;
case WAIT_OBJECT_0 + 1:
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
break;
default:
return false;
}
}
});
}
void AlwaysOnTop::SubscribeToEvents()
{
// subscribe to windows events

View File

@ -13,7 +13,7 @@
class AlwaysOnTop : public SettingsObserver
{
public:
AlwaysOnTop();
AlwaysOnTop(bool useLLKH);
~AlwaysOnTop();
protected:
@ -47,12 +47,16 @@ private:
HWND m_window{ nullptr };
HINSTANCE m_hinstance;
std::map<HWND, std::unique_ptr<WindowBorder>> m_topmostWindows{};
HANDLE m_hPinEvent;
std::thread m_thread;
const bool m_useCentralizedLLKH;
LRESULT WndProc(HWND, UINT, WPARAM, LPARAM) noexcept;
void HandleWinHookEvent(WinHookEvent* data) noexcept;
bool InitMainWindow();
void RegisterHotkey() const;
void RegisterLLKH();
void SubscribeToEvents();
void ProcessCommand(HWND window);

View File

@ -52,7 +52,7 @@ int WINAPI wWinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance,
Trace::RegisterProvider();
AlwaysOnTop app;
AlwaysOnTop app(!pid.empty());
run_message_loop();

View File

@ -10,12 +10,26 @@
#include <AlwaysOnTop/ModuleConstants.h>
#include <shellapi.h>
#include <common/SettingsAPI/settings_objects.h>
#include <common/interop/shared_constants.h>
namespace NonLocalizable
{
const wchar_t ModulePath[] = L"modules\\AlwaysOnTop\\PowerToys.AlwaysOnTop.exe";
}
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_WIN[] = L"win";
const wchar_t JSON_KEY_ALT[] = L"alt";
const wchar_t JSON_KEY_CTRL[] = L"ctrl";
const wchar_t JSON_KEY_SHIFT[] = L"shift";
const wchar_t JSON_KEY_CODE[] = L"code";
const wchar_t JSON_KEY_HOTKEY[] = L"hotkey";
const wchar_t JSON_KEY_VALUE[] = L"value";
}
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved)
{
switch (ul_reason_for_call)
@ -52,15 +66,73 @@ public:
// Return JSON with the configuration options.
// These are the settings shown on the settings page along with their current values.
virtual bool get_config(_Out_ PWSTR buffer, _Out_ int* buffer_size) override
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
return false;
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
// Create a Settings object.
PowerToysSettings::Settings settings(hinstance, get_name());
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Passes JSON with the configuration settings for the powertoy.
// This is called when the user hits Save on the settings page.
virtual void set_config(PCWSTR config) override
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_hotkey(values);
// If you don't need to do any custom processing of the settings, proceed
// to persists the values calling:
values.save_to_settings_file();
// Otherwise call a custom function to process the settings before saving them to disk:
// save_settings();
}
catch (std::exception ex)
{
// Improper JSON.
}
}
virtual bool on_hotkey(size_t hotkeyId) override
{
if (m_enabled)
{
Logger::trace(L"AlwaysOnTop hotkey pressed");
if (!is_process_running())
{
Enable();
}
SetEvent(m_hPinEvent);
return true;
}
return false;
}
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
if (m_hotkey.key)
{
if (hotkeys && buffer_size >= 1)
{
hotkeys[0] = m_hotkey;
}
return 1;
}
else
{
return 0;
}
}
// Enable the powertoy
@ -96,6 +168,8 @@ public:
{
app_name = L"AlwaysOnTop"; //TODO: localize
app_key = NonLocalizable::ModuleKey;
m_hPinEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
init_settings();
}
private:
@ -109,6 +183,7 @@ private:
unsigned long powertoys_pid = GetCurrentProcessId();
std::wstring executable_args = L"";
executable_args.append(std::to_wstring(powertoys_pid));
ResetEvent(m_hPinEvent);
SHELLEXECUTEINFOW sei{ sizeof(sei) };
sei.fMask = { SEE_MASK_NOCLOSEPROCESS | SEE_MASK_FLAG_NO_UI };
@ -133,6 +208,8 @@ private:
void Disable(bool const traceEvent)
{
m_enabled = false;
ResetEvent(m_hPinEvent);
// Log telemetry
if (traceEvent)
{
@ -146,11 +223,72 @@ private:
}
}
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
auto jsonHotkeyObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_HOTKEY).GetNamedObject(JSON_KEY_VALUE);
m_hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
m_hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
m_hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
m_hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
m_hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
}
catch (...)
{
Logger::error("Failed to initialize AlwaysOnTop start shortcut");
}
}
else
{
Logger::info("AlwaysOnTop settings are empty");
}
if (!m_hotkey.key)
{
Logger::info("AlwaysOnTop is going to use default shortcut");
m_hotkey.win = true;
m_hotkey.alt = false;
m_hotkey.shift = false;
m_hotkey.ctrl = true;
m_hotkey.key = 'T';
}
}
bool is_process_running()
{
return WaitForSingleObject(m_hProcess, 0) == WAIT_TIMEOUT;
}
void init_settings()
{
try
{
// Load and parse the settings file for this PowerToy.
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(get_key());
parse_hotkey(settings);
}
catch (std::exception ex)
{
Logger::warn(L"An exception occurred while loading the settings file");
// Error while loading from the settings file. Let default values stay as they are.
}
}
std::wstring app_name;
std::wstring app_key; //contains the non localized key of the powertoy
bool m_enabled = false;
HANDLE m_hProcess = nullptr;
Hotkey m_hotkey;
// Handle to event used to pin/unpin windows
HANDLE m_hPinEvent;
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()

View File

@ -3,7 +3,9 @@
// See the LICENSE file in the project root for more information.
using System;
using System.Globalization;
using System.Runtime.CompilerServices;
using System.Text.Json;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
@ -97,6 +99,14 @@ namespace Microsoft.PowerToys.Settings.UI.Library.ViewModels
Settings.Properties.Hotkey.Value = _hotkey;
NotifyPropertyChanged();
// Using InvariantCulture as this is an IPC message
SendConfigMSG(
string.Format(
CultureInfo.InvariantCulture,
"{{ \"powertoys\": {{ \"{0}\": {1} }} }}",
AlwaysOnTopSettings.ModuleName,
JsonSerializer.Serialize(Settings)));
}
}
}