Always on top: Add transparent support for on topped window (#44815)

<!-- 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
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.
<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [X] Closes: 
#43278 
#42929
#28773

<!-- - [ ] 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


https://github.com/user-attachments/assets/c97a87f2-3126-4e19-990f-8c684dbeb631

<img width="1119" height="426" alt="image"
src="https://github.com/user-attachments/assets/547671ee-81d3-4c94-8199-bf0c4b1b7760"
/>

---------

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kai Tao
2026-01-29 13:48:27 +08:00
committed by GitHub
parent 4986915dae
commit 6d4f56cd83
10 changed files with 409 additions and 25 deletions

View File

@@ -1532,6 +1532,7 @@ riid
RKey RKey
RNumber RNumber
rollups rollups
ROOTOWNER
rop rop
ROUNDSMALL ROUNDSMALL
ROWSETEXT ROWSETEXT

View File

@@ -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_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 // Path to the event used by PowerAccent
const wchar_t POWERACCENT_EXIT_EVENT[] = L"Local\\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17"; const wchar_t POWERACCENT_EXIT_EVENT[] = L"Local\\PowerToysPowerAccentExitEvent-53e93389-d19a-4fbb-9b36-1981c8965e17";

View File

@@ -153,9 +153,21 @@ LRESULT AlwaysOnTop::WndProc(HWND window, UINT message, WPARAM wparam, LPARAM lp
{ {
if (message == WM_HOTKEY) if (message == WM_HOTKEY)
{ {
int hotkeyId = static_cast<int>(wparam);
if (HWND fw{ GetForegroundWindow() }) if (HWND fw{ GetForegroundWindow() })
{ {
ProcessCommand(fw); if (hotkeyId == static_cast<int>(HotkeyId::Pin))
{
ProcessCommand(fw);
}
else if (hotkeyId == static_cast<int>(HotkeyId::IncreaseOpacity))
{
StepWindowTransparency(fw, Settings::transparencyStep);
}
else if (hotkeyId == static_cast<int>(HotkeyId::DecreaseOpacity))
{
StepWindowTransparency(fw, -Settings::transparencyStep);
}
} }
} }
else if (message == WM_PRIV_SETTINGS_CHANGED) else if (message == WM_PRIV_SETTINGS_CHANGED)
@@ -191,6 +203,10 @@ void AlwaysOnTop::ProcessCommand(HWND window)
m_topmostWindows.erase(iter); m_topmostWindows.erase(iter);
} }
// Restore transparency when unpinning
RestoreWindowAlpha(window);
m_windowOriginalLayeredState.erase(window);
Trace::AlwaysOnTop::UnpinWindow(); Trace::AlwaysOnTop::UnpinWindow();
} }
} }
@@ -200,6 +216,7 @@ void AlwaysOnTop::ProcessCommand(HWND window)
{ {
soundType = Sound::Type::On; soundType = Sound::Type::On;
AssignBorder(window); AssignBorder(window);
Trace::AlwaysOnTop::PinWindow(); Trace::AlwaysOnTop::PinWindow();
} }
} }
@@ -269,11 +286,22 @@ void AlwaysOnTop::RegisterHotkey() const
{ {
if (m_useCentralizedLLKH) if (m_useCentralizedLLKH)
{ {
// All hotkeys are handled by centralized LLKH
return; return;
} }
// Register hotkeys only when not using centralized LLKH
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::Pin)); UnregisterHotKey(m_window, static_cast<int>(HotkeyId::Pin));
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::IncreaseOpacity));
UnregisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity));
// Register pin hotkey
RegisterHotKey(m_window, static_cast<int>(HotkeyId::Pin), AlwaysOnTopSettings::settings().hotkey.get_modifiers(), AlwaysOnTopSettings::settings().hotkey.get_code()); RegisterHotKey(m_window, static_cast<int>(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<int>(HotkeyId::IncreaseOpacity), modifiers, VK_OEM_PLUS);
RegisterHotKey(m_window, static_cast<int>(HotkeyId::DecreaseOpacity), modifiers, VK_OEM_MINUS);
} }
void AlwaysOnTop::RegisterLLKH() void AlwaysOnTop::RegisterLLKH()
@@ -285,6 +313,8 @@ void AlwaysOnTop::RegisterLLKH()
m_hPinEvent = CreateEventW(nullptr, false, false, CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT); 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_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) if (!m_hPinEvent)
{ {
@@ -298,30 +328,54 @@ void AlwaysOnTop::RegisterLLKH()
return; return;
} }
HANDLE handles[2] = { m_hPinEvent, if (!m_hIncreaseOpacityEvent)
m_hTerminateEvent }; {
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]() { m_thread = std::thread([this, handles]() {
MSG msg; MSG msg;
while (m_running) while (m_running)
{ {
DWORD dwEvt = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT); DWORD dwEvt = MsgWaitForMultipleObjects(4, handles, false, INFINITE, QS_ALLINPUT);
if (!m_running) if (!m_running)
{ {
break; break;
} }
switch (dwEvt) switch (dwEvt)
{ {
case WAIT_OBJECT_0: case WAIT_OBJECT_0: // Pin event
if (HWND fw{ GetForegroundWindow() }) if (HWND fw{ GetForegroundWindow() })
{ {
ProcessCommand(fw); ProcessCommand(fw);
} }
break; break;
case WAIT_OBJECT_0 + 1: case WAIT_OBJECT_0 + 1: // Terminate event
PostThreadMessage(m_mainThreadId, WM_QUIT, 0, 0); PostThreadMessage(m_mainThreadId, WM_QUIT, 0, 0);
break; 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)) if (PeekMessageW(&msg, nullptr, 0, 0, PM_REMOVE))
{ {
TranslateMessage(&msg); TranslateMessage(&msg);
@@ -370,9 +424,12 @@ void AlwaysOnTop::UnpinAll()
{ {
Logger::error(L"Unpinning topmost window failed"); Logger::error(L"Unpinning topmost window failed");
} }
// Restore transparency when unpinning all
RestoreWindowAlpha(topWindow);
} }
m_topmostWindows.clear(); m_topmostWindows.clear();
m_windowOriginalLayeredState.clear();
} }
void AlwaysOnTop::CleanUp() void AlwaysOnTop::CleanUp()
@@ -456,6 +513,7 @@ void AlwaysOnTop::HandleWinHookEvent(WinHookEvent* data) noexcept
for (const auto window : toErase) for (const auto window : toErase)
{ {
m_topmostWindows.erase(window); m_topmostWindows.erase(window);
m_windowOriginalLayeredState.erase(window);
} }
switch (data->event) switch (data->event)
@@ -557,3 +615,165 @@ 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<BYTE>((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);
}
}
}

View File

@@ -10,6 +10,7 @@
#include <common/hooks/WinHookEvent.h> #include <common/hooks/WinHookEvent.h>
#include <common/notifications/NotificationUtil.h> #include <common/notifications/NotificationUtil.h>
#include <common/utils/window.h>
class AlwaysOnTop : public SettingsObserver class AlwaysOnTop : public SettingsObserver
{ {
@@ -38,6 +39,8 @@ private:
enum class HotkeyId : int enum class HotkeyId : int
{ {
Pin = 1, Pin = 1,
IncreaseOpacity = 2,
DecreaseOpacity = 3,
}; };
static inline AlwaysOnTop* s_instance = nullptr; static inline AlwaysOnTop* s_instance = nullptr;
@@ -48,8 +51,20 @@ private:
HWND m_window{ nullptr }; HWND m_window{ nullptr };
HINSTANCE m_hinstance; HINSTANCE m_hinstance;
std::map<HWND, std::unique_ptr<WindowBorder>> m_topmostWindows{}; std::map<HWND, std::unique_ptr<WindowBorder>> 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<HWND, WindowLayeredState> m_windowOriginalLayeredState{};
HANDLE m_hPinEvent; HANDLE m_hPinEvent;
HANDLE m_hTerminateEvent; HANDLE m_hTerminateEvent;
HANDLE m_hIncreaseOpacityEvent;
HANDLE m_hDecreaseOpacityEvent;
DWORD m_mainThreadId; DWORD m_mainThreadId;
std::thread m_thread; std::thread m_thread;
const bool m_useCentralizedLLKH; const bool m_useCentralizedLLKH;
@@ -78,6 +93,12 @@ private:
bool AssignBorder(HWND window); bool AssignBorder(HWND window);
void RefreshBorders(); 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; virtual void SettingsUpdate(SettingId type) override;
static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook, static void CALLBACK WinHookProc(HWINEVENTHOOK winEventHook,

View File

@@ -15,6 +15,9 @@ class SettingsObserver;
struct Settings struct Settings
{ {
PowerToysSettings::HotkeyObject hotkey = PowerToysSettings::HotkeyObject::from_settings(true, true, false, false, 84); // win + ctrl + T 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 enableFrame = true;
bool enableSound = true; bool enableSound = true;
bool roundCornersEnabled = true; bool roundCornersEnabled = true;

View File

@@ -2,7 +2,6 @@
#include "pch.h" #include "pch.h"
#include <atomic>
#include <mmsystem.h> // sound #include <mmsystem.h> // sound
class Sound class Sound
@@ -12,12 +11,10 @@ public:
{ {
On, On,
Off, Off,
IncreaseOpacity,
DecreaseOpacity,
}; };
Sound()
: isPlaying(false)
{}
void Play(Type type) void Play(Type type)
{ {
BOOL success = false; BOOL success = false;
@@ -29,6 +26,12 @@ public:
case Type::Off: case Type::Off:
success = PlaySound(TEXT("Media\\Speech Sleep.wav"), NULL, SND_FILENAME | SND_ASYNC); success = PlaySound(TEXT("Media\\Speech Sleep.wav"), NULL, SND_FILENAME | SND_ASYNC);
break; 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: default:
break; break;
} }
@@ -38,7 +41,4 @@ public:
Logger::error(L"Sound playing error"); Logger::error(L"Sound playing error");
} }
} }
private:
std::atomic<bool> isPlaying;
}; };

View File

@@ -105,17 +105,28 @@ public:
} }
} }
virtual bool on_hotkey(size_t /*hotkeyId*/) override virtual bool on_hotkey(size_t hotkeyId) override
{ {
if (m_enabled) if (m_enabled)
{ {
Logger::trace(L"AlwaysOnTop hotkey pressed"); Logger::trace(L"AlwaysOnTop hotkey pressed, id={}", hotkeyId);
if (!is_process_running()) if (!is_process_running())
{ {
Enable(); 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; return true;
} }
@@ -125,19 +136,48 @@ public:
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override 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 (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; // Hotkey 1: Increase opacity (same modifiers + VK_OEM_PLUS '=')
} if (m_hotkey.key)
else
{ {
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 // Enable the powertoy
@@ -175,6 +215,8 @@ public:
app_key = NonLocalizable::ModuleKey; app_key = NonLocalizable::ModuleKey;
m_hPinEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT); m_hPinEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_PIN_EVENT);
m_hTerminateEvent = CreateDefaultEvent(CommonSharedConstants::ALWAYS_ON_TOP_TERMINATE_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(); init_settings();
} }
@@ -292,6 +334,8 @@ private:
// Handle to event used to pin/unpin windows // Handle to event used to pin/unpin windows
HANDLE m_hPinEvent; HANDLE m_hPinEvent;
HANDLE m_hTerminateEvent; HANDLE m_hTerminateEvent;
HANDLE m_hIncreaseOpacityEvent;
HANDLE m_hDecreaseOpacityEvent;
}; };
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()

View File

@@ -38,6 +38,24 @@
</tkcontrols:SettingsCard> </tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items> </tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander> </tkcontrols:SettingsExpander>
<tkcontrols:SettingsExpander
x:Uid="AlwaysOnTop_TransparencyInfo"
HeaderIcon="{ui:FontIcon Glyph=&#xE790;}"
IsExpanded="True">
<tkcontrols:SettingsExpander.Items>
<tkcontrols:SettingsCard ContentAlignment="Left">
<controls:ShortcutWithTextLabelControl x:Uid="AlwaysOnTop_IncreaseOpacity" Keys="{x:Bind ViewModel.IncreaseOpacityKeysList, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard ContentAlignment="Left">
<controls:ShortcutWithTextLabelControl x:Uid="AlwaysOnTop_DecreaseOpacity" Keys="{x:Bind ViewModel.DecreaseOpacityKeysList, Mode=OneWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard>
<tkcontrols:SettingsCard.Description>
<TextBlock x:Uid="AlwaysOnTop_TransparencyRange" Style="{StaticResource SecondaryTextStyle}" />
</tkcontrols:SettingsCard.Description>
</tkcontrols:SettingsCard>
</tkcontrols:SettingsExpander.Items>
</tkcontrols:SettingsExpander>
</controls:SettingsGroup> </controls:SettingsGroup>
<controls:SettingsGroup x:Uid="AlwaysOnTop_Behavior_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}"> <controls:SettingsGroup x:Uid="AlwaysOnTop_Behavior_GroupSettings" IsEnabled="{x:Bind ViewModel.IsEnabled, Mode=OneWay}">

View File

@@ -3240,7 +3240,19 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<value>Activation shortcut</value> <value>Activation shortcut</value>
</data> </data>
<data name="AlwaysOnTop_ActivationShortcut.Description" xml:space="preserve"> <data name="AlwaysOnTop_ActivationShortcut.Description" xml:space="preserve">
<value>Customize the shortcut to pin or unpin an app window</value> <value>Customize the shortcut to pin or unpin an app window. Use the same modifier keys with + or - to adjust window transparency.</value>
</data>
<data name="AlwaysOnTop_TransparencyInfo.Header" xml:space="preserve">
<value>Transparency adjustment</value>
</data>
<data name="AlwaysOnTop_IncreaseOpacity.Text" xml:space="preserve">
<value>Increase opacity</value>
</data>
<data name="AlwaysOnTop_DecreaseOpacity.Text" xml:space="preserve">
<value>Decrease opacity</value>
</data>
<data name="AlwaysOnTop_TransparencyRange.Text" xml:space="preserve">
<value>Range: 20%-100%</value>
</data> </data>
<data name="Oobe_AlwaysOnTop.Title" xml:space="preserve"> <data name="Oobe_AlwaysOnTop.Title" xml:space="preserve">
<value>Always On Top</value> <value>Always On Top</value>

View File

@@ -5,6 +5,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using System.Text.Json; using System.Text.Json;
using global::PowerToys.GPOWrapper; using global::PowerToys.GPOWrapper;
@@ -133,6 +134,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
Settings.Properties.Hotkey.Value = _hotkey; Settings.Properties.Hotkey.Value = _hotkey;
NotifyPropertyChanged(); NotifyPropertyChanged();
// Also notify that transparency keys have changed
OnPropertyChanged(nameof(IncreaseOpacityKeysList));
OnPropertyChanged(nameof(DecreaseOpacityKeysList));
// Using InvariantCulture as this is an IPC message // Using InvariantCulture as this is an IPC message
SendConfigMSG( SendConfigMSG(
string.Format( string.Format(
@@ -289,6 +294,62 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
} }
} }
/// <summary>
/// Gets the keys list for increasing window opacity (modifier keys + "+").
/// </summary>
public List<object> IncreaseOpacityKeysList
{
get
{
var keys = GetModifierKeysList();
keys.Add("+");
return keys;
}
}
/// <summary>
/// Gets the keys list for decreasing window opacity (modifier keys + "-").
/// </summary>
public List<object> DecreaseOpacityKeysList
{
get
{
var keys = GetModifierKeysList();
keys.Add("-");
return keys;
}
}
/// <summary>
/// Gets only the modifier keys from the current hotkey setting.
/// </summary>
private List<object> GetModifierKeysList()
{
var modifierKeys = new List<object>();
if (_hotkey.Win)
{
modifierKeys.Add(92); // The Windows key
}
if (_hotkey.Ctrl)
{
modifierKeys.Add("Ctrl");
}
if (_hotkey.Alt)
{
modifierKeys.Add("Alt");
}
if (_hotkey.Shift)
{
modifierKeys.Add(16); // The Shift key
}
return modifierKeys;
}
public void NotifyPropertyChanged([CallerMemberName] string propertyName = null) public void NotifyPropertyChanged([CallerMemberName] string propertyName = null)
{ {
OnPropertyChanged(propertyName); OnPropertyChanged(propertyName);