diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index 731fa0aca8..73438db10f 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -35,6 +35,7 @@ ALarger alekhyareddy alignas ALLAPPS +ALLINPUT Alloc ALLOWUNDO ALPHATYPE @@ -1096,6 +1097,7 @@ LINQTo Linux listview lld +LLKH llkhf Llvm lmcons diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h index 0cb38f97f9..688bf4907e 100644 --- a/src/common/interop/shared_constants.h +++ b/src/common/interop/shared_constants.h @@ -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; diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp index 931e806d2d..b0493c7c8e 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp +++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp @@ -9,6 +9,7 @@ #include #include +#include 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(&__ImageBase)) + m_hinstance(reinterpret_cast(&__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(HotkeyId::Pin)); RegisterHotKey(m_window, static_cast(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 diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h index ed6e5e7fcb..663aae65ba 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h +++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h @@ -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> 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); diff --git a/src/modules/alwaysontop/AlwaysOnTop/main.cpp b/src/modules/alwaysontop/AlwaysOnTop/main.cpp index 61716bc87b..4b7909079a 100644 --- a/src/modules/alwaysontop/AlwaysOnTop/main.cpp +++ b/src/modules/alwaysontop/AlwaysOnTop/main.cpp @@ -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(); diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp index 28227f83fb..e73f3364ca 100644 --- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp +++ b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp @@ -10,12 +10,26 @@ #include #include +#include +#include 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(&__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(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() diff --git a/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs b/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs index 59468962ea..4fac7c85c9 100644 --- a/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs +++ b/src/settings-ui/Settings.UI.Library/ViewModels/AlwaysOnTopViewModel.cs @@ -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))); } } }