mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
<!-- Enter a brief description/summary of your PR here. What does it fix/what does it change/how was it tested (even manually, if necessary)? --> ## Summary of the Pull Request This pull request adds support for customizing the hotkeys used to increase and decrease the opacity of pinned windows in the Always On Top module. Previously, these shortcuts were hardcoded to use the same modifiers as the main pin hotkey. With these changes, users can now independently configure the increase and decrease opacity shortcuts via the settings UI, and the backend has been updated to respect and store these new settings. Another change: If window is not Always On Topped, the opacity change take no effect, so we should not intercept, we should pass through to minimize the impact. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [X] Closes: #46391, #46387 <!-- - [ ] Closes: #yyy (add separate lines for additional resolved issues) --> - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx <!-- Provide a more detailed description of the PR, other things fixed, or any additional comments/features here --> ## Detailed Description of the Pull Request / Additional comments <!-- Describe how you validated the behavior. Add automated tests wherever possible, but list manual validation steps taken as well --> ## Validation Steps Performed <img width="1184" height="351" alt="image" src="https://github.com/user-attachments/assets/5d20ffae-9f0c-4ce3-9d85-2ba1efea6301" /> <img width="336" height="244" alt="image" src="https://github.com/user-attachments/assets/a78cc4a3-9eb3-49f1-bbb9-d6db37554e53" /> Verified locally that transparency hotkey will not intercept the normal hotkey in window if Always on top not enabled --------- Co-authored-by: Niels Laute <niels.laute@live.nl>
375 lines
12 KiB
C++
375 lines
12 KiB
C++
#include "pch.h"
|
|
|
|
#include <interface/powertoy_module_interface.h>
|
|
|
|
#include <common/logger/logger.h>
|
|
#include <common/utils/resources.h>
|
|
#include <common/utils/winapi_error.h>
|
|
|
|
#include <AlwaysOnTop/trace.h>
|
|
#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"PowerToys.AlwaysOnTop.exe";
|
|
// Keep in sync with src\modules\alwaysontop\AlwaysOnTop\AlwaysOnTop.cpp
|
|
const wchar_t PinnedWindowProp[] = L"AlwaysOnTop_Pinned";
|
|
}
|
|
|
|
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_INCREASE_OPACITY_HOTKEY[] = L"increase-opacity-hotkey";
|
|
const wchar_t JSON_KEY_DECREASE_OPACITY_HOTKEY[] = L"decrease-opacity-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)
|
|
{
|
|
case DLL_PROCESS_ATTACH:
|
|
Trace::AlwaysOnTop::RegisterProvider();
|
|
break;
|
|
|
|
case DLL_THREAD_ATTACH:
|
|
case DLL_THREAD_DETACH:
|
|
break;
|
|
|
|
case DLL_PROCESS_DETACH:
|
|
Trace::AlwaysOnTop::UnregisterProvider();
|
|
break;
|
|
}
|
|
return TRUE;
|
|
}
|
|
|
|
class AlwaysOnTopModuleInterface : public PowertoyModuleIface
|
|
{
|
|
public:
|
|
// Return the localized display name of the powertoy
|
|
virtual PCWSTR get_name() override
|
|
{
|
|
return app_name.c_str();
|
|
}
|
|
|
|
// Return the non localized key of the powertoy, this will be cached by the runner
|
|
virtual const wchar_t* get_key() override
|
|
{
|
|
return app_key.c_str();
|
|
}
|
|
|
|
// Return the configured status for the gpo policy for the module
|
|
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
|
|
{
|
|
return powertoys_gpo::getConfiguredAlwaysOnTopEnabledValue();
|
|
}
|
|
|
|
// Return JSON with the configuration options.
|
|
// These are the settings shown on the settings page along with their current values.
|
|
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
|
|
{
|
|
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(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&)
|
|
{
|
|
// Improper JSON.
|
|
}
|
|
}
|
|
|
|
virtual bool on_hotkey(size_t hotkeyId) override
|
|
{
|
|
if (!m_enabled)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
|
|
|
|
if (hotkeyId == 0)
|
|
{
|
|
if (!is_process_running())
|
|
{
|
|
Enable();
|
|
}
|
|
|
|
SetEvent(m_hPinEvent);
|
|
return true;
|
|
}
|
|
|
|
if (hotkeyId == 1 || hotkeyId == 2)
|
|
{
|
|
const HWND foregroundWindow = GetForegroundWindow();
|
|
if (!foregroundWindow || !IsWindow(foregroundWindow) || !GetPropW(foregroundWindow, NonLocalizable::PinnedWindowProp))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
if (!is_process_running())
|
|
{
|
|
Enable();
|
|
}
|
|
|
|
SetEvent(hotkeyId == 1 ? m_hIncreaseOpacityEvent : m_hDecreaseOpacityEvent);
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
|
{
|
|
constexpr size_t hotkeyCount = 3;
|
|
Hotkey configuredHotkeys[hotkeyCount] = { m_hotkey, m_increaseOpacityHotkey, m_decreaseOpacityHotkey };
|
|
|
|
for (size_t i = 0; i < hotkeyCount; ++i)
|
|
{
|
|
configuredHotkeys[i].id = static_cast<int>(i);
|
|
configuredHotkeys[i].isShown = configuredHotkeys[i].key != 0;
|
|
}
|
|
|
|
if (hotkeys)
|
|
{
|
|
const size_t countToCopy = (buffer_size < hotkeyCount) ? buffer_size : hotkeyCount;
|
|
for (size_t i = 0; i < countToCopy; ++i)
|
|
{
|
|
hotkeys[i] = configuredHotkeys[i];
|
|
}
|
|
}
|
|
|
|
Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
|
|
configuredHotkeys[0].win,
|
|
configuredHotkeys[0].ctrl,
|
|
configuredHotkeys[0].shift,
|
|
configuredHotkeys[0].alt,
|
|
configuredHotkeys[0].key,
|
|
configuredHotkeys[0].isShown);
|
|
Logger::trace(L"AlwaysOnTop hotkey[1] (increase opacity): win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
|
|
configuredHotkeys[1].win,
|
|
configuredHotkeys[1].ctrl,
|
|
configuredHotkeys[1].shift,
|
|
configuredHotkeys[1].alt,
|
|
configuredHotkeys[1].key,
|
|
configuredHotkeys[1].isShown);
|
|
Logger::trace(L"AlwaysOnTop hotkey[2] (decrease opacity): win={}, ctrl={}, shift={}, alt={}, key={}, shown={}",
|
|
configuredHotkeys[2].win,
|
|
configuredHotkeys[2].ctrl,
|
|
configuredHotkeys[2].shift,
|
|
configuredHotkeys[2].alt,
|
|
configuredHotkeys[2].key,
|
|
configuredHotkeys[2].isShown);
|
|
|
|
Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", hotkeyCount);
|
|
return hotkeyCount;
|
|
}
|
|
|
|
// Enable the powertoy
|
|
virtual void enable()
|
|
{
|
|
Logger::info("AlwaysOnTop enabling");
|
|
|
|
Enable();
|
|
}
|
|
|
|
// Disable the powertoy
|
|
virtual void disable()
|
|
{
|
|
Logger::info("AlwaysOnTop disabling");
|
|
|
|
Disable(true);
|
|
}
|
|
|
|
// Returns if the powertoy is enabled
|
|
virtual bool is_enabled() override
|
|
{
|
|
return m_enabled;
|
|
}
|
|
|
|
// Destroy the powertoy and free memory
|
|
virtual void destroy() override
|
|
{
|
|
Disable(false);
|
|
delete this;
|
|
}
|
|
|
|
AlwaysOnTopModuleInterface()
|
|
{
|
|
app_name = L"AlwaysOnTop"; //TODO: localize
|
|
app_key = NonLocalizable::ModuleKey;
|
|
m_hPinEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
|
|
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_EVENT);
|
|
m_hIncreaseOpacityEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_INCREASE_OPACITY_EVENT);
|
|
m_hDecreaseOpacityEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_DECREASE_OPACITY_EVENT);
|
|
init_settings();
|
|
}
|
|
|
|
private:
|
|
void Enable()
|
|
{
|
|
m_enabled = true;
|
|
|
|
// Log telemetry
|
|
Trace::AlwaysOnTop::Enable(true);
|
|
|
|
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 };
|
|
sei.lpFile = NonLocalizable::ModulePath;
|
|
sei.nShow = SW_SHOWNORMAL;
|
|
sei.lpParameters = executable_args.data();
|
|
if (ShellExecuteExW(&sei) == false)
|
|
{
|
|
Logger::error(L"Failed to start AlwaysOnTop");
|
|
auto message = get_last_error_message(GetLastError());
|
|
if (message.has_value())
|
|
{
|
|
Logger::error(message.value());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
m_hProcess = sei.hProcess;
|
|
}
|
|
}
|
|
|
|
void Disable(bool const traceEvent)
|
|
{
|
|
m_enabled = false;
|
|
ResetEvent(m_hPinEvent);
|
|
|
|
// Log telemetry
|
|
if (traceEvent)
|
|
{
|
|
Trace::AlwaysOnTop::Enable(false);
|
|
}
|
|
|
|
SetEvent(m_hTerminateEvent);
|
|
|
|
// Wait for 1.5 seconds for the process to end correctly and stop etw tracer
|
|
WaitForSingleObject(m_hProcess, 1500);
|
|
|
|
// If process is still running, terminate it
|
|
if (m_hProcess)
|
|
{
|
|
TerminateProcess(m_hProcess, 0);
|
|
m_hProcess = nullptr;
|
|
}
|
|
}
|
|
|
|
void parse_hotkey(PowerToysSettings::PowerToyValues& settings)
|
|
{
|
|
const auto parseSingleHotkey = [](const winrt::Windows::Data::Json::JsonObject& propertiesObject, const wchar_t* hotkeyName, Hotkey& hotkey) {
|
|
try
|
|
{
|
|
auto jsonHotkeyObject = propertiesObject.GetNamedObject(hotkeyName).GetNamedObject(JSON_KEY_VALUE);
|
|
hotkey.win = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_WIN);
|
|
hotkey.alt = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_ALT);
|
|
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
|
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
|
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
|
}
|
|
catch (...)
|
|
{
|
|
}
|
|
};
|
|
|
|
auto settingsObject = settings.get_raw_json();
|
|
if (settingsObject.GetView().Size())
|
|
{
|
|
try
|
|
{
|
|
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
|
parseSingleHotkey(propertiesObject, JSON_KEY_HOTKEY, m_hotkey);
|
|
parseSingleHotkey(propertiesObject, JSON_KEY_INCREASE_OPACITY_HOTKEY, m_increaseOpacityHotkey);
|
|
parseSingleHotkey(propertiesObject, JSON_KEY_DECREASE_OPACITY_HOTKEY, m_decreaseOpacityHotkey);
|
|
}
|
|
catch (...)
|
|
{
|
|
Logger::error("Failed to initialize AlwaysOnTop shortcuts");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Logger::info("AlwaysOnTop settings are empty");
|
|
}
|
|
}
|
|
|
|
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&)
|
|
{
|
|
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{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = 'T' };
|
|
Hotkey m_increaseOpacityHotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = VK_OEM_PLUS };
|
|
Hotkey m_decreaseOpacityHotkey{ .win = true, .ctrl = true, .shift = false, .alt = false, .key = VK_OEM_MINUS };
|
|
|
|
// Handle to event used to pin/unpin windows
|
|
HANDLE m_hPinEvent;
|
|
HANDLE m_hTerminateEvent;
|
|
HANDLE m_hIncreaseOpacityEvent;
|
|
HANDLE m_hDecreaseOpacityEvent;
|
|
};
|
|
|
|
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
|
{
|
|
return new AlwaysOnTopModuleInterface();
|
|
}
|