mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
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:
1
.github/actions/spell-check/expect.txt
vendored
1
.github/actions/spell-check/expect.txt
vendored
@@ -1532,6 +1532,7 @@ riid
|
|||||||
RKey
|
RKey
|
||||||
RNumber
|
RNumber
|
||||||
rollups
|
rollups
|
||||||
|
ROOTOWNER
|
||||||
rop
|
rop
|
||||||
ROUNDSMALL
|
ROUNDSMALL
|
||||||
ROWSETEXT
|
ROWSETEXT
|
||||||
|
|||||||
@@ -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";
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
};
|
||||||
@@ -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()
|
||||||
|
|||||||
@@ -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=}"
|
||||||
|
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}">
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user