From 6d4f56cd83d4d9d0327710154bdb64713a9b6556 Mon Sep 17 00:00:00 2001
From: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date: Thu, 29 Jan 2026 13:48:27 +0800
Subject: [PATCH] Always on top: Add transparent support for on topped window
(#44815)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## Summary of the Pull Request
Transparency support (best-effort)
> Not every window can be made transparent. Transparency is applied on a
best-effort basis and depends on how the target app/window is built and
rendered.
## When it may not work
* Windows with special rendering pipelines (e.g., certain
hardware-accelerated / compositor-managed surfaces).
* Some tool/popup/owned windows where the foreground window isn’t the
actual surface being drawn.
## How it works (high-level)
* Resolve the best target window (preferring the top-level/root window
over transient children).
* Apply Windows’ standard layered-window alpha mechanism (per-window
opacity) to adjust transparency.
* When unpinned, Restore the original opacity/state when possible.
If transparency doesn’t change, it means the window doesn’t support this
mechanism in its current configuration.
## PR Checklist
- [X] Closes:
#43278
#42929
#28773
- [ ] **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
## Detailed Description of the Pull Request / Additional comments
## Validation Steps Performed
https://github.com/user-attachments/assets/c97a87f2-3126-4e19-990f-8c684dbeb631
---------
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
.github/actions/spell-check/expect.txt | 1 +
src/common/interop/shared_constants.h | 4 +
.../alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp | 234 +++++++++++++++++-
.../alwaysontop/AlwaysOnTop/AlwaysOnTop.h | 21 ++
.../alwaysontop/AlwaysOnTop/Settings.h | 3 +
src/modules/alwaysontop/AlwaysOnTop/Sound.h | 16 +-
.../AlwaysOnTopModuleInterface/dllmain.cpp | 62 ++++-
.../SettingsXAML/Views/AlwaysOnTopPage.xaml | 18 ++
.../Settings.UI/Strings/en-us/Resources.resw | 14 +-
.../ViewModels/AlwaysOnTopViewModel.cs | 61 +++++
10 files changed, 409 insertions(+), 25 deletions(-)
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index fb6af4b35f..72ded1f791 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -1532,6 +1532,7 @@ riid
RKey
RNumber
rollups
+ROOTOWNER
rop
ROUNDSMALL
ROWSETEXT
diff --git a/src/common/interop/shared_constants.h b/src/common/interop/shared_constants.h
index 118683f24c..3dad776cf6 100644
--- a/src/common/interop/shared_constants.h
+++ b/src/common/interop/shared_constants.h
@@ -72,6 +72,10 @@ namespace CommonSharedConstants
const wchar_t ALWAYS_ON_TOP_TERMINATE_EVENT[] = L"Local\\AlwaysOnTopTerminateEvent-cfdf1eae-791f-4953-8021-2f18f3837eae";
+ const wchar_t ALWAYS_ON_TOP_INCREASE_OPACITY_EVENT[] = L"Local\\AlwaysOnTopIncreaseOpacityEvent-a1b2c3d4-e5f6-7890-abcd-ef1234567890";
+
+ const wchar_t ALWAYS_ON_TOP_DECREASE_OPACITY_EVENT[] = L"Local\\AlwaysOnTopDecreaseOpacityEvent-b2c3d4e5-f6a7-8901-bcde-f12345678901";
+
// Path to the event used by PowerAccent
const wchar_t POWERACCENT_EXIT_EVENT[] = L"Local\\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17";
diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp
index 8287ee1cce..f09cc2997f 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp
+++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.cpp
@@ -153,9 +153,21 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
{
if (message == WM_HOTKEY)
{
+ int hotkeyId = static_cast(wparam);
if (HWND fw{ GetForegroundWindow() })
{
- ProcessCommand(fw);
+ if (hotkeyId == static_cast(HotkeyId::Pin))
+ {
+ ProcessCommand(fw);
+ }
+ else if (hotkeyId == static_cast(HotkeyId::IncreaseOpacity))
+ {
+ StepWindowTransparency(fw, Settings::transparencyStep);
+ }
+ else if (hotkeyId == static_cast(HotkeyId::DecreaseOpacity))
+ {
+ StepWindowTransparency(fw, -Settings::transparencyStep);
+ }
}
}
else if (message == WM_PRIV_SETTINGS_CHANGED)
@@ -191,6 +203,10 @@ void AlwaysOnTop::ProcessCommand(HWND window)
m_topmostWindows.erase(iter);
}
+ // Restore transparency when unpinning
+ RestoreWindowAlpha(window);
+ m_windowOriginalLayeredState.erase(window);
+
Trace::AlwaysOnTop::UnpinWindow();
}
}
@@ -200,6 +216,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
{
soundType = Sound::Type::On;
AssignBorder(window);
+
Trace::AlwaysOnTop::PinWindow();
}
}
@@ -269,11 +286,22 @@ void AlwaysOnTop::RegisterHotkey() const
{
if (m_useCentralizedLLKH)
{
+ // All hotkeys are handled by centralized LLKH
return;
}
+ // Register hotkeys only when not using centralized LLKH
UnregisterHotKey(m_window, static_cast(HotkeyId::Pin));
+ UnregisterHotKey(m_window, static_cast(HotkeyId::IncreaseOpacity));
+ UnregisterHotKey(m_window, static_cast(HotkeyId::DecreaseOpacity));
+
+ // Register pin hotkey
RegisterHotKey(m_window, static_cast(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code());
+
+ // Register transparency hotkeys using the same modifiers as the pin hotkey
+ UINT modifiers = AlwaysOnTopSettings::settings().hotkey.get_modifiers();
+ RegisterHotKey(m_window, static_cast(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
+ RegisterHotKey(m_window, static_cast(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
}
void AlwaysOnTop::RegisterLLKH()
@@ -285,6 +313,8 @@ void AlwaysOnTop::RegisterLLKH()
m_hPinEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
m_hTerminateEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_EVENT);
+ m_hIncreaseOpacityEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_INCREASE_OPACITY_EVENT);
+ m_hDecreaseOpacityEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_DECREASE_OPACITY_EVENT);
if (!m_hPinEvent)
{
@@ -298,30 +328,54 @@ void AlwaysOnTop::RegisterLLKH()
return;
}
- HANDLE handles[2] = { m_hPinEvent,
- m_hTerminateEvent };
+ if (!m_hIncreaseOpacityEvent)
+ {
+ Logger::warn(L"Failed to create increaseOpacityEvent. {}", get_last_error_or_default(GetLastError()));
+ }
+
+ if (!m_hDecreaseOpacityEvent)
+ {
+ Logger::warn(L"Failed to create decreaseOpacityEvent. {}", get_last_error_or_default(GetLastError()));
+ }
+
+ HANDLE handles[4] = { m_hPinEvent,
+ m_hTerminateEvent,
+ m_hIncreaseOpacityEvent,
+ m_hDecreaseOpacityEvent };
m_thread = std::thread([this, handles]() {
MSG msg;
while (m_running)
{
- DWORD dwEvt = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT);
+ DWORD dwEvt = MsgWaitForMultipleObjects(4, handles, false, INFINITE, QS_ALLINPUT);
if (!m_running)
{
break;
}
switch (dwEvt)
{
- case WAIT_OBJECT_0:
+ case WAIT_OBJECT_0: // Pin event
if (HWND fw{ GetForegroundWindow() })
{
ProcessCommand(fw);
}
break;
- case WAIT_OBJECT_0 + 1:
+ case WAIT_OBJECT_0 + 1: // Terminate event
PostThreadMessage(m_mainThreadId, WM_QUIT, 0, 0);
break;
- case WAIT_OBJECT_0 + 2:
+ case WAIT_OBJECT_0 + 2: // Increase opacity event
+ if (HWND fw{ GetForegroundWindow() })
+ {
+ StepWindowTransparency(fw, Settings::transparencyStep);
+ }
+ break;
+ case WAIT_OBJECT_0 + 3: // Decrease opacity event
+ if (HWND fw{ GetForegroundWindow() })
+ {
+ StepWindowTransparency(fw, -Settings::transparencyStep);
+ }
+ break;
+ case WAIT_OBJECT_0 + 4: // Message queue
if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
@@ -370,9 +424,12 @@ void AlwaysOnTop::UnpinAll()
{
Logger::error(L"Unpinning topmost window failed");
}
+ // Restore transparency when unpinning all
+ RestoreWindowAlpha(topWindow);
}
m_topmostWindows.clear();
+ m_windowOriginalLayeredState.clear();
}
void AlwaysOnTop::CleanUp()
@@ -456,6 +513,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
for (const auto window : toErase)
{
m_topmostWindows.erase(window);
+ m_windowOriginalLayeredState.erase(window);
}
switch (data->event)
@@ -556,4 +614,166 @@ void AlwaysOnTop::RefreshBorders()
}
}
}
+}
+
+HWND AlwaysOnTop::ResolveTransparencyTargetWindow(HWND window)
+{
+ if (!window || !IsWindow(window))
+ {
+ return nullptr;
+ }
+
+ // Only allow transparency changes on pinned windows
+ if (!IsPinned(window))
+ {
+ return nullptr;
+ }
+
+ return window;
+}
+
+
+void AlwaysOnTop::StepWindowTransparency(HWND window, int delta)
+{
+ HWND targetWindow = ResolveTransparencyTargetWindow(window);
+ if (!targetWindow)
+ {
+ return;
+ }
+
+ int currentTransparency = Settings::maxTransparencyPercentage;
+ LONG exStyle = GetWindowLong(targetWindow, GWL_EXSTYLE);
+ if (exStyle & WS_EX_LAYERED)
+ {
+ BYTE alpha = 255;
+ if (GetLayeredWindowAttributes(targetWindow, nullptr, &alpha, nullptr))
+ {
+ currentTransparency = (alpha * 100) / 255;
+ }
+ }
+
+ int newTransparency = (std::max)(Settings::minTransparencyPercentage,
+ (std::min)(Settings::maxTransparencyPercentage, currentTransparency + delta));
+
+ if (newTransparency != currentTransparency)
+ {
+ ApplyWindowAlpha(targetWindow, newTransparency);
+
+ if (AlwaysOnTopSettings::settings().enableSound)
+ {
+ m_sound.Play(delta > 0 ? Sound::Type::IncreaseOpacity : Sound::Type::DecreaseOpacity);
+ }
+
+ Logger::debug(L"Transparency adjusted to {}%", newTransparency);
+ }
+}
+
+void AlwaysOnTop::ApplyWindowAlpha(HWND window, int percentage)
+{
+ if (!window || !IsWindow(window))
+ {
+ return;
+ }
+
+ percentage = (std::max)(Settings::minTransparencyPercentage,
+ (std::min)(Settings::maxTransparencyPercentage, percentage));
+
+ LONG exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ bool isCurrentlyLayered = (exStyle & WS_EX_LAYERED) != 0;
+
+ // Cache original state on first transparency application
+ if (m_windowOriginalLayeredState.find(window) == m_windowOriginalLayeredState.end())
+ {
+ WindowLayeredState state;
+ state.hadLayeredStyle = isCurrentlyLayered;
+
+ if (isCurrentlyLayered)
+ {
+ BYTE alpha = 255;
+ COLORREF colorKey = 0;
+ DWORD flags = 0;
+ if (GetLayeredWindowAttributes(window, &colorKey, &alpha, &flags))
+ {
+ state.originalAlpha = alpha;
+ state.usedColorKey = (flags & LWA_COLORKEY) != 0;
+ state.colorKey = colorKey;
+ }
+ else
+ {
+ Logger::warn(L"GetLayeredWindowAttributes failed for layered window, skipping");
+ return;
+ }
+ }
+ m_windowOriginalLayeredState[window] = state;
+ }
+
+ // Clear WS_EX_LAYERED first to ensure SetLayeredWindowAttributes works
+ if (isCurrentlyLayered)
+ {
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+ exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ }
+
+ BYTE alphaValue = static_cast((255 * percentage) / 100);
+ SetWindowLong(window, GWL_EXSTYLE, exStyle | WS_EX_LAYERED);
+ SetLayeredWindowAttributes(window, 0, alphaValue, LWA_ALPHA);
+}
+
+void AlwaysOnTop::RestoreWindowAlpha(HWND window)
+{
+ if (!window || !IsWindow(window))
+ {
+ return;
+ }
+
+ LONG exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ auto it = m_windowOriginalLayeredState.find(window);
+
+ if (it != m_windowOriginalLayeredState.end())
+ {
+ const auto& originalState = it->second;
+
+ if (originalState.hadLayeredStyle)
+ {
+ // Window originally had WS_EX_LAYERED - restore original attributes
+ // Clear and re-add to ensure clean state
+ if (exStyle & WS_EX_LAYERED)
+ {
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ exStyle = GetWindowLong(window, GWL_EXSTYLE);
+ }
+ SetWindowLong(window, GWL_EXSTYLE, exStyle | WS_EX_LAYERED);
+
+ // Restore original alpha and/or color key
+ DWORD flags = LWA_ALPHA;
+ if (originalState.usedColorKey)
+ {
+ flags |= LWA_COLORKEY;
+ }
+ SetLayeredWindowAttributes(window, originalState.colorKey, originalState.originalAlpha, flags);
+ SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+ }
+ else
+ {
+ // Window originally didn't have WS_EX_LAYERED - remove it completely
+ if (exStyle & WS_EX_LAYERED)
+ {
+ SetLayeredWindowAttributes(window, 0, 255, LWA_ALPHA);
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ SetWindowPos(window, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_FRAMECHANGED);
+ }
+ }
+
+ m_windowOriginalLayeredState.erase(it);
+ }
+ else
+ {
+ // Fallback: no cached state, just remove layered style
+ if (exStyle & WS_EX_LAYERED)
+ {
+ SetLayeredWindowAttributes(window, 0, 255, LWA_ALPHA);
+ SetWindowLong(window, GWL_EXSTYLE, exStyle & ~WS_EX_LAYERED);
+ }
+ }
}
\ No newline at end of file
diff --git a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h
index 0505c837a2..438eaa64c4 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h
+++ b/src/modules/alwaysontop/AlwaysOnTop/AlwaysOnTop.h
@@ -10,6 +10,7 @@
#include
#include
+#include
class AlwaysOnTop : public SettingsObserver
{
@@ -38,6 +39,8 @@ private:
enum class HotkeyId : int
{
Pin = 1,
+ IncreaseOpacity = 2,
+ DecreaseOpacity = 3,
};
static inline AlwaysOnTop* s_instance = nullptr;
@@ -48,8 +51,20 @@ private:
HWND m_window{ nullptr };
HINSTANCE m_hinstance;
std::map> m_topmostWindows{};
+
+ // Store original window layered state for proper restoration
+ struct WindowLayeredState {
+ bool hadLayeredStyle = false;
+ BYTE originalAlpha = 255;
+ bool usedColorKey = false;
+ COLORREF colorKey = 0;
+ };
+ std::map m_windowOriginalLayeredState{};
+
HANDLE m_hPinEvent;
HANDLE m_hTerminateEvent;
+ HANDLE m_hIncreaseOpacityEvent;
+ HANDLE m_hDecreaseOpacityEvent;
DWORD m_mainThreadId;
std::thread m_thread;
const bool m_useCentralizedLLKH;
@@ -78,6 +93,12 @@ private:
bool AssignBorder(HWND window);
void RefreshBorders();
+ // Transparency methods
+ HWND ResolveTransparencyTargetWindow(HWND window);
+ void StepWindowTransparency(HWND window, int delta);
+ void ApplyWindowAlpha(HWND window, int percentage);
+ void RestoreWindowAlpha(HWND window);
+
virtual void SettingsUpdate(SettingId type) override;
static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook,
diff --git a/src/modules/alwaysontop/AlwaysOnTop/Settings.h b/src/modules/alwaysontop/AlwaysOnTop/Settings.h
index 7b674c5cf0..9c0624298e 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/Settings.h
+++ b/src/modules/alwaysontop/AlwaysOnTop/Settings.h
@@ -15,6 +15,9 @@ class SettingsObserver;
struct Settings
{
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T
+ 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
bool enableFrame = true;
bool enableSound = true;
bool roundCornersEnabled = true;
diff --git a/src/modules/alwaysontop/AlwaysOnTop/Sound.h b/src/modules/alwaysontop/AlwaysOnTop/Sound.h
index e8f8dd5de4..3bb868b179 100644
--- a/src/modules/alwaysontop/AlwaysOnTop/Sound.h
+++ b/src/modules/alwaysontop/AlwaysOnTop/Sound.h
@@ -2,7 +2,6 @@
#include "pch.h"
-#include
#include // sound
class Sound
@@ -12,12 +11,10 @@ public:
{
On,
Off,
+ IncreaseOpacity,
+ DecreaseOpacity,
};
- Sound()
- : isPlaying(false)
- {}
-
void Play(Type type)
{
BOOL success = false;
@@ -29,6 +26,12 @@ public:
case Type::Off:
success = PlaySound(TEXT("Media\\Speech Sleep.wav"), NULL, SND_FILENAME | SND_ASYNC);
break;
+ case Type::IncreaseOpacity:
+ success = PlaySound(TEXT("Media\\Windows Hardware Insert.wav"), NULL, SND_FILENAME | SND_ASYNC);
+ break;
+ case Type::DecreaseOpacity:
+ success = PlaySound(TEXT("Media\\Windows Hardware Remove.wav"), NULL, SND_FILENAME | SND_ASYNC);
+ break;
default:
break;
}
@@ -38,7 +41,4 @@ public:
Logger::error(L"Sound playing error");
}
}
-
-private:
- std::atomic isPlaying;
};
\ No newline at end of file
diff --git a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
index 1ed96e79bd..bc52137ed2 100644
--- a/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
+++ b/src/modules/alwaysontop/AlwaysOnTopModuleInterface/dllmain.cpp
@@ -105,17 +105,28 @@ public:
}
}
- virtual bool on_hotkey(size_t /*hotkeyId*/) override
+ virtual bool on_hotkey(size_t hotkeyId) override
{
if (m_enabled)
{
- Logger::trace(L"AlwaysOnTop hotkey pressed");
+ Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
if (!is_process_running())
{
Enable();
}
- SetEvent(m_hPinEvent);
+ if (hotkeyId == 0)
+ {
+ SetEvent(m_hPinEvent);
+ }
+ else if (hotkeyId == 1)
+ {
+ SetEvent(m_hIncreaseOpacityEvent);
+ }
+ else if (hotkeyId == 2)
+ {
+ SetEvent(m_hDecreaseOpacityEvent);
+ }
return true;
}
@@ -125,19 +136,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)
{
- if (hotkeys && buffer_size >= 1)
+ if (hotkeys && buffer_size > count)
{
- hotkeys[0] = m_hotkey;
+ 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++;
+ }
- return 1;
- }
- else
+ // Hotkey 1: Increase opacity (same modifiers + VK_OEM_PLUS '=')
+ if (m_hotkey.key)
{
- return 0;
+ if (hotkeys && buffer_size > count)
+ {
+ 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);
+ }
+ 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 get_hotkeys returning count={}", count);
+ return count;
}
// Enable the powertoy
@@ -175,6 +215,8 @@ public:
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();
}
@@ -292,6 +334,8 @@ private:
// 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()
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
index d6194dc5b2..f36483ffb1 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/AlwaysOnTopPage.xaml
@@ -38,6 +38,24 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 83a524b938..2b8c39da57 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -3240,7 +3240,19 @@ Right-click to remove the key combination, thereby deactivating the shortcut.Activation shortcut
- Customize the shortcut to pin or unpin an app window
+ Customize the shortcut to pin or unpin an app window. Use the same modifier keys with + or - to adjust window transparency.
+
+
+ Transparency adjustment
+
+
+ Increase opacity
+
+
+ Decrease opacity
+
+
+ Range: 20%-100%
Always On Top
diff --git a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
index 0a4860b016..65842cf942 100644
--- a/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/AlwaysOnTopViewModel.cs
@@ -5,6 +5,7 @@
using System;
using System.Collections.Generic;
using System.Globalization;
+using System.Linq;
using System.Runtime.CompilerServices;
using System.Text.Json;
using global::PowerToys.GPOWrapper;
@@ -133,6 +134,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings.Properties.Hotkey.Value = _hotkey;
NotifyPropertyChanged();
+ // Also notify that transparency keys have changed
+ OnPropertyChanged(nameof(IncreaseOpacityKeysList));
+ OnPropertyChanged(nameof(DecreaseOpacityKeysList));
+
// Using InvariantCulture as this is an IPC message
SendConfigMSG(
string.Format(
@@ -289,6 +294,62 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
}
}
+ ///
+ /// Gets the keys list for increasing window opacity (modifier keys + "+").
+ ///
+ public List