mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
Always On Top: The opacity should be able to configure the hotkey individually (#46410)
<!-- 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>
This commit is contained in:
@@ -76,7 +76,7 @@ bool isExcluded(HWND window)
|
||||
}
|
||||
|
||||
AlwaysOnTop::AlwaysOnTop(bool useLLKH, DWORD mainThreadId) :
|
||||
SettingsObserver({SettingId::FrameEnabled, SettingId::Hotkey, SettingId::ExcludeApps, SettingId::ShowInSystemMenu}),
|
||||
SettingsObserver({ SettingId::FrameEnabled, SettingId::Hotkey, SettingId::IncreaseOpacityHotkey, SettingId::DecreaseOpacityHotkey, SettingId::ExcludeApps, SettingId::ShowInSystemMenu }),
|
||||
m_hinstance(reinterpret_cast<HINSTANCE>(&__ImageBase)),
|
||||
m_useCentralizedLLKH(useLLKH),
|
||||
m_mainThreadId(mainThreadId),
|
||||
@@ -150,6 +150,8 @@ void AlwaysOnTop::SettingsUpdate(SettingId id)
|
||||
switch (id)
|
||||
{
|
||||
case SettingId::Hotkey:
|
||||
case SettingId::IncreaseOpacityHotkey:
|
||||
case SettingId::DecreaseOpacityHotkey:
|
||||
{
|
||||
RegisterHotkey();
|
||||
}
|
||||
@@ -360,10 +362,8 @@ void AlwaysOnTop::RegisterHotkey() const
|
||||
// Register pin hotkey
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), settings->hotkey.get_modifiers(), settings->hotkey.get_code());
|
||||
|
||||
// Register transparency hotkeys using the same modifiers as the pin hotkey
|
||||
UINT modifiers = settings->hotkey.get_modifiers();
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity), settings->increaseOpacityHotkey.get_modifiers(), settings->increaseOpacityHotkey.get_code());
|
||||
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), settings->decreaseOpacityHotkey.get_modifiers(), settings->decreaseOpacityHotkey.get_code());
|
||||
}
|
||||
|
||||
void AlwaysOnTop::RegisterLLKH()
|
||||
|
||||
@@ -13,6 +13,8 @@ namespace NonLocalizable
|
||||
const static wchar_t* SettingsFileName = L"settings.json";
|
||||
|
||||
const static wchar_t* HotkeyID = L"hotkey";
|
||||
const static wchar_t* IncreaseOpacityHotkeyID = L"increase-opacity-hotkey";
|
||||
const static wchar_t* DecreaseOpacityHotkeyID = L"decrease-opacity-hotkey";
|
||||
const static wchar_t* SoundEnabledID = L"sound-enabled";
|
||||
const static wchar_t* ShowInSystemMenuID = L"show-in-system-menu";
|
||||
const static wchar_t* FrameEnabledID = L"frame-enabled";
|
||||
@@ -100,16 +102,21 @@ void AlwaysOnTopSettings::LoadSettings()
|
||||
const auto currentSettings = AlwaysOnTopSettings::settings();
|
||||
auto updatedSettings = std::make_shared<Settings>(*currentSettings);
|
||||
std::vector<SettingId> changedSettings;
|
||||
|
||||
if (const auto jsonVal = values.get_json(NonLocalizable::HotkeyID))
|
||||
{
|
||||
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
|
||||
if (updatedSettings->hotkey.get_modifiers() != val.get_modifiers() || updatedSettings->hotkey.get_key() != val.get_key() || updatedSettings->hotkey.get_code() != val.get_code())
|
||||
const auto updateHotkeySetting = [&](const wchar_t* hotkeyName, auto& currentHotkey, SettingId settingId) {
|
||||
if (const auto jsonVal = values.get_json(hotkeyName))
|
||||
{
|
||||
updatedSettings->hotkey = val;
|
||||
changedSettings.push_back(SettingId::Hotkey);
|
||||
auto val = PowerToysSettings::HotkeyObject::from_json(*jsonVal);
|
||||
if (currentHotkey.get_modifiers() != val.get_modifiers() || currentHotkey.get_key() != val.get_key() || currentHotkey.get_code() != val.get_code())
|
||||
{
|
||||
currentHotkey = val;
|
||||
changedSettings.push_back(settingId);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
updateHotkeySetting(NonLocalizable::HotkeyID, updatedSettings->hotkey, SettingId::Hotkey);
|
||||
updateHotkeySetting(NonLocalizable::IncreaseOpacityHotkeyID, updatedSettings->increaseOpacityHotkey, SettingId::IncreaseOpacityHotkey);
|
||||
updateHotkeySetting(NonLocalizable::DecreaseOpacityHotkeyID, updatedSettings->decreaseOpacityHotkey, SettingId::DecreaseOpacityHotkey);
|
||||
|
||||
if (const auto jsonVal = values.get_bool_value(NonLocalizable::SoundEnabledID))
|
||||
{
|
||||
|
||||
@@ -18,6 +18,8 @@ class SettingsObserver;
|
||||
struct Settings
|
||||
{
|
||||
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T
|
||||
PowerToysSettings::HotkeyObject increaseOpacityHotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, VK_OEM_PLUS); // win + ctrl + '+'
|
||||
PowerToysSettings::HotkeyObject decreaseOpacityHotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, VK_OEM_MINUS); // win + ctrl + '-'
|
||||
static constexpr int minTransparencyPercentage = 20; // minimum transparency (can't go below 20%)
|
||||
static constexpr int maxTransparencyPercentage = 100; // maximum (fully opaque)
|
||||
static constexpr int transparencyStep = 10; // step size for +/- adjustment
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
enum class SettingId
|
||||
{
|
||||
Hotkey = 0,
|
||||
IncreaseOpacityHotkey,
|
||||
DecreaseOpacityHotkey,
|
||||
SoundEnabled,
|
||||
ShowInSystemMenu,
|
||||
FrameEnabled,
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
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
|
||||
@@ -27,6 +29,8 @@ namespace
|
||||
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";
|
||||
}
|
||||
|
||||
@@ -107,27 +111,38 @@ public:
|
||||
|
||||
virtual bool on_hotkey(size_t hotkeyId) override
|
||||
{
|
||||
if (m_enabled)
|
||||
if (!m_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
|
||||
|
||||
if (hotkeyId == 0)
|
||||
{
|
||||
Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
|
||||
if (!is_process_running())
|
||||
{
|
||||
Enable();
|
||||
}
|
||||
|
||||
if (hotkeyId == 0)
|
||||
SetEvent(m_hPinEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (hotkeyId == 1 || hotkeyId == 2)
|
||||
{
|
||||
const HWND foregroundWindow = GetForegroundWindow();
|
||||
if (!foregroundWindow || !IsWindow(foregroundWindow) || !GetPropW(foregroundWindow, NonLocalizable::PinnedWindowProp))
|
||||
{
|
||||
SetEvent(m_hPinEvent);
|
||||
}
|
||||
else if (hotkeyId == 1)
|
||||
{
|
||||
SetEvent(m_hIncreaseOpacityEvent);
|
||||
}
|
||||
else if (hotkeyId == 2)
|
||||
{
|
||||
SetEvent(m_hDecreaseOpacityEvent);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!is_process_running())
|
||||
{
|
||||
Enable();
|
||||
}
|
||||
|
||||
SetEvent(hotkeyId == 1 ? m_hIncreaseOpacityEvent : m_hDecreaseOpacityEvent);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -136,48 +151,48 @@ public:
|
||||
|
||||
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
|
||||
{
|
||||
size_t count = 0;
|
||||
|
||||
// Hotkey 0: Pin/Unpin (e.g., Win+Ctrl+T)
|
||||
if (m_hotkey.key)
|
||||
constexpr size_t hotkeyCount = 3;
|
||||
Hotkey configuredHotkeys[hotkeyCount] = { m_hotkey, m_increaseOpacityHotkey, m_decreaseOpacityHotkey };
|
||||
|
||||
for (size_t i = 0; i < hotkeyCount; ++i)
|
||||
{
|
||||
if (hotkeys && buffer_size > count)
|
||||
{
|
||||
hotkeys[count] = m_hotkey;
|
||||
Logger::trace(L"AlwaysOnTop hotkey[0]: win={}, ctrl={}, shift={}, alt={}, key={}",
|
||||
m_hotkey.win, m_hotkey.ctrl, m_hotkey.shift, m_hotkey.alt, m_hotkey.key);
|
||||
}
|
||||
count++;
|
||||
configuredHotkeys[i].id = static_cast<int>(i);
|
||||
configuredHotkeys[i].isShown = configuredHotkeys[i].key != 0;
|
||||
}
|
||||
|
||||
// Hotkey 1: Increase opacity (same modifiers + VK_OEM_PLUS '=')
|
||||
if (m_hotkey.key)
|
||||
if (hotkeys)
|
||||
{
|
||||
if (hotkeys && buffer_size > count)
|
||||
const size_t countToCopy = (buffer_size < hotkeyCount) ? buffer_size : hotkeyCount;
|
||||
for (size_t i = 0; i < countToCopy; ++i)
|
||||
{
|
||||
hotkeys[count] = m_hotkey;
|
||||
hotkeys[count].key = VK_OEM_PLUS; // '=' key
|
||||
Logger::trace(L"AlwaysOnTop hotkey[1] (increase opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
|
||||
hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
|
||||
hotkeys[i] = configuredHotkeys[i];
|
||||
}
|
||||
count++;
|
||||
}
|
||||
|
||||
// Hotkey 2: Decrease opacity (same modifiers + VK_OEM_MINUS '-')
|
||||
if (m_hotkey.key)
|
||||
{
|
||||
if (hotkeys && buffer_size > count)
|
||||
{
|
||||
hotkeys[count] = m_hotkey;
|
||||
hotkeys[count].key = VK_OEM_MINUS; // '-' key
|
||||
Logger::trace(L"AlwaysOnTop hotkey[2] (decrease opacity): win={}, ctrl={}, shift={}, alt={}, key={}",
|
||||
hotkeys[count].win, hotkeys[count].ctrl, hotkeys[count].shift, hotkeys[count].alt, hotkeys[count].key);
|
||||
}
|
||||
count++;
|
||||
}
|
||||
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={}", count);
|
||||
return count;
|
||||
Logger::trace(L"AlwaysOnTop get_hotkeys returning count={}", hotkeyCount);
|
||||
return hotkeyCount;
|
||||
}
|
||||
|
||||
// Enable the powertoy
|
||||
@@ -279,21 +294,34 @@ private:
|
||||
|
||||
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 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));
|
||||
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 start shortcut");
|
||||
Logger::error("Failed to initialize AlwaysOnTop shortcuts");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -329,7 +357,9 @@ private:
|
||||
|
||||
bool m_enabled = false;
|
||||
HANDLE m_hProcess = nullptr;
|
||||
Hotkey m_hotkey;
|
||||
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;
|
||||
|
||||
Reference in New Issue
Block a user