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 image --------- 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 IncreaseOpacityKeysList + { + get + { + var keys = GetModifierKeysList(); + keys.Add("+"); + return keys; + } + } + + /// + /// Gets the keys list for decreasing window opacity (modifier keys + "-"). + /// + public List DecreaseOpacityKeysList + { + get + { + var keys = GetModifierKeysList(); + keys.Add("-"); + return keys; + } + } + + /// + /// Gets only the modifier keys from the current hotkey setting. + /// + private List GetModifierKeysList() + { + var modifierKeys = new List(); + + 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) { OnPropertyChanged(propertyName);