Files
PowerToys/src/modules/GrabAndMove/GrabAndMove/main.cpp
Alex Mihaiuc 23a26428d3 Grab And Move mod click passthrough (#49121)
This lets Alt/Win + click/rclick (no drag) go through if no mouse
movement is recorded.
Now the target window goes either into the "all clicks go through" or
"all clicks are considered resizes/moves" based on whether the first
mouse action after the modifier is pressed is a click or the beginning
of a mouse drag action.

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

<!-- Please review the items on the PR checklist before submitting-->
## PR Checklist

- [ ] Closes: #xxx
<!-- - [ ] 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
2026-07-05 15:16:55 +02:00

1925 lines
68 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#include "pch.h"
#include <common/SettingsAPI/settings_objects.h>
#include <common/SettingsAPI/settings_helpers.h>
#include <common/Telemetry/ProjectTelemetry.h>
#include <common/utils/process_path.h>
#include <common/utils/ProcessWaiter.h>
#include <common/utils/excluded_apps.h>
#include <common/utils/game_mode.h>
#include "resource.h"
#include <dwmapi.h>
TRACELOGGING_DEFINE_PROVIDER(
g_hProvider,
"Microsoft.PowerToys",
// {38e8889b-9731-53f5-e901-e8a7c1753074}
(0x38e8889b, 0x9731, 0x53f5, 0xe9, 0x01, 0xe8, 0xa7, 0xc1, 0x75, 0x30, 0x74),
TraceLoggingOptionProjectTelemetry());
// ---------------------------------------------------------------------------
// Globals
// ---------------------------------------------------------------------------
static HINSTANCE g_hInstance = nullptr;
static ULONG_PTR g_gdiplusToken = 0; // GDI+ token for overlay border rendering
static HHOOK g_hhkKeyboard = nullptr;
static HHOOK g_hhkMouse = nullptr;
static HWND g_hMsgWnd = nullptr;
// 0 = Alt (default), 1 = Win
enum class GrabAndMoveModifier
{
Alt = 0,
Win = 1,
};
static GrabAndMoveModifier g_modifierKey = GrabAndMoveModifier::Alt;
static bool g_altPressed = false;
static bool g_winPressed = false; // true while LWIN or RWIN is held (Win modifier mode)
static bool g_ignoreModifier = false; // true if the user pressed the modifier then clicked the mouse without dragging
static bool g_winAbsorbed = false; // true if we absorbed a Win keydown
static bool g_dragging = false;
static bool g_dragFirstMove = false; // true until first WM_MOUSEMOVE of a drag
static HWND g_dragTarget = nullptr;
static POINT g_dragStart = {}; // cursor pos at drag start
static RECT g_dragWndRect = {}; // window rect at drag start
static HWND g_hOverlay = nullptr; // semi-transparent overlay during drag
// Current target window rect for overlay info display
static int g_overlayInfoX = 0, g_overlayInfoY = 0;
static int g_overlayInfoW = 0, g_overlayInfoH = 0;
// Visible frame overlay metrics. Computed once per drag/resize (cold path) and
// reused while rendering - never recomputed in the mouse-move hot path.
// Margins are the difference between GetWindowRect and the DWM extended frame
// bounds (the invisible resize border), so the fill and border hug the visible
// window. The border is drawn just inside the visible edge; Always On Top draws
// its own border just outside that edge, so the two stack into a clean double
// layer without Grab and Move having to widen its stroke.
static int g_overlayMarginL = 0, g_overlayMarginT = 0, g_overlayMarginR = 0, g_overlayMarginB = 0;
static int g_overlayCornerRadius = 0; // physical px; 0 = square corners
static int g_overlayBorderThickness = 4; // physical px
// Fluent "warning" gold - copy of WinUI SystemFillColorCaution
// (used as a ThemeResource for warnings across the Settings UI). A Win32 layered
// window can't resolve a ThemeResource, so the literal is required here.
static constexpr COLORREF OVERLAY_BORDER_COLOR = RGB(255, 185, 0); // #FFB900
// Border thickness in DIPs (scaled by the target window DPI).
static constexpr int OVERLAY_BORDER_DIP = 4;
// Translucent white wash painted over the visible window during a drag/resize,
// matching the prior overlay. ~40% opacity (premultiplied white = 0x66666666).
static constexpr BYTE OVERLAY_FILL_ALPHA = 0x66;
static bool g_shouldAbsorbAlt =
true; // true if we want to absorb Alt on the next keydown (set when Alt is pressed without dragging, cleared on next non-Alt key or Alt keyup)
static bool g_altAbsorbed = false; // true if we absorbed an Alt keydown
static bool g_dragConsumedAlt = false; // true if a drag consumed the absorbed Alt
static DWORD g_absorbedVk = 0; // VK code of absorbed Alt key
static DWORD g_absorbedScanCode = 0; // scan code for replay
static DWORD g_absorbedFlags = 0; // flags for replay (extended key, etc.)
static bool g_showGeometry = false; // true if we want to draw the X, Y, W and H on the overlay on move and resize
static bool g_doNotActivateOnGameMode = true; // true if GrabAndMove is suppressed when Windows Game Mode is active
static bool g_useAltResize = true; // This can be toggled from the settings. If false, Alt + right click does nothing.
// Count of non-modifier keys currently held. Used to suppress GrabAndMove when the
// modifier key is pressed while another key is already down (e.g. Q held, then modifier pressed).
static int g_heldNonAltKeyCount = 0;
// Per-vkCode tracking for held keys. Only the initial press increments
// g_heldNonAltKeyCount; auto-repeat keydowns are ignored. This avoids relying
// on KF_REPEAT which is not available in KBDLLHOOKSTRUCT::flags.
static bool g_keyHeld[256] = {};
// Resize handle identifiers
enum ResizeHandle
{
RESIZE_NONE,
RESIZE_TOP_LEFT,
RESIZE_TOP,
RESIZE_TOP_RIGHT,
RESIZE_RIGHT,
RESIZE_BOTTOM_RIGHT,
RESIZE_BOTTOM,
RESIZE_BOTTOM_LEFT,
RESIZE_LEFT
};
static bool g_resizing = false;
static bool g_resizeFirstMove = false;
static HWND g_resizeTarget = nullptr;
static POINT g_resizeLast = {}; // cursor pos from previous frame
static RECT g_resizeWndRect = {}; // current window rect (updated each frame)
static ResizeHandle g_currentHandle = RESIZE_NONE;
// Deferred activation state: a modifier+button press is held "pending" until the
// cursor moves past the drag threshold (promoting it to a real drag/resize) or the
// button is released first (replaying a normal modifier+click to the target).
static bool g_pendingDrag = false;
static bool g_pendingResize = false;
static HWND g_pendingTarget = nullptr;
static POINT g_pendingDownPt = {};
// Set when a pending press was replayed to the target on modifier release while the
// physical button was still down; the matching physical button-up is then swallowed.
static bool g_swallowNextLButtonUp = false;
static bool g_swallowNextRButtonUp = false;
// Set once ReplayPendingClick has replayed the absorbed modifier key-down for a pending
// click. The absorbed-modifier flags stay set so the keyboard hook still forwards the
// real modifier key-up; this guard stops the key-down from being replayed a second time.
static bool g_pendingModifierReplayed = false;
// Set once a drag or resize has actually started during the current modifier hold.
// While set, subsequent modifier+button presses skip the deferred/pending phase and
// begin the drag/resize immediately - the user has already committed to Grab and Move.
// Cleared when the modifier key is released.
static bool g_activatedDuringHold = false;
static const int MIN_WINDOW_WIDTH = 150;
static const int MIN_WINDOW_HEIGHT = 50;
// Minimum interval (ms) between move/resize updates. Lower = snappier but
// more CPU/GPU work. 0 = unlimited (every mouse event triggers an update).
// 4 ms ≈ 240 Hz, 8 ms ≈ 120 Hz, 16 ms ≈ 60 Hz.
static constexpr ULONGLONG THROTTLE_INTERVAL_MS = 16;
// QPC helpers for throttle
static ULONGLONG QpcMs()
{
static LARGE_INTEGER freq = {};
if (freq.QuadPart == 0)
QueryPerformanceFrequency(&freq);
LARGE_INTEGER now;
QueryPerformanceCounter(&now);
return static_cast<ULONGLONG>(now.QuadPart * 1000 / freq.QuadPart);
}
static ULONGLONG g_lastMoveTick = 0;
// Cached system cursors (loaded once at startup) fix #6
static HCURSOR g_curSizeAll = nullptr;
static HCURSOR g_curSizeNWSE = nullptr;
static HCURSOR g_curSizeNESW = nullptr;
static HCURSOR g_curSizeNS = nullptr;
static HCURSOR g_curSizeWE = nullptr;
// IsExcluded result cache keyed by HWND (cleared on foreground change / settings reload) fix #4
static std::unordered_map<HWND, bool> g_excludedCache;
// Custom message: posted from the settings thread to invalidate g_excludedCache
// on the main thread (where all other accesses occur), avoiding a data race.
static constexpr UINT WM_INVALIDATE_EXCLUDED_CACHE = WM_APP + 1;
static const wchar_t* const CLASS_NAME = L"GrabAndMove_MsgWnd";
static const wchar_t* const OVERLAY_CLASS_NAME = L"GrabAndMove_Overlay";
static const wchar_t* const APP_TITLE = L"GrabAndMove";
// Must match CommonSharedConstants::GRABANDMOVE_REFRESH_SETTINGS_EVENT in shared_constants.h
static const wchar_t* const GRABANDMOVE_REFRESH_SETTINGS_EVENT =
L"Local\\PowerToysGrabAndMove-RefreshSettingsEvent-a7b3c1d2-4e5f-6a7b-8c9d-0e1f2a3b4c5d";
// Must match CommonSharedConstants::GRABANDMOVE_EXIT_EVENT in shared_constants.h
static const wchar_t* const GRABANDMOVE_EXIT_EVENT =
L"Local\\PowerToysGrabAndMove-ExitEvent-b8c4d2e3-5f6a-7b8c-9d0e-1f2a3b4c5d6e";
// Lock-free excluded apps list readers use .load(), writer uses .store() (fix #5)
static std::atomic<std::shared_ptr<const std::vector<std::wstring>>> g_excludedApps{
std::make_shared<const std::vector<std::wstring>>()};
static HANDLE g_hReloadSettingsEvent = nullptr;
static HANDLE g_hExitEvent = nullptr;
static std::thread g_settingsThread;
static bool g_running = true;
static HWINEVENTHOOK g_hWinEventHook = nullptr;
static void StopDragging();
static void StopResizing();
static void EndInteraction(bool endDrag, bool endResize)
{
if (endDrag && g_dragging)
{
StopDragging();
}
if (endResize && g_resizing)
{
StopResizing();
}
}
// ---------------------------------------------------------------------------
// WinEvent hook detects foreground switch to elevated processes where
// low-level keyboard hooks stop delivering key-up events.
// ---------------------------------------------------------------------------
static void CALLBACK WinEventProc(HWINEVENTHOOK, DWORD, HWND hwnd, LONG, LONG, DWORD, DWORD)
{
// Ignore focus changes to our own windows these are benign and fire constantly
// (overlay creation/destruction, repositioned drag targets, etc.).
if (hwnd == g_hOverlay || hwnd == g_hMsgWnd)
return;
// Any foreground switch to a non-own window can eat key-up events (e.g. Win+L eats
// the 'L' keyup before the session locks). Always reset the held-key counter so that
// the next Alt/Win press is never blocked by a stale non-zero count.
g_heldNonAltKeyCount = 0;
memset(g_keyHeld, 0, sizeof(g_keyHeld));
// Invalidate the IsExcluded cache on foreground change (fix #4)
g_excludedCache.clear();
// Only validate modifier state when there is actually something to reset.
// Skipping here when all flags are clear prevents spurious resets that would
// break continuous Alt-dragging between multiple drags.
if (!g_altPressed && !g_winPressed && !g_altAbsorbed && !g_winAbsorbed && !g_dragging && !g_resizing)
return;
bool modActuallyHeld = (g_modifierKey == GrabAndMoveModifier::Win)
? ((GetAsyncKeyState(VK_LWIN) | GetAsyncKeyState(VK_RWIN)) & 0x8000) != 0
: ((GetAsyncKeyState(VK_LMENU) | GetAsyncKeyState(VK_RMENU)) & 0x8000) != 0;
if (!modActuallyHeld)
{
g_altPressed = false;
g_winPressed = false;
g_winAbsorbed = false;
g_altAbsorbed = false;
g_dragConsumedAlt = false;
g_ignoreModifier = false;
g_activatedDuringHold = false;
g_pendingDrag = false;
g_pendingResize = false;
g_pendingTarget = nullptr;
g_swallowNextLButtonUp = false;
g_swallowNextRButtonUp = false;
g_pendingModifierReplayed = false;
EndInteraction(true, true);
}
}
static bool IsSuppressedByGameMode()
{
// Remote sessions can report fullscreen notification states that are not actual games.
if (GetSystemMetrics(SM_REMOTESESSION))
{
return false;
}
return g_doNotActivateOnGameMode && detect_game_mode();
}
static bool IsActivationModifierPressed()
{
if (g_ignoreModifier)
{
return false;
}
if (g_modifierKey == GrabAndMoveModifier::Win)
return g_winPressed;
return g_altPressed;
}
enum class GrabAndMoveShortcutAction
{
Move,
Resize,
};
static void TraceShortcutUse(bool successful, GrabAndMoveShortcutAction action, const wchar_t* reason) noexcept
{
const wchar_t* actionName = action == GrabAndMoveShortcutAction::Move ? L"move" : L"resize";
TraceLoggingWrite(
g_hProvider,
"GrabAndMove_ShortcutUse",
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE),
TraceLoggingBoolean(successful, "Successful"),
TraceLoggingWideString(actionName, "Action"),
TraceLoggingWideString(reason, "Reason"));
}
// ---------------------------------------------------------------------------
// Settings file helpers
// ---------------------------------------------------------------------------
static void LoadSettingsFromFile()
{
try
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(L"GrabAndMove");
if (auto v = values.get_bool_value(L"shouldAbsorbAlt"))
{
g_shouldAbsorbAlt = *v;
}
if (auto v = values.get_bool_value(L"showGeometry"))
{
g_showGeometry = *v;
}
if (auto v = values.get_bool_value(L"doNotActivateOnGameMode"))
{
g_doNotActivateOnGameMode = *v;
}
if (auto v = values.get_bool_value(L"useAltResize"))
{
g_useAltResize = *v;
}
if (auto v = values.get_int_value(L"modifierKey"))
{
g_modifierKey = (*v == 1) ? GrabAndMoveModifier::Win : GrabAndMoveModifier::Alt;
}
if (auto v = values.get_string_value(L"excluded_apps"))
{
std::vector<std::wstring> apps;
std::wstring upper = *v;
CharUpperBuffW(upper.data(), static_cast<DWORD>(upper.length()));
std::wstring_view view(upper);
while (!view.empty())
{
// skip leading whitespace / newlines
auto start = view.find_first_not_of(L" \t\r\n");
if (start == std::wstring_view::npos)
break;
view.remove_prefix(start);
auto pos = view.find_first_of(L"\r\n");
if (pos == std::wstring_view::npos)
pos = view.length();
apps.emplace_back(view.substr(0, pos));
view.remove_prefix(pos);
}
g_excludedApps.store(
std::make_shared<const std::vector<std::wstring>>(std::move(apps)));
// Invalidate the cache on the main thread to avoid a data race
// g_excludedCache is an unordered_map accessed from the hook/main thread.
if (g_hMsgWnd)
PostMessage(g_hMsgWnd, WM_INVALIDATE_EXCLUDED_CACHE, 0, 0);
}
}
catch (...)
{
// Keep defaults on error
}
}
static void SettingsWatcherThread(DWORD mainThreadId)
{
HANDLE events[2] = { g_hReloadSettingsEvent, g_hExitEvent };
DWORD eventCount = g_hExitEvent ? 2 : 1;
while (g_running)
{
DWORD wait = WaitForMultipleObjects(eventCount, events, FALSE, 1000);
if (!g_running)
break;
if (wait == WAIT_OBJECT_0)
{
LoadSettingsFromFile();
}
else if (wait == WAIT_OBJECT_0 + 1)
{
// Exit event signaled by the module interface
PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
break;
}
}
}
// ---------------------------------------------------------------------------
// Overlay window helpers persistent window, shown/hidden per interaction
// ---------------------------------------------------------------------------
// Tracks the last rendered overlay dimensions so we can skip re-rendering
// when only the position changed (move-only path).
static int g_overlayRenderedW = 0;
static int g_overlayRenderedH = 0;
// Maps the DWM window corner preference to a base radius in DIPs, matching
// Always On Top (WindowCornerUtils::CornersRadius).
static int CornerRadiusForWindow(HWND hwnd)
{
// Remote sessions draw square windows even on Win11, yet still report DWMWCP_DEFAULT. Match the
// window: a remote session gets square (radius 0) so the overlay border doesn't round off the corner.
if (GetSystemMetrics(SM_REMOTESESSION))
{
return 0;
}
int pref = 0; // DWMWCP_DEFAULT
if (DwmGetWindowAttribute(hwnd, DWMWA_WINDOW_CORNER_PREFERENCE, &pref, sizeof(pref)) != S_OK)
{
return 0; // pre-Win11 / unsupported -> square corners
}
switch (pref)
{
case DWMWCP_ROUND:
return 8;
case DWMWCP_ROUNDSMALL:
return 4;
case DWMWCP_DEFAULT:
return 8;
default:
return 0; // DWMWCP_DONOTROUND
}
}
// Computes the overlay metrics (margins to the visible frame, corner radius, border
// thickness) for the target window. Cold path only: called at the start of a
// drag/resize and after un-maximize, never from the mouse-move hot path.
static void PrepareOverlayMetrics(HWND target)
{
g_overlayMarginL = g_overlayMarginT = g_overlayMarginR = g_overlayMarginB = 0;
g_overlayCornerRadius = 0;
g_overlayBorderThickness = OVERLAY_BORDER_DIP;
if (!target)
{
return;
}
const UINT dpi = GetDpiForWindow(target);
const float scale = (dpi != 0) ? dpi / 96.0f : 1.0f;
RECT windowRect{};
RECT frameRect{};
if (GetWindowRect(target, &windowRect) &&
SUCCEEDED(DwmGetWindowAttribute(target, DWMWA_EXTENDED_FRAME_BOUNDS, &frameRect, sizeof(frameRect))))
{
g_overlayMarginL = max(0, static_cast<int>(frameRect.left - windowRect.left));
g_overlayMarginT = max(0, static_cast<int>(frameRect.top - windowRect.top));
g_overlayMarginR = max(0, static_cast<int>(windowRect.right - frameRect.right));
g_overlayMarginB = max(0, static_cast<int>(windowRect.bottom - frameRect.bottom));
}
g_overlayCornerRadius = static_cast<int>(CornerRadiusForWindow(target) * scale);
g_overlayBorderThickness = static_cast<int>(OVERLAY_BORDER_DIP * scale);
}
// Draws an antialiased (optionally rounded) border stroke fully inside `rect` using
// GDI+. The stroke hugs the inner edge of `rect` (the visible window frame).
static void DrawOverlayBorder(Gdiplus::Graphics& graphics, const RECT& rect, int thickness, int radius)
{
const int w = rect.right - rect.left;
const int h = rect.bottom - rect.top;
if (w <= 0 || h <= 0 || thickness <= 0)
{
return;
}
// Keep the whole stroke inside the visible frame on every side.
thickness = min(thickness, min(w, h) / 2);
if (thickness <= 0)
{
return;
}
const float half = thickness / 2.0f;
const Gdiplus::RectF path(
rect.left + half,
rect.top + half,
static_cast<Gdiplus::REAL>(w) - thickness,
static_cast<Gdiplus::REAL>(h) - thickness);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
Gdiplus::Pen pen(
Gdiplus::Color(255, GetRValue(OVERLAY_BORDER_COLOR), GetGValue(OVERLAY_BORDER_COLOR), GetBValue(OVERLAY_BORDER_COLOR)),
static_cast<Gdiplus::REAL>(thickness));
if (radius <= 0)
{
graphics.DrawRectangle(&pen, path);
return;
}
// The stroke is centred, so the path corner radius is the window radius minus
// half the thickness; that keeps the outer edge aligned with the window corner.
const float pathRadius = max(0.0f, radius - half);
const float diameter = min(pathRadius * 2.0f, min(path.Width, path.Height));
if (diameter <= 0.0f)
{
graphics.DrawRectangle(&pen, path);
return;
}
Gdiplus::GraphicsPath border;
border.AddArc(path.X, path.Y, diameter, diameter, 180.0f, 90.0f);
border.AddArc(path.GetRight() - diameter, path.Y, diameter, diameter, 270.0f, 90.0f);
border.AddArc(path.GetRight() - diameter, path.GetBottom() - diameter, diameter, diameter, 0.0f, 90.0f);
border.AddArc(path.X, path.GetBottom() - diameter, diameter, diameter, 90.0f, 90.0f);
border.CloseFigure();
graphics.DrawPath(&pen, &border);
}
// Renders the overlay surface using per-pixel alpha via UpdateLayeredWindow.
// A translucent white wash covers the visible window (matching the prior overlay)
// with a tight warning-gold border on top, both hugging the visible window frame;
// the optional geometry label box is painted fully opaque so it remains legible
// regardless of what is beneath.
static void RenderOverlayContent(HWND hwnd, int cw, int ch)
{
if (!hwnd || cw <= 0 || ch <= 0)
return;
BITMAPINFO bmi = {};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = cw;
bmi.bmiHeader.biHeight = -ch; // top-down so (0,0) is top-left
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;
HDC screenDC = GetDC(nullptr);
DWORD* pBits = nullptr;
HBITMAP hDib = CreateDIBSection(screenDC, &bmi, DIB_RGB_COLORS,
reinterpret_cast<void**>(&pBits), nullptr, 0);
if (!hDib)
{
ReleaseDC(nullptr, screenDC);
return;
}
HDC memDC = CreateCompatibleDC(screenDC);
HBITMAP hOldBmp = static_cast<HBITMAP>(SelectObject(memDC, hDib));
// Start fully transparent.
memset(pBits, 0, static_cast<size_t>(cw) * ch * sizeof(DWORD));
// We apply a translucent white rect with a gold border.
// The overlay window spans GetWindowRect, so inset by
// the invisible-border margins so both hug the visible edge; Always On Top draws
// its own border just outside that edge, giving a clean double layer.
{
const RECT visible = {
g_overlayMarginL,
g_overlayMarginT,
cw - g_overlayMarginR,
ch - g_overlayMarginB
};
const int vw = visible.right - visible.left;
const int vh = visible.bottom - visible.top;
Gdiplus::Bitmap bitmap(cw, ch, cw * 4, PixelFormat32bppPARGB, reinterpret_cast<BYTE*>(pBits));
Gdiplus::Graphics graphics(&bitmap);
graphics.SetSmoothingMode(Gdiplus::SmoothingModeAntiAlias);
if (vw > 0 && vh > 0)
{
Gdiplus::SolidBrush fillBrush(Gdiplus::Color(OVERLAY_FILL_ALPHA, 255, 255, 255));
if (g_overlayCornerRadius > 0)
{
// Round the wash to match the window corners (and the border).
const float d = min(static_cast<float>(g_overlayCornerRadius) * 2.0f,
static_cast<float>(min(vw, vh)));
Gdiplus::GraphicsPath fillPath;
fillPath.AddArc(static_cast<float>(visible.left), static_cast<float>(visible.top), d, d, 180.0f, 90.0f);
fillPath.AddArc(static_cast<float>(visible.right) - d, static_cast<float>(visible.top), d, d, 270.0f, 90.0f);
fillPath.AddArc(static_cast<float>(visible.right) - d, static_cast<float>(visible.bottom) - d, d, d, 0.0f, 90.0f);
fillPath.AddArc(static_cast<float>(visible.left), static_cast<float>(visible.bottom) - d, d, d, 90.0f, 90.0f);
fillPath.CloseFigure();
graphics.FillPath(&fillBrush, &fillPath);
}
else
{
graphics.FillRectangle(&fillBrush, visible.left, visible.top, vw, vh);
}
}
DrawOverlayBorder(graphics, visible, g_overlayBorderThickness, g_overlayCornerRadius);
graphics.Flush();
}
if (g_showGeometry)
{
wchar_t text[128];
swprintf_s(text, L"X: %d Y: %d\nW: %d H: %d",
g_overlayInfoX, g_overlayInfoY, g_overlayInfoW, g_overlayInfoH);
HFONT hFont = static_cast<HFONT>(GetStockObject(DEFAULT_GUI_FONT));
HFONT hOldFont = static_cast<HFONT>(SelectObject(memDC, hFont));
RECT textRect = {};
DrawTextW(memDC, text, -1, &textRect, DT_CALCRECT | DT_CENTER | DT_NOPREFIX);
const int pad = 10;
const int boxW = (textRect.right - textRect.left) + pad * 2;
const int boxH = (textRect.bottom - textRect.top) + pad * 2;
RECT boxRect = {cw / 2 - boxW / 2, ch / 2 - boxH / 2,
cw / 2 + boxW / 2, ch / 2 + boxH / 2};
HBRUSH hBlack = CreateSolidBrush(RGB(0, 0, 0));
FillRect(memDC, &boxRect, hBlack);
DeleteObject(hBlack);
RECT textDrawRect = {boxRect.left + pad, boxRect.top + pad,
boxRect.right - pad, boxRect.bottom - pad};
SetTextColor(memDC, RGB(255, 255, 255));
SetBkMode(memDC, TRANSPARENT);
DrawTextW(memDC, text, -1, &textDrawRect, DT_CENTER | DT_NOPREFIX);
SelectObject(memDC, hOldFont);
// GDI zeroes the alpha byte for every pixel it touches in a 32bpp DIB.
// Walk the box region and force A=255 so those pixels are fully opaque.
// With A=255 premultiplied alpha equals straight RGB, so the colours GDI
// wrote (black fill, white text, anti-aliased edges) are correct as-is.
const int x0 = max(0, boxRect.left);
const int y0 = max(0, boxRect.top);
const int x1 = min(cw, boxRect.right);
const int y1 = min(ch, boxRect.bottom);
for (int y = y0; y < y1; ++y)
for (int x = x0; x < x1; ++x)
pBits[y * cw + x] |= 0xFF000000u;
}
SIZE sz = {cw, ch};
POINT ptSrc = {0, 0};
BLENDFUNCTION blend = {AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
UpdateLayeredWindow(hwnd, screenDC, nullptr, &sz, memDC, &ptSrc, 0, &blend, ULW_ALPHA);
SelectObject(memDC, hOldBmp);
DeleteObject(hDib);
DeleteDC(memDC);
ReleaseDC(nullptr, screenDC);
g_overlayRenderedW = cw;
g_overlayRenderedH = ch;
}
// Ensures the persistent overlay window exists (created once, reused).
static void EnsureOverlayWindow()
{
if (g_hOverlay)
return;
g_hOverlay = CreateWindowExW(
WS_EX_LAYERED | WS_EX_TOPMOST | WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
OVERLAY_CLASS_NAME,
nullptr,
WS_POPUP, // initially hidden (no WS_VISIBLE)
0, 0, 1, 1,
nullptr,
nullptr,
g_hInstance,
nullptr);
}
static void ShowOverlay(const RECT& rc, HCURSOR hCursor)
{
EnsureOverlayWindow();
if (!g_hOverlay)
return;
int w = rc.right - rc.left;
int h = rc.bottom - rc.top;
SetClassLongPtrW(g_hOverlay, GCLP_HCURSOR, reinterpret_cast<LONG_PTR>(hCursor));
g_overlayInfoX = rc.left;
g_overlayInfoY = rc.top;
g_overlayInfoW = w;
g_overlayInfoH = h;
g_overlayRenderedW = 0; // force re-render
g_overlayRenderedH = 0;
SetWindowPos(g_hOverlay, HWND_TOPMOST, rc.left, rc.top, w, h,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
RenderOverlayContent(g_hOverlay, w, h);
}
// Repositions (and optionally re-renders) the overlay.
// For move-only (size unchanged), skips the expensive RenderOverlayContent.
// For resize (size changed), always re-renders so the layered surface matches.
static void RepositionOverlay(int x, int y, int w, int h)
{
if (!g_hOverlay)
return;
g_overlayInfoX = x;
g_overlayInfoY = y;
g_overlayInfoW = w;
g_overlayInfoH = h;
SetWindowPos(g_hOverlay, HWND_TOPMOST, x, y, w, h,
SWP_NOACTIVATE | SWP_SHOWWINDOW);
// Re-render only when the size changed or geometry text needs updating
bool sizeChanged = (w != g_overlayRenderedW || h != g_overlayRenderedH);
if (sizeChanged || g_showGeometry)
{
RenderOverlayContent(g_hOverlay, w, h);
}
}
static void HideOverlay()
{
if (g_hOverlay)
{
ShowWindow(g_hOverlay, SW_HIDE);
}
}
static void StopDragging()
{
g_dragging = false;
g_dragFirstMove = false;
g_dragTarget = nullptr;
HideOverlay();
}
static ResizeHandle GetClosestHandle(POINT pt, const RECT& rc)
{
int cx = (rc.left + rc.right) / 2;
int cy = (rc.top + rc.bottom) / 2;
struct
{
int x;
int y;
ResizeHandle h;
} handles[] = {
{rc.left, rc.top, RESIZE_TOP_LEFT},
{cx, rc.top, RESIZE_TOP},
{rc.right, rc.top, RESIZE_TOP_RIGHT},
{rc.right, cy, RESIZE_RIGHT},
{rc.right, rc.bottom, RESIZE_BOTTOM_RIGHT},
{cx, rc.bottom, RESIZE_BOTTOM},
{rc.left, rc.bottom, RESIZE_BOTTOM_LEFT},
{rc.left, cy, RESIZE_LEFT},
};
ResizeHandle closest = RESIZE_BOTTOM_RIGHT;
LONG minDist = (std::numeric_limits<LONG>::max)();
for (auto& e : handles)
{
LONG dx = pt.x - e.x;
LONG dy = pt.y - e.y;
LONG dist = dx * dx + dy * dy;
if (dist < minDist)
{
minDist = dist;
closest = e.h;
}
}
return closest;
}
static HCURSOR CursorForHandle(ResizeHandle handle)
{
switch (handle)
{
case RESIZE_TOP_LEFT:
case RESIZE_BOTTOM_RIGHT:
return g_curSizeNWSE;
case RESIZE_TOP_RIGHT:
case RESIZE_BOTTOM_LEFT:
return g_curSizeNESW;
case RESIZE_TOP:
case RESIZE_BOTTOM:
return g_curSizeNS;
case RESIZE_LEFT:
case RESIZE_RIGHT:
return g_curSizeWE;
default:
return g_curSizeAll;
}
}
static void StopResizing()
{
g_resizing = false;
g_resizeFirstMove = false;
g_resizeTarget = nullptr;
g_currentHandle = RESIZE_NONE;
HideOverlay();
}
static void ReplayAbsorbedModifier(bool alsoKeyUp)
{
INPUT inputs[2] = {};
inputs[0].type = INPUT_KEYBOARD;
inputs[0].ki.wVk = static_cast<WORD>(g_absorbedVk);
inputs[0].ki.wScan = static_cast<WORD>(g_absorbedScanCode);
inputs[0].ki.dwFlags = (g_absorbedFlags & LLKHF_EXTENDED) ? KEYEVENTF_EXTENDEDKEY : 0;
inputs[1] = inputs[0];
inputs[1].ki.dwFlags |= KEYEVENTF_KEYUP;
SendInput(alsoKeyUp ? 2 : 1, inputs, sizeof(INPUT));
}
// ---------------------------------------------------------------------------
// Deferred activation helpers.
// A modifier+button press does not start an interaction immediately; it is held
// "pending" until the cursor moves past the drag threshold (then it becomes a
// real drag/resize) or the button is released first (then the absorbed input is
// replayed so the target application receives a normal modifier+click).
// ---------------------------------------------------------------------------
static void BeginDrag(HWND hwnd, POINT pt)
{
g_dragging = true;
g_dragFirstMove = true;
g_dragConsumedAlt = true;
g_activatedDuringHold = true;
g_dragTarget = hwnd;
g_dragStart = pt;
GetWindowRect(hwnd, &g_dragWndRect);
PrepareOverlayMetrics(hwnd);
ShowOverlay(g_dragWndRect, g_curSizeAll);
TraceShortcutUse(true, GrabAndMoveShortcutAction::Move, L"started");
}
static void BeginResize(HWND hwnd, POINT pt)
{
g_resizing = true;
g_resizeFirstMove = true;
g_dragConsumedAlt = true;
g_activatedDuringHold = true;
g_resizeTarget = hwnd;
g_resizeLast = pt;
GetWindowRect(hwnd, &g_resizeWndRect);
PrepareOverlayMetrics(hwnd);
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
ShowOverlay(g_resizeWndRect, CursorForHandle(g_currentHandle));
TraceShortcutUse(true, GrabAndMoveShortcutAction::Resize, L"started");
}
// Replays a modifier+click to the target: first the absorbed modifier key-down
// (if it was swallowed), then the button click. The modifier key-up is not
// synthesized here - the real key-up still reaches the target when the user
// releases the modifier. The absorbed-modifier flags are intentionally left set so
// the keyboard hook forwards that real key-up; g_pendingModifierReplayed guards
// against replaying the key-down more than once (e.g. across repeated clicks).
static void ReplayPendingClick(bool isRight)
{
if ((g_altAbsorbed || g_winAbsorbed) && !g_pendingModifierReplayed)
{
g_pendingModifierReplayed = true;
ReplayAbsorbedModifier(false); // modifier down only
}
INPUT inputs[2] = {};
inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dwFlags = isRight ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_LEFTDOWN;
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = isRight ? MOUSEEVENTF_RIGHTUP : MOUSEEVENTF_LEFTUP;
SendInput(2, inputs, sizeof(INPUT));
}
// Called when the modifier is released while a press is still pending (button
// held, no move yet). Delivers the click to the target now and swallows the
// matching physical button-up that will arrive later.
static void FlushPendingClickOnModifierRelease()
{
if (g_pendingDrag)
{
g_pendingDrag = false;
g_pendingTarget = nullptr;
ReplayPendingClick(false);
g_swallowNextLButtonUp = true;
}
else if (g_pendingResize)
{
g_pendingResize = false;
g_pendingTarget = nullptr;
ReplayPendingClick(true);
g_swallowNextRButtonUp = true;
}
}
// ---------------------------------------------------------------------------
// System tray helpers
// ---------------------------------------------------------------------------
static NOTIFYICONDATAW g_nid = {};
static void AddTrayIcon(HWND hwnd)
{
g_nid.cbSize = sizeof(NOTIFYICONDATAW);
g_nid.hWnd = hwnd;
g_nid.uID = 1;
g_nid.uFlags = NIF_ICON | NIF_MESSAGE | NIF_TIP;
g_nid.uCallbackMessage = WM_TRAY_ICON;
g_nid.hIcon = LoadIconW(g_hInstance, MAKEINTRESOURCEW(IDI_APP_ICON));
wcscpy_s(g_nid.szTip, APP_TITLE);
Shell_NotifyIconW(NIM_ADD, &g_nid);
}
static void RemoveTrayIcon()
{
Shell_NotifyIconW(NIM_DELETE, &g_nid);
}
static void ShowTrayMenu(HWND hwnd)
{
HMENU hMenu = CreatePopupMenu();
if (!hMenu)
return;
AppendMenuW(hMenu, MF_STRING, IDM_EXIT, L"Exit");
POINT pt;
GetCursorPos(&pt);
// Required so the menu dismisses when clicking outside it
SetForegroundWindow(hwnd);
TrackPopupMenu(hMenu, TPM_RIGHTBUTTON, pt.x, pt.y, 0, hwnd, nullptr);
PostMessageW(hwnd, WM_NULL, 0, 0);
DestroyMenu(hMenu);
}
static bool IsSystemClass(HWND hwnd)
{
wchar_t cls[256] = {};
GetClassNameW(hwnd, cls, ARRAYSIZE(cls));
// Desktop and primary/secondary taskbars
if (wcscmp(cls, L"Progman") == 0 ||
wcscmp(cls, L"Shell_TrayWnd") == 0 ||
wcscmp(cls, L"Shell_SecondaryTrayWnd") == 0)
return true;
// System tray / notification area popups and overflow
if (wcscmp(cls, L"NotifyIconOverflowWindow") == 0 ||
wcscmp(cls, L"TopLevelWindowForOverflowXamlIsland") == 0)
return true;
// Tooltips (e.g. "Show hidden icons" tooltip)
if (wcscmp(cls, L"tooltips_class32") == 0)
return true;
// Task View (Win+Tab)
if (wcscmp(cls, L"MultitaskingViewFrame") == 0 ||
wcscmp(cls, L"XamlExplorerHostIslandWindow") == 0)
return true;
// System tray flyouts (Quick Settings, calendar, input switcher)
if (wcscmp(cls, L"Windows.UI.Composition.DesktopWindowContentBridge") == 0 ||
wcscmp(cls, L"Shell_InputSwitchTopLevelWindow") == 0)
return true;
return false;
}
std::wstring ToUpperInvariant(std::wstring_view input)
{
int required = LCMapStringEx(
LOCALE_NAME_INVARIANT,
LCMAP_UPPERCASE,
input.data(),
static_cast<int>(input.size()),
nullptr,
0,
nullptr,
nullptr,
0);
std::wstring result(required, L'\0');
LCMapStringEx(
LOCALE_NAME_INVARIANT,
LCMAP_UPPERCASE,
input.data(),
static_cast<int>(input.size()),
result.data(),
required,
nullptr,
nullptr,
0);
return result;
}
static bool IsExcluded(HWND hwnd)
{
if (IsSystemClass(hwnd))
return true;
// To identify these for adding a new exception:
// 1. Resolve the hwnd class name.
// 2. Resolve the process path.
// 3. Add OutputDebugStringW() for the class name and process path.
// 4. Build the executable.
// 5. Check with the debugger (or with Sysinternals DebugView) the outputs.
// 6. Delete the added code.
// 7. Add the exception below, according to the pattern there.
//
// Shell experience windows: Start menu, Notifications (Win+N), Search,
// Quick Settings (volume / network / battery).
// These use the generic Windows.UI.Core.CoreWindow class, so filter by process.
{
wchar_t cls[256] = {};
GetClassNameW(hwnd, cls, ARRAYSIZE(cls));
if (wcscmp(cls, L"Windows.UI.Core.CoreWindow") == 0)
{
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
if (processPath.find(L"STARTMENUEXPERIENCEHOST.EXE") != std::wstring::npos ||
processPath.find(L"SHELLEXPERIENCEHOST.EXE") != std::wstring::npos ||
processPath.find(L"SEARCHHOST.EXE") != std::wstring::npos)
return true;
}
else if (wcscmp(cls, L"ControlCenterWindow") == 0)
{
// The Quick Settings flyout.
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
if (processPath.find(L"SHELLHOST.EXE") != std::wstring::npos)
return true;
}
else if (wcscmp(cls, L"WindowsDashboard") == 0)
{
// The Windows 11 Widgets flyout.
std::wstring processPath = ToUpperInvariant(get_process_path(hwnd));
if (processPath.find(L"WIDGETBOARD.EXE") != std::wstring::npos)
return true;
}
}
auto apps = g_excludedApps.load();
if (!apps || apps->empty())
return false;
// Check process-path exclusion (cached per HWND fix #4)
bool pathExcluded = false;
auto it = g_excludedCache.find(hwnd);
if (it != g_excludedCache.end())
{
pathExcluded = it->second;
}
else
{
std::wstring processPath = get_process_path(hwnd);
CharUpperBuffW(processPath.data(), static_cast<DWORD>(processPath.length()));
pathExcluded = find_app_name_in_path(processPath, *apps);
g_excludedCache[hwnd] = pathExcluded;
}
if (pathExcluded)
return true;
// Title-based exclusion is always evaluated live (titles can change)
if (check_excluded_app_with_title(hwnd, *apps))
return true;
return false;
}
// ---------------------------------------------------------------------------
// Hook callbacks
// ---------------------------------------------------------------------------
static LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
auto* kb = reinterpret_cast<KBDLLHOOKSTRUCT*>(lParam);
// Ignore injected events (includes our own replayed keys)
if (kb->flags & LLKHF_INJECTED)
goto forward;
// Skip processing if the foreground window is excluded
{
HWND fg = GetForegroundWindow();
if (fg && IsExcluded(fg))
goto forward;
}
bool isAltKey = (kb->vkCode == VK_MENU || kb->vkCode == VK_LMENU || kb->vkCode == VK_RMENU);
bool isWinKey = (kb->vkCode == VK_LWIN || kb->vkCode == VK_RWIN);
// ----- Win key modifier handling -----
if (isWinKey && g_modifierKey == GrabAndMoveModifier::Win)
{
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
{
if (IsSuppressedByGameMode())
goto forward;
// Suppress if Ctrl, Alt, Shift, or any regular key is already held
if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) ||
(GetAsyncKeyState(VK_MENU) & 0x8000) ||
(GetAsyncKeyState(VK_SHIFT) & 0x8000) ||
g_heldNonAltKeyCount > 0)
{
goto forward;
}
g_winPressed = true;
if (!g_winAbsorbed)
{
g_winAbsorbed = true;
g_dragConsumedAlt = false;
g_pendingModifierReplayed = false;
g_absorbedVk = kb->vkCode;
g_absorbedScanCode = kb->scanCode;
g_absorbedFlags = kb->flags;
}
return 1; // swallow Win keydown
}
else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
{
g_winPressed = false;
g_ignoreModifier = false;
g_activatedDuringHold = false;
// If a press was still pending (button held, no move), deliver it now.
FlushPendingClickOnModifierRelease();
bool wasDragging = g_dragging;
bool wasResizing = g_resizing;
EndInteraction(true, true);
if (g_winAbsorbed)
{
g_winAbsorbed = false;
if (wasDragging || wasResizing || g_dragConsumedAlt)
{
// Win was used for a drag/resize; swallow the keyup too
g_dragConsumedAlt = false;
return 1;
}
if (g_pendingModifierReplayed)
{
// The key-down was already replayed for a pending click; let the
// real key-up reach the target (falls through to goto forward).
g_pendingModifierReplayed = false;
}
else
{
// No drag happened; replay the keydown, THEN the keyup
ReplayAbsorbedModifier(true);
return 1; // swallow this keyup since the replay already sent one
}
}
}
goto forward; // let Win keyup pass through
}
if (g_modifierKey == GrabAndMoveModifier::Alt)
{
if (isAltKey)
{
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
{
if (IsSuppressedByGameMode())
{
goto forward;
}
// If any other modifier is already held (e.g. Ctrl in Ctrl+Alt+Del),
// or any regular key is currently held (e.g. Q held before Alt),
// don't intercept this Alt press let it reach the target application.
if ((GetAsyncKeyState(VK_CONTROL) & 0x8000) ||
(GetAsyncKeyState(VK_SHIFT) & 0x8000) ||
(GetAsyncKeyState(VK_LWIN) & 0x8000) ||
(GetAsyncKeyState(VK_RWIN) & 0x8000) ||
g_heldNonAltKeyCount > 0)
{
goto forward;
}
g_altPressed = true;
if (g_shouldAbsorbAlt)
{
if (!g_altAbsorbed)
{
g_altAbsorbed = true;
g_dragConsumedAlt = false;
g_pendingModifierReplayed = false;
g_absorbedVk = kb->vkCode;
g_absorbedScanCode = kb->scanCode;
g_absorbedFlags = kb->flags;
}
return 1; // swallow the Alt keydown
}
}
else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
{
g_altPressed = false;
g_ignoreModifier = false;
g_activatedDuringHold = false;
// If a press was still pending (button held, no move), deliver it now.
FlushPendingClickOnModifierRelease();
bool wasDragging = g_dragging;
bool wasResizing = g_resizing;
EndInteraction(true, true);
if (g_altAbsorbed)
{
g_altAbsorbed = false;
if (wasDragging || wasResizing || g_dragConsumedAlt)
{
// Alt was used for a drag/resize; swallow the keyup too
g_dragConsumedAlt = false;
return 1;
}
if (g_pendingModifierReplayed)
{
// The key-down was already replayed for a pending click; let
// the real key-up reach the target.
g_pendingModifierReplayed = false;
}
else
{
// No drag happened; replay the keydown, then let keyup through
ReplayAbsorbedModifier(false);
}
}
}
}
else
{
// Non-Alt key while Alt was absorbed without a drag: replay Alt first
if (g_altAbsorbed && !g_dragConsumedAlt && !g_pendingModifierReplayed)
{
g_altAbsorbed = false;
g_altPressed = false;
ReplayAbsorbedModifier(false);
}
}
}
// Non-Win key while Win was absorbed without a drag: replay Win first
if (g_winAbsorbed && !g_dragConsumedAlt && !isWinKey && !g_pendingModifierReplayed)
{
g_winAbsorbed = false;
g_winPressed = false;
ReplayAbsorbedModifier(false);
}
// Track held non-modifier keys (used to suppress GrabAndMove when the modifier
// is pressed while another key is already down). Applies to both Alt and Win modes.
// Use per-vkCode tracking to detect initial press vs auto-repeat, since
// KBDLLHOOKSTRUCT::flags does not carry the KF_REPEAT bit.
if (!isAltKey && !isWinKey)
{
BYTE vk = static_cast<BYTE>(kb->vkCode);
if (wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN)
{
if (!g_keyHeld[vk])
{
g_keyHeld[vk] = true;
++g_heldNonAltKeyCount;
}
}
else if (wParam == WM_KEYUP || wParam == WM_SYSKEYUP)
{
if (g_keyHeld[vk])
{
g_keyHeld[vk] = false;
if (g_heldNonAltKeyCount > 0)
--g_heldNonAltKeyCount;
}
}
}
}
forward:
return CallNextHookEx(g_hhkKeyboard, nCode, wParam, lParam);
}
static HWND ResolveTargetWindow(POINT pt)
{
auto normalizeTarget = [](HWND candidate) -> HWND {
if (!candidate)
{
return nullptr;
}
if (HWND root = GetAncestor(candidate, GA_ROOT))
{
candidate = root;
}
if (!candidate || candidate == g_hOverlay || candidate == g_hMsgWnd || IsSystemClass(candidate))
{
return nullptr;
}
return candidate;
};
// Remote sessions can produce unstable hit-testing for topmost windows.
// Prefer the actual foreground top-level window there.
if (GetSystemMetrics(SM_REMOTESESSION))
{
if (HWND foreground = normalizeTarget(GetForegroundWindow()))
{
return foreground;
}
}
HWND hwnd = WindowFromPoint(pt);
hwnd = normalizeTarget(hwnd);
if (!hwnd)
{
HWND foreground = GetForegroundWindow();
if (HWND normalizedForeground = normalizeTarget(foreground))
{
RECT foregroundRect{};
if (GetWindowRect(normalizedForeground, &foregroundRect) && PtInRect(&foregroundRect, pt))
{
hwnd = normalizedForeground;
}
}
}
return hwnd;
}
// Forward declarations for helpers called from MouseProc
static void HandleDragMove(POINT pt);
static void HandleDragResize(POINT pt);
static LRESULT CALLBACK MouseProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0)
{
auto* ms = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
// Ignore injected events to avoid feedback loops
if (ms->flags & LLMHF_INJECTED)
goto forward;
// Deliver-as-click bookkeeping: swallow the physical button-up that matches a
// press we already replayed to the target on modifier release.
if (wParam == WM_LBUTTONUP && g_swallowNextLButtonUp)
{
g_swallowNextLButtonUp = false;
return 1;
}
if (wParam == WM_RBUTTONUP && g_swallowNextRButtonUp)
{
g_swallowNextRButtonUp = false;
return 1;
}
// Recovery path: if a non-modifier click occurs while stale drag/resize/pending state exists, clear it.
if ((wParam == WM_LBUTTONDOWN || wParam == WM_RBUTTONDOWN) && !IsActivationModifierPressed())
{
g_pendingDrag = false;
g_pendingResize = false;
g_pendingTarget = nullptr;
g_pendingModifierReplayed = false;
EndInteraction(true, true);
}
if (!g_dragging && !g_resizing && g_hOverlay && IsWindowVisible(g_hOverlay))
{
HideOverlay();
}
// ----- Modifier + Left Button Down: arm a pending drag (deferred until move) -----
if (wParam == WM_LBUTTONDOWN && IsActivationModifierPressed())
{
if (IsSuppressedByGameMode())
{
TraceShortcutUse(false, GrabAndMoveShortcutAction::Move, L"game_mode");
goto forward;
}
POINT pt = ms->pt;
HWND hwnd = ResolveTargetWindow(pt);
if (hwnd)
{
// Don't drag excluded windows (desktop, taskbar, user-excluded apps)
if (IsExcluded(hwnd))
{
goto forward;
}
// Already committed to Grab and Move this hold: start dragging immediately.
if (g_activatedDuringHold)
{
BeginDrag(hwnd, pt);
return 1; // swallow the press; the drag is now active
}
// A press is already pending (the other button is still held): don't
// overwrite it. Forward this one so we never strand the already-swallowed
// button-down (which would leak an unmatched button-up to the target).
if (g_pendingDrag || g_pendingResize)
{
goto forward;
}
// Defer: absorb the button-down but do nothing until the cursor moves.
g_pendingDrag = true;
g_pendingResize = false;
g_pendingTarget = hwnd;
g_pendingDownPt = pt;
return 1; // swallow the press for now; replayed on release if no move
}
}
// ----- Promote a pending press to a real drag/resize once past the threshold -----
if (wParam == WM_MOUSEMOVE && (g_pendingDrag || g_pendingResize))
{
int ddx = ms->pt.x - g_pendingDownPt.x;
int ddy = ms->pt.y - g_pendingDownPt.y;
if (ddx < 0) ddx = -ddx;
if (ddy < 0) ddy = -ddy;
if (ddx >= GetSystemMetrics(SM_CXDRAG) || ddy >= GetSystemMetrics(SM_CYDRAG))
{
bool wasResize = g_pendingResize;
HWND target = g_pendingTarget;
POINT downPt = g_pendingDownPt;
g_pendingDrag = false;
g_pendingResize = false;
g_pendingTarget = nullptr;
if (wasResize)
{
BeginResize(target, downPt);
HandleDragResize(ms->pt);
}
else
{
BeginDrag(target, downPt);
HandleDragMove(ms->pt);
}
}
return 0; // keep the cursor moving while waiting for the threshold
}
// ----- Mouse Move while dragging -----
if (wParam == WM_MOUSEMOVE && g_dragging && g_dragTarget)
{
ULONGLONG now = QpcMs();
if (now - g_lastMoveTick >= THROTTLE_INTERVAL_MS)
{
g_lastMoveTick = now;
HandleDragMove(ms->pt);
}
return 0;
}
// ----- Left Button Up while a drag is pending (no move): deliver the click -----
if (wParam == WM_LBUTTONUP && g_pendingDrag)
{
g_ignoreModifier = true;
g_pendingDrag = false;
g_pendingTarget = nullptr;
ReplayPendingClick(false);
return 1; // swallow the physical up; the injected click replaces it
}
// ----- Left Button Up: end drag -----
if (wParam == WM_LBUTTONUP && g_dragging)
{
// Flush final position (may have been throttled)
POINT pt = ms->pt;
int dx = pt.x - g_dragStart.x;
int dy = pt.y - g_dragStart.y;
int newX = g_dragWndRect.left + dx;
int newY = g_dragWndRect.top + dy;
int w = g_dragWndRect.right - g_dragWndRect.left;
int h = g_dragWndRect.bottom - g_dragWndRect.top;
SetWindowPos(g_dragTarget, nullptr, newX, newY, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
EndInteraction(true, false);
return 1; // swallow the release
}
// ----- Modifier + Right Button Down: arm a pending resize (deferred until move) -----
if (wParam == WM_RBUTTONDOWN && g_useAltResize && IsActivationModifierPressed())
{
if (IsSuppressedByGameMode())
{
TraceShortcutUse(false, GrabAndMoveShortcutAction::Resize, L"game_mode");
goto forward;
}
POINT pt = ms->pt;
HWND hwnd = ResolveTargetWindow(pt);
if (hwnd)
{
if (IsExcluded(hwnd))
{
goto forward;
}
// Only allow resize if the window style permits resizing
if (!(GetWindowLongW(hwnd, GWL_STYLE) & WS_THICKFRAME))
{
goto forward;
}
// Already committed to Grab and Move this hold: start resizing immediately.
if (g_activatedDuringHold)
{
BeginResize(hwnd, pt);
return 1; // swallow the press; the resize is now active
}
// A press is already pending (the other button is still held): don't
// overwrite it. Forward this one so we never strand the already-swallowed
// button-down (which would leak an unmatched button-up to the target).
if (g_pendingDrag || g_pendingResize)
{
goto forward;
}
// Defer: absorb the button-down but do nothing until the cursor moves.
g_pendingResize = true;
g_pendingDrag = false;
g_pendingTarget = hwnd;
g_pendingDownPt = pt;
return 1; // swallow the press for now; replayed on release if no move
}
}
// ----- Mouse Move while resizing -----
if (wParam == WM_MOUSEMOVE && g_resizing && g_resizeTarget)
{
ULONGLONG now = QpcMs();
if (now - g_lastMoveTick >= THROTTLE_INTERVAL_MS)
{
g_lastMoveTick = now;
HandleDragResize(ms->pt);
}
return 0;
}
// ----- Right Button Up while a resize is pending (no move): deliver the click -----
if (wParam == WM_RBUTTONUP && g_pendingResize)
{
g_ignoreModifier = true;
g_pendingResize = false;
g_pendingTarget = nullptr;
ReplayPendingClick(true);
return 1; // swallow the physical up; the injected click replaces it
}
// ----- Right Button Up: end resize -----
if (wParam == WM_RBUTTONUP && g_resizing)
{
// Flush final size (may have been throttled)
RECT nr = g_resizeWndRect;
int w = nr.right - nr.left;
int h = nr.bottom - nr.top;
SetWindowPos(g_resizeTarget, nullptr, nr.left, nr.top, w, h, SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
EndInteraction(false, true);
return 1; // swallow the right button release
}
}
forward:
return CallNextHookEx(g_hhkMouse, nCode, wParam, lParam);
}
// ---------------------------------------------------------------------------
// Drag / resize move helpers called directly from the LL mouse hook.
// DeferWindowPos batches the target + overlay into one DWM composition pass.
// ---------------------------------------------------------------------------
static void HandleDragMove(POINT pt)
{
if (!g_dragging || !g_dragTarget)
return;
// On the first move, restore maximized windows
if (g_dragFirstMove)
{
g_dragFirstMove = false;
if (IsZoomed(g_dragTarget))
{
RECT maxRect;
GetWindowRect(g_dragTarget, &maxRect);
int maxW = maxRect.right - maxRect.left;
int maxH = maxRect.bottom - maxRect.top;
ShowWindow(g_dragTarget, SW_RESTORE);
GetWindowRect(g_dragTarget, &g_dragWndRect);
int restoredW = g_dragWndRect.right - g_dragWndRect.left;
int restoredH = g_dragWndRect.bottom - g_dragWndRect.top;
// Preserve the relative grab position in both axes so the cursor stays
// at the same proportional spot within the restored window.
float ratioL = (maxW > 0) ? static_cast<float>(g_dragStart.x - maxRect.left) / maxW : 0.5f;
float ratioT = (maxH > 0) ? static_cast<float>(g_dragStart.y - maxRect.top) / maxH : 0.5f;
int newX = g_dragStart.x - static_cast<int>(restoredW * ratioL);
int newY = g_dragStart.y - static_cast<int>(restoredH * ratioT);
SetWindowPos(g_dragTarget, nullptr, newX, newY, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
g_dragStart = pt;
g_dragWndRect = {newX, newY, newX + restoredW, newY + restoredH};
// Corner radius / invisible-border margins differ once restored.
PrepareOverlayMetrics(g_dragTarget);
}
}
int dx = pt.x - g_dragStart.x;
int dy = pt.y - g_dragStart.y;
int newX = g_dragWndRect.left + dx;
int newY = g_dragWndRect.top + dy;
int w = g_dragWndRect.right - g_dragWndRect.left;
int h = g_dragWndRect.bottom - g_dragWndRect.top;
// Move target + overlay (separate SetWindowPos DeferWindowPos doesn't
// work reliably for cross-process target windows)
SetWindowPos(g_dragTarget, nullptr, newX, newY, 0, 0,
SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
RepositionOverlay(newX, newY, w, h);
}
static void HandleDragResize(POINT pt)
{
if (!g_resizing || !g_resizeTarget)
return;
// On the first resize, restore maximized windows
if (g_resizeFirstMove)
{
g_resizeFirstMove = false;
if (IsZoomed(g_resizeTarget))
{
RECT maxRect = g_resizeWndRect;
int maxW = maxRect.right - maxRect.left;
int maxH = maxRect.bottom - maxRect.top;
float ratioL = (maxW > 0) ? static_cast<float>(pt.x - maxRect.left) / maxW : 0.5f;
float ratioT = (maxH > 0) ? static_cast<float>(pt.y - maxRect.top) / maxH : 0.5f;
ShowWindow(g_resizeTarget, SW_RESTORE);
GetWindowRect(g_resizeTarget, &g_resizeWndRect);
int newW = g_resizeWndRect.right - g_resizeWndRect.left;
int newH = g_resizeWndRect.bottom - g_resizeWndRect.top;
int newLeft = pt.x - static_cast<int>(ratioL * newW);
int newTop = pt.y - static_cast<int>(ratioT * newH);
SetWindowPos(g_resizeTarget, nullptr, newLeft, newTop, newW, newH,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
g_resizeWndRect = {newLeft, newTop, newLeft + newW, newTop + newH};
// Corner radius / invisible-border margins differ once restored.
PrepareOverlayMetrics(g_resizeTarget);
g_resizeLast = pt;
g_currentHandle = GetClosestHandle(pt, g_resizeWndRect);
}
}
int dx = pt.x - g_resizeLast.x;
int dy = pt.y - g_resizeLast.y;
// Re-evaluate closest handle as the cursor moves
ResizeHandle newHandle = GetClosestHandle(pt, g_resizeWndRect);
if (newHandle != g_currentHandle)
{
g_currentHandle = newHandle;
HCURSOR hCur = CursorForHandle(g_currentHandle);
SetClassLongPtrW(g_hOverlay, GCLP_HCURSOR, reinterpret_cast<LONG_PTR>(hCur));
SetCursor(hCur);
}
// Apply delta to the edges indicated by the current handle
RECT nr = g_resizeWndRect;
switch (g_currentHandle)
{
case RESIZE_TOP_LEFT: nr.left += dx; nr.top += dy; break;
case RESIZE_TOP: nr.top += dy; break;
case RESIZE_TOP_RIGHT: nr.right += dx; nr.top += dy; break;
case RESIZE_RIGHT: nr.right += dx; break;
case RESIZE_BOTTOM_RIGHT: nr.right += dx; nr.bottom += dy; break;
case RESIZE_BOTTOM: nr.bottom += dy; break;
case RESIZE_BOTTOM_LEFT: nr.left += dx; nr.bottom += dy; break;
case RESIZE_LEFT: nr.left += dx; break;
default: break;
}
// Enforce minimum window size
bool leftMoving = (g_currentHandle == RESIZE_TOP_LEFT || g_currentHandle == RESIZE_BOTTOM_LEFT || g_currentHandle == RESIZE_LEFT);
bool topMoving = (g_currentHandle == RESIZE_TOP_LEFT || g_currentHandle == RESIZE_TOP || g_currentHandle == RESIZE_TOP_RIGHT);
if (nr.right - nr.left < MIN_WINDOW_WIDTH)
{
if (leftMoving) nr.left = nr.right - MIN_WINDOW_WIDTH;
else nr.right = nr.left + MIN_WINDOW_WIDTH;
}
if (nr.bottom - nr.top < MIN_WINDOW_HEIGHT)
{
if (topMoving) nr.top = nr.bottom - MIN_WINDOW_HEIGHT;
else nr.bottom = nr.top + MIN_WINDOW_HEIGHT;
}
g_resizeWndRect = nr;
g_resizeLast = pt;
int w = nr.right - nr.left;
int h = nr.bottom - nr.top;
// Move target + overlay (separate SetWindowPos DeferWindowPos doesn't
// work reliably for cross-process target windows)
SetWindowPos(g_resizeTarget, nullptr, nr.left, nr.top, w, h,
SWP_NOZORDER | SWP_NOACTIVATE | SWP_ASYNCWINDOWPOS);
RepositionOverlay(nr.left, nr.top, w, h);
}
static LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_INVALIDATE_EXCLUDED_CACHE:
g_excludedCache.clear();
return 0;
case WM_TRAY_ICON:
if (LOWORD(lParam) == WM_RBUTTONUP)
{
ShowTrayMenu(hwnd);
}
return 0;
case WM_COMMAND:
if (LOWORD(wParam) == IDM_EXIT)
{
PostQuitMessage(0);
}
return 0;
case WM_DESTROY:
RemoveTrayIcon();
PostQuitMessage(0);
return 0;
}
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
// ---------------------------------------------------------------------------
// Entry point
// ---------------------------------------------------------------------------
const std::wstring instanceMutexName = L"Local\\PowerToys_GrabAndMove_InstanceMutex";
int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE, LPWSTR lpCmdLine, int)
{
g_hInstance = hInstance;
TraceLoggingRegister(g_hProvider);
// Prevent multiple instances
auto mutex = CreateMutexW(nullptr, TRUE, instanceMutexName.c_str());
if (GetLastError() == ERROR_ALREADY_EXISTS)
{
TraceLoggingUnregister(g_hProvider);
return 1;
}
// Require runner PID on the command line; refuse to run standalone
std::wstring pid = std::wstring(lpCmdLine);
if (pid.empty())
{
MessageBoxW(nullptr, L"GrabAndMove can't run as a standalone. Start it from PowerToys.", L"GrabAndMove", MB_ICONERROR);
TraceLoggingUnregister(g_hProvider);
return 1;
}
auto mainThreadId = GetCurrentThreadId();
ProcessWaiter::OnProcessTerminate(pid, [mainThreadId](int err) {
if (err != ERROR_SUCCESS)
{
// Failed to wait for parent process exit
}
PostThreadMessage(mainThreadId, WM_QUIT, 0, 0);
});
// Load initial settings from JSON before anything else
LoadSettingsFromFile();
// Open the named event for settings reload notifications
g_hReloadSettingsEvent = CreateEventW(nullptr, FALSE, FALSE, GRABANDMOVE_REFRESH_SETTINGS_EVENT);
// Open the named event for exit signal from the module interface
g_hExitEvent = CreateEventW(nullptr, FALSE, FALSE, GRABANDMOVE_EXIT_EVENT);
if (g_hReloadSettingsEvent)
{
g_settingsThread = std::thread(SettingsWatcherThread, mainThreadId);
}
// Ensure common controls are initialised (for Shell_NotifyIcon)
INITCOMMONCONTROLSEX commonControls = { sizeof(commonControls), ICC_STANDARD_CLASSES };
InitCommonControlsEx(&commonControls);
// Initialise GDI+ for antialiased overlay border rendering
Gdiplus::GdiplusStartupInput gdiplusStartupInput;
Gdiplus::GdiplusStartup(&g_gdiplusToken, &gdiplusStartupInput, nullptr);
// Register a message-only window class
WNDCLASSEXW wc = {};
wc.cbSize = sizeof(wc);
wc.lpfnWndProc = WndProc;
wc.hInstance = hInstance;
wc.lpszClassName = CLASS_NAME;
if (!RegisterClassExW(&wc))
{
TraceLoggingUnregister(g_hProvider);
return 1;
}
// Register the overlay window class (layered per-pixel-alpha surface, ARROW cursor)
WNDCLASSEXW overlayWindowClass = {};
overlayWindowClass.cbSize = sizeof(overlayWindowClass);
overlayWindowClass.lpfnWndProc = DefWindowProcW;
overlayWindowClass.hInstance = hInstance;
overlayWindowClass.hCursor = LoadCursorW(nullptr, IDC_ARROW);
overlayWindowClass.hbrBackground = nullptr; // per-pixel alpha via UpdateLayeredWindow
overlayWindowClass.lpszClassName = OVERLAY_CLASS_NAME;
if (!RegisterClassExW(&overlayWindowClass))
{
TraceLoggingUnregister(g_hProvider);
return 1;
}
// Create a message-only window (invisible)
g_hMsgWnd = CreateWindowExW(0, CLASS_NAME, APP_TITLE, 0, 0, 0, 0, 0, HWND_MESSAGE, nullptr, hInstance, nullptr);
if (!g_hMsgWnd)
{
TraceLoggingUnregister(g_hProvider);
return 1;
}
// Pre-load system cursors (fix #6 avoid LoadCursorW in hot path)
g_curSizeAll = LoadCursorW(nullptr, IDC_SIZEALL);
g_curSizeNWSE = LoadCursorW(nullptr, IDC_SIZENWSE);
g_curSizeNESW = LoadCursorW(nullptr, IDC_SIZENESW);
g_curSizeWE = LoadCursorW(nullptr, IDC_SIZEWE);
g_curSizeNS = LoadCursorW(nullptr, IDC_SIZENS);
// Install global low-level hooks
g_hhkKeyboard = SetWindowsHookExW(WH_KEYBOARD_LL, KeyboardProc, hInstance, 0);
g_hhkMouse = SetWindowsHookExW(WH_MOUSE_LL, MouseProc, hInstance, 0);
// Detect foreground switches to elevated processes (where key-up events stop arriving)
g_hWinEventHook = SetWinEventHook(
EVENT_SYSTEM_FOREGROUND, EVENT_SYSTEM_FOREGROUND,
nullptr, WinEventProc, 0, 0, WINEVENT_OUTOFCONTEXT);
if (!g_hhkKeyboard || !g_hhkMouse)
{
MessageBoxW(
nullptr,
L"Failed to install global hooks.\n"
L"Try running as Administrator.",
APP_TITLE,
MB_ICONERROR | MB_OK);
if (g_hhkKeyboard)
UnhookWindowsHookEx(g_hhkKeyboard);
if (g_hhkMouse)
UnhookWindowsHookEx(g_hhkMouse);
TraceLoggingUnregister(g_hProvider);
return 1;
}
// This is a good place to add system tray icon with AddTrayIcon(g_hMsgWnd);
// Message loop required for low-level hooks to function
MSG msg;
while (GetMessageW(&msg, nullptr, 0, 0) > 0)
{
TranslateMessage(&msg);
DispatchMessageW(&msg);
}
// Cleanup
g_running = false;
if (g_hReloadSettingsEvent)
{
SetEvent(g_hReloadSettingsEvent); // wake the thread so it exits
}
if (g_settingsThread.joinable())
{
g_settingsThread.join();
}
if (g_hReloadSettingsEvent)
{
CloseHandle(g_hReloadSettingsEvent);
}
if (g_hExitEvent)
{
CloseHandle(g_hExitEvent);
}
UnhookWindowsHookEx(g_hhkKeyboard);
UnhookWindowsHookEx(g_hhkMouse);
if (g_hWinEventHook)
{
UnhookWinEvent(g_hWinEventHook);
}
// Destroy persistent overlay window (fix #9)
if (g_hOverlay)
{
DestroyWindow(g_hOverlay);
g_hOverlay = nullptr;
}
RemoveTrayIcon();
if (g_gdiplusToken)
{
Gdiplus::GdiplusShutdown(g_gdiplusToken);
g_gdiplusToken = 0;
}
TraceLoggingUnregister(g_hProvider);
return static_cast<int>(msg.wParam);
}