mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-06-07 09:28:03 +08:00
[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:
parent
2159e2722e
commit
301f26aca1
2
.github/actions/spell-check/expect.txt
vendored
2
.github/actions/spell-check/expect.txt
vendored
@ -35,6 +35,7 @@ ALarger
|
||||
alekhyareddy
|
||||
alignas
|
||||
ALLAPPS
|
||||
ALLINPUT
|
||||
Alloc
|
||||
ALLOWUNDO
|
||||
ALPHATYPE
|
||||
@ -1096,6 +1097,7 @@ LINQTo
|
||||
Linux
|
||||
listview
|
||||
lld
|
||||
LLKH
|
||||
llkhf
|
||||
Llvm
|
||||
lmcons
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
|
@ -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);
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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)));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user