Files
PowerToys/src/modules/MouseUtils/CursorWrap/dllmain.cpp
Thanh Nguyen 2be4c4eb46 Fix CursorWrap "Automatically activate on utility startup" setting not persisting (#45210)
## Summary of the Pull Request

Fixes #45185 - CursorWrap "Automatically activate on utility startup"
setting cannot be disabled, and prevents spurious activation on startup.

## PR Checklist

- [x] Closes: #45185
- [x] **Communication:** Issue was reported by community; fix follows
established patterns from MousePointerCrosshairs
- [x] **Tests:** Manual validation performed by contributor (video
available)
- [x] **Localization:** No new user-facing strings added
- [ ] **Dev docs:** N/A - bug fix only
- [ ] **New binaries:** N/A - no new binaries
- [ ] **Documentation updated:** N/A - bug fix only

## Detailed Description of the Pull Request / Additional comments

### Problem

Users reported that disabling the "Automatically activate on utility
startup" setting for CursorWrap does not work - the mouse hook always
starts automatically regardless of the setting value.

### Root Causes

1. **`dllmain.cpp` `enable()` method**: `StartMouseHook()` was always
called unconditionally, ignoring `m_autoActivate`.
2. **`MouseUtilsViewModel.cs` `IsCursorWrapEnabled` setter**: enabling
CursorWrap forced `AutoActivate = true`, overriding the user's
preference.
3. **Startup edge case**: the trigger event could remain signaled from a
previous session, immediately toggling CursorWrap on startup even when
AutoActivate is off.

### Solution

1. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: only start the
mouse hook if `m_autoActivate` is true.
2. **`src/settings-ui/Settings.UI/ViewModels/MouseUtilsViewModel.cs`**:
remove the line that forced `AutoActivate = true` when enabling
CursorWrap.
3. **`src/modules/MouseUtils/CursorWrap/dllmain.cpp`**: reset the
trigger event on enable to avoid immediate activation on startup.

### Pattern Reference

This fix follows the same pattern used by **MousePointerCrosshairs**
module which has a similar `AutoActivate` setting that works correctly.

## Validation Steps Performed

### Build

- `tools\build\build.ps1 -Platform x64 -Configuration Debug`

### Manual validation (contributor)

#### Test Case 1: AutoActivate = false (should NOT auto-start mouse
hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Disable** "Automatically activate on utility startup"
4. Close PowerToys completely (right-click tray icon → Exit)
5. Restart PowerToys
6. **Expected Result**: CursorWrap module is loaded but mouse hook is
NOT active - cursor does NOT wrap at screen edges
7. Press activation hotkey (default: `Win+Alt+U`)
8. **Expected Result**: Mouse hook activates, cursor now wraps at screen
edges
9. **Actual Result**:  Works as expected

#### Test Case 2: AutoActivate = true (should auto-start mouse hook)

1. Open PowerToys Settings → Mouse Utilities → Cursor Wrap
2. Enable Cursor Wrap
3. **Enable** "Automatically activate on utility startup"
4. Close PowerToys completely
5. Restart PowerToys
6. **Expected Result**: Mouse hook is immediately active, cursor wraps
at screen edges without pressing hotkey
7. **Actual Result**:  Works as expected

#### Test Case 3: Setting persistence after restart

1. Set AutoActivate = false, restart PowerToys
2. Open Settings and verify AutoActivate is still false
3. Set AutoActivate = true, restart PowerToys
4. Open Settings and verify AutoActivate is still true
5. **Actual Result**:  Setting persists correctly

#### Test Case 4: Hotkey toggle works correctly

1. With AutoActivate = false, restart PowerToys
2. Press hotkey → cursor should start wrapping
3. Press hotkey again → cursor should stop wrapping
4. **Actual Result**:  Hotkey toggle works correctly

---

**Note**: Video demonstration available from contributor.
2026-02-05 19:58:49 +08:00

701 lines
24 KiB
C++

// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
#include "pch.h"
#include "../../../interface/powertoy_module_interface.h"
#include "../../../common/SettingsAPI/settings_objects.h"
#include "trace.h"
#include "../../../common/utils/process_path.h"
#include "../../../common/utils/resources.h"
#include "../../../common/logger/logger.h"
#include "../../../common/utils/logger_helper.h"
#include "../../../common/interop/shared_constants.h"
#include <atomic>
#include <thread>
#include <vector>
#include <map>
#include <string>
#include <algorithm>
#include <windows.h>
#include <dbt.h>
#include <sstream>
#include "resource.h"
#include "CursorWrapCore.h"
// Disable C26451 arithmetic overflow warning for this file since the operations are safe in this context
#pragma warning(disable: 26451)
extern "C" IMAGE_DOS_HEADER __ImageBase;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
Trace::RegisterProvider();
break;
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
break;
case DLL_PROCESS_DETACH:
Trace::UnregisterProvider();
break;
}
return TRUE;
}
// Non-Localizable strings
namespace
{
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_DISABLE_WRAP_DURING_DRAG[] = L"disable_wrap_during_drag";
const wchar_t JSON_KEY_WRAP_MODE[] = L"wrap_mode";
const wchar_t JSON_KEY_DISABLE_ON_SINGLE_MONITOR[] = L"disable_cursor_wrap_on_single_monitor";
}
// The PowerToy name that will be shown in the settings.
const static wchar_t* MODULE_NAME = L"CursorWrap";
// Add a description that will we shown in the module settings page.
const static wchar_t* MODULE_DESC = L"<no description>";
// Monitor device interface GUID for RegisterDeviceNotification
// {e6f07b5f-ee97-4a90-b076-33f57bf4eaa7}
static const GUID GUID_DEVINTERFACE_MONITOR =
{ 0xe6f07b5f, 0xee97, 0x4a90, { 0xb0, 0x76, 0x33, 0xf5, 0x7b, 0xf4, 0xea, 0xa7 } };
// Forward declaration
class CursorWrap;
// Global instance pointer for the mouse hook
static CursorWrap* g_cursorWrapInstance = nullptr;
// Implement the PowerToy Module Interface and all the required methods.
class CursorWrap : public PowertoyModuleIface
{
private:
// The PowerToy state.
bool m_enabled = false;
bool m_autoActivate = false;
bool m_disableWrapDuringDrag = true; // Default to true to prevent wrap during drag
bool m_disableOnSingleMonitor = false; // Default to false
int m_wrapMode = 0; // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
// Mouse hook
HHOOK m_mouseHook = nullptr;
std::atomic<bool> m_hookActive{ false };
// Core wrapping engine (edge-based polygon model)
CursorWrapCore m_core;
// Hotkey
Hotkey m_activationHotkey{};
// Event-driven trigger support (for CmdPal/automation)
HANDLE m_triggerEventHandle = nullptr;
HANDLE m_terminateEventHandle = nullptr;
std::thread m_eventThread;
std::atomic_bool m_listening{ false };
// Display change notification
HWND m_messageWindow = nullptr;
HDEVNOTIFY m_deviceNotify = nullptr;
static constexpr UINT_PTR TIMER_UPDATE_MONITORS = 1;
static constexpr UINT DEBOUNCE_DELAY_MS = 500;
public:
// Constructor
CursorWrap()
{
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::cursorWrapLoggerName);
init_settings();
m_core.UpdateMonitorInfo();
g_cursorWrapInstance = this; // Set global instance pointer
};
// Destroy the powertoy and free memory
virtual void destroy() override
{
// Ensure hooks/threads/handles are torn down before deletion
disable();
g_cursorWrapInstance = nullptr; // Clear global instance pointer
delete this;
}
// Return the localized display name of the powertoy
virtual const wchar_t* get_name() override
{
return MODULE_NAME;
}
// Return the non localized key of the powertoy, this will be cached by the runner
virtual const wchar_t* get_key() override
{
return MODULE_NAME;
}
// Return the configured status for the gpo policy for the module
virtual powertoys_gpo::gpo_rule_configured_t gpo_policy_enabled_configuration() override
{
return powertoys_gpo::getConfiguredCursorWrapEnabledValue();
}
// Return JSON with the configuration options.
virtual bool get_config(wchar_t* buffer, int* buffer_size) override
{
HINSTANCE hinstance = reinterpret_cast<HINSTANCE>(&__ImageBase);
PowerToysSettings::Settings settings(hinstance, get_name());
settings.set_description(IDS_CURSORWRAP_NAME);
settings.set_icon_key(L"pt-cursor-wrap");
// Create HotkeyObject from the Hotkey struct for the settings
auto hotkey_object = PowerToysSettings::HotkeyObject::from_settings(
m_activationHotkey.win,
m_activationHotkey.ctrl,
m_activationHotkey.alt,
m_activationHotkey.shift,
m_activationHotkey.key);
settings.add_hotkey(JSON_KEY_ACTIVATION_SHORTCUT, IDS_CURSORWRAP_NAME, hotkey_object);
settings.add_bool_toggle(JSON_KEY_AUTO_ACTIVATE, IDS_CURSORWRAP_NAME, m_autoActivate);
settings.add_bool_toggle(JSON_KEY_DISABLE_WRAP_DURING_DRAG, IDS_CURSORWRAP_NAME, m_disableWrapDuringDrag);
return settings.serialize_to_buffer(buffer, buffer_size);
}
// Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors.
virtual void call_custom_action(const wchar_t* /*action*/) override {}
// Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override
{
try
{
// Parse the input JSON string.
PowerToysSettings::PowerToyValues values =
PowerToysSettings::PowerToyValues::from_json_string(config, get_key());
parse_settings(values);
}
catch (std::exception&)
{
Logger::error("Invalid json when trying to parse CursorWrap settings json.");
}
}
// Enable the powertoy
virtual void enable()
{
m_enabled = true;
Trace::EnableCursorWrap(true);
// Start listening for external trigger event so we can invoke the same logic as the activation hotkey.
m_triggerEventHandle = CreateEventW(nullptr, false, false, CommonSharedConstants::CURSOR_WRAP_TRIGGER_EVENT);
m_terminateEventHandle = CreateEventW(nullptr, false, false, nullptr);
if (m_triggerEventHandle)
{
ResetEvent(m_triggerEventHandle);
}
if (m_triggerEventHandle && m_terminateEventHandle)
{
m_listening = true;
m_eventThread = std::thread([this]() {
HANDLE handles[2] = { m_triggerEventHandle, m_terminateEventHandle };
// WH_MOUSE_LL callbacks are delivered to the thread that installed the hook.
// Ensure this thread has a message queue and pumps messages while the hook is active.
MSG msg;
PeekMessage(&msg, nullptr, WM_USER, WM_USER, PM_NOREMOVE);
// Create message window for display change notifications
RegisterForDisplayChanges();
// Only start the mouse hook automatically if auto-activate is enabled
if (m_autoActivate)
{
StartMouseHook();
Logger::info("CursorWrap enabled - mouse hook started (auto-activate on)");
}
else
{
Logger::info("CursorWrap enabled - waiting for activation hotkey (auto-activate off)");
}
while (m_listening)
{
auto res = MsgWaitForMultipleObjects(2, handles, false, INFINITE, QS_ALLINPUT);
if (!m_listening)
{
break;
}
if (res == WAIT_OBJECT_0)
{
ToggleMouseHook();
}
else if (res == WAIT_OBJECT_0 + 1)
{
break;
}
else
{
while (PeekMessage(&msg, nullptr, 0, 0, PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
}
// Cleanup display change notifications
UnregisterDisplayChanges();
StopMouseHook();
Logger::info("CursorWrap event listener stopped");
});
}
}
// Disable the powertoy
virtual void disable()
{
m_enabled = false;
Trace::EnableCursorWrap(false);
m_listening = false;
if (m_terminateEventHandle)
{
SetEvent(m_terminateEventHandle);
}
if (m_eventThread.joinable())
{
m_eventThread.join();
}
if (m_triggerEventHandle)
{
CloseHandle(m_triggerEventHandle);
m_triggerEventHandle = nullptr;
}
if (m_terminateEventHandle)
{
CloseHandle(m_terminateEventHandle);
m_terminateEventHandle = nullptr;
}
}
// Returns if the powertoys is enabled
virtual bool is_enabled() override
{
return m_enabled;
}
// Returns whether the PowerToys should be enabled by default
virtual bool is_enabled_by_default() const override
{
return false;
}
// Legacy hotkey support
virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override
{
if (buffer && buffer_size >= 1)
{
buffer[0] = m_activationHotkey;
}
return 1;
}
virtual bool on_hotkey(size_t hotkeyId) override
{
if (!m_enabled || hotkeyId != 0)
{
return false;
}
// Toggle on the thread that owns the WH_MOUSE_LL hook (the event listener thread).
if (m_triggerEventHandle)
{
return SetEvent(m_triggerEventHandle);
}
return false;
}
// Called when display configuration changes - update monitor topology
void OnDisplayChange()
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] Display configuration changed, updating monitor topology\n");
#endif
Logger::info("Display configuration changed, updating monitor topology");
m_core.UpdateMonitorInfo();
}
private:
void ToggleMouseHook()
{
// Toggle cursor wrapping.
if (m_hookActive)
{
StopMouseHook();
}
else
{
StartMouseHook();
}
}
// Load the settings file.
void init_settings()
{
try
{
// Load and parse the settings file for this PowerToy.
PowerToysSettings::PowerToyValues settings =
PowerToysSettings::PowerToyValues::load_from_settings_file(CursorWrap::get_key());
parse_settings(settings);
}
catch (std::exception&)
{
Logger::error("Invalid json when trying to load the CursorWrap settings json from file.");
}
}
void parse_settings(PowerToysSettings::PowerToyValues& settings)
{
auto settingsObject = settings.get_raw_json();
if (settingsObject.GetView().Size())
{
try
{
// Parse activation HotKey
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_activationHotkey.win = hotkey.win_pressed();
m_activationHotkey.ctrl = hotkey.ctrl_pressed();
m_activationHotkey.shift = hotkey.shift_pressed();
m_activationHotkey.alt = hotkey.alt_pressed();
m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap activation shortcut");
}
try
{
// Parse auto activate
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_AUTO_ACTIVATE);
m_autoActivate = jsonPropertiesObject.GetNamedBoolean(JSON_KEY_VALUE);
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap auto activate from settings. Will use default value");
}
try
{
// Parse disable wrap during drag
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_DISABLE_WRAP_DURING_DRAG))
{
auto disableDragObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_WRAP_DURING_DRAG);
m_disableWrapDuringDrag = disableDragObject.GetNamedBoolean(JSON_KEY_VALUE);
}
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap disable wrap during drag from settings. Will use default value (true)");
}
try
{
// Parse wrap mode
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_WRAP_MODE))
{
auto wrapModeObject = propertiesObject.GetNamedObject(JSON_KEY_WRAP_MODE);
m_wrapMode = static_cast<int>(wrapModeObject.GetNamedNumber(JSON_KEY_VALUE));
}
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)");
}
try
{
// Parse disable on single monitor
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
if (propertiesObject.HasKey(JSON_KEY_DISABLE_ON_SINGLE_MONITOR))
{
auto disableOnSingleMonitorObject = propertiesObject.GetNamedObject(JSON_KEY_DISABLE_ON_SINGLE_MONITOR);
m_disableOnSingleMonitor = disableOnSingleMonitorObject.GetNamedBoolean(JSON_KEY_VALUE);
}
}
catch (...)
{
Logger::warn("Failed to initialize CursorWrap disable on single monitor from settings. Will use default value (false)");
}
}
else
{
Logger::info("CursorWrap settings are empty");
}
// Set default hotkey if not configured
if (m_activationHotkey.key == 0)
{
m_activationHotkey.win = true;
m_activationHotkey.alt = true;
m_activationHotkey.ctrl = false;
m_activationHotkey.shift = false;
m_activationHotkey.key = 'U'; // Win+Alt+U
}
}
void StartMouseHook()
{
if (m_mouseHook || m_hookActive)
{
Logger::info("CursorWrap mouse hook already active");
return;
}
// Refresh monitor info before starting hook
m_core.UpdateMonitorInfo();
m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, GetModuleHandle(nullptr), 0);
if (m_mouseHook)
{
m_hookActive = true;
Logger::info("CursorWrap mouse hook started successfully");
#ifdef _DEBUG
Logger::info(L"CursorWrap DEBUG: Hook installed");
#endif
}
else
{
DWORD error = GetLastError();
Logger::error(L"Failed to install CursorWrap mouse hook, error: {}", error);
}
}
void StopMouseHook()
{
if (m_mouseHook)
{
UnhookWindowsHookEx(m_mouseHook);
m_mouseHook = nullptr;
m_hookActive = false;
Logger::info("CursorWrap mouse hook stopped");
#ifdef _DEBUG
Logger::info("CursorWrap DEBUG: Mouse hook stopped");
#endif
}
}
void RegisterForDisplayChanges()
{
if (m_messageWindow)
{
return; // Already registered
}
// Create a hidden top-level window to receive broadcast messages
// NOTE: Message-only windows (HWND_MESSAGE parent) do NOT receive
// WM_DISPLAYCHANGE, WM_SETTINGCHANGE, or WM_DEVICECHANGE broadcasts.
// We must use a real (hidden) top-level window instead.
WNDCLASSEXW wc = { sizeof(WNDCLASSEXW) };
wc.lpfnWndProc = MessageWindowProc;
wc.hInstance = GetModuleHandle(nullptr);
wc.lpszClassName = L"CursorWrapDisplayChangeWindow";
RegisterClassExW(&wc);
// Create a hidden top-level window (not message-only)
// WS_EX_TOOLWINDOW prevents taskbar button, WS_POPUP with no size makes it invisible
m_messageWindow = CreateWindowExW(
WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE,
L"CursorWrapDisplayChangeWindow",
nullptr,
WS_POPUP, // Minimal window style
0, 0, 0, 0, // Zero size = invisible
nullptr, // No parent - top-level window to receive broadcasts
nullptr,
GetModuleHandle(nullptr),
nullptr);
if (m_messageWindow)
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] Registered for display change notifications\n");
#endif
Logger::info("Registered for display change notifications");
// Register for device notifications (monitor hardware add/remove)
DEV_BROADCAST_DEVICEINTERFACE filter = {};
filter.dbcc_size = sizeof(DEV_BROADCAST_DEVICEINTERFACE);
filter.dbcc_devicetype = DBT_DEVTYP_DEVICEINTERFACE;
filter.dbcc_classguid = GUID_DEVINTERFACE_MONITOR;
m_deviceNotify = RegisterDeviceNotificationW(
m_messageWindow,
&filter,
DEVICE_NOTIFY_WINDOW_HANDLE);
if (m_deviceNotify)
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] Registered for device notifications (monitor hardware changes)\n");
#endif
Logger::info("Registered for device notifications (monitor hardware changes)");
}
else
{
DWORD error = GetLastError();
#ifdef _DEBUG
std::wostringstream oss;
oss << L"[CursorWrap] Failed to register device notifications. Error: " << error << L"\n";
OutputDebugStringW(oss.str().c_str());
#endif
Logger::warn("Failed to register device notifications. Error: {}", error);
}
}
else
{
DWORD error = GetLastError();
Logger::error(L"Failed to create message window for display changes, error: {}", error);
}
}
void UnregisterDisplayChanges()
{
if (m_deviceNotify)
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] Unregistering device notifications...\n");
#endif
UnregisterDeviceNotification(m_deviceNotify);
m_deviceNotify = nullptr;
Logger::info("Unregistered device notifications");
}
if (m_messageWindow)
{
KillTimer(m_messageWindow, TIMER_UPDATE_MONITORS);
DestroyWindow(m_messageWindow);
m_messageWindow = nullptr;
UnregisterClassW(L"CursorWrapDisplayChangeWindow", GetModuleHandle(nullptr));
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] Unregistered display change notifications\n");
#endif
Logger::info("Unregistered display change notifications");
}
}
static LRESULT CALLBACK MessageWindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
if (!g_cursorWrapInstance)
{
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
switch (msg)
{
case WM_DISPLAYCHANGE:
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] WM_DISPLAYCHANGE received - monitor resolution/DPI changed\n");
#endif
Logger::info("WM_DISPLAYCHANGE received - resolution/DPI changed");
// Debounce: Wait for multiple changes to settle
KillTimer(hwnd, TIMER_UPDATE_MONITORS);
SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr);
break;
case WM_SETTINGCHANGE:
if (wParam == SPI_SETWORKAREA)
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] WM_SETTINGCHANGE (SPI_SETWORKAREA) received - taskbar changed\n");
#endif
Logger::info("WM_SETTINGCHANGE (SPI_SETWORKAREA) received");
// Taskbar position/size changed
KillTimer(hwnd, TIMER_UPDATE_MONITORS);
SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr);
}
break;
case WM_DEVICECHANGE:
// Handle monitor hardware add/remove
if (wParam == DBT_DEVNODES_CHANGED)
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] DBT_DEVNODES_CHANGED received - monitor hardware change detected\n");
#endif
Logger::info("DBT_DEVNODES_CHANGED received - monitor hardware change detected");
// Debounce: Wait for multiple changes to settle
KillTimer(hwnd, TIMER_UPDATE_MONITORS);
SetTimer(hwnd, TIMER_UPDATE_MONITORS, DEBOUNCE_DELAY_MS, nullptr);
return TRUE;
}
break;
case WM_TIMER:
if (wParam == TIMER_UPDATE_MONITORS)
{
#ifdef _DEBUG
OutputDebugStringW(L"[CursorWrap] Debounce timer expired - triggering topology update\n");
#endif
KillTimer(hwnd, TIMER_UPDATE_MONITORS);
g_cursorWrapInstance->OnDisplayChange();
}
break;
}
return DefWindowProcW(hwnd, msg, wParam, lParam);
}
static LRESULT CALLBACK MouseHookProc(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode >= 0 && wParam == WM_MOUSEMOVE)
{
auto* pMouseStruct = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
POINT currentPos = { pMouseStruct->pt.x, pMouseStruct->pt.y };
if (g_cursorWrapInstance && g_cursorWrapInstance->m_hookActive)
{
POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove(
currentPos,
g_cursorWrapInstance->m_disableWrapDuringDrag,
g_cursorWrapInstance->m_wrapMode,
g_cursorWrapInstance->m_disableOnSingleMonitor);
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
{
#ifdef _DEBUG
Logger::info(L"CursorWrap DEBUG: Wrapping cursor from ({}, {}) to ({}, {})",
currentPos.x, currentPos.y, newPos.x, newPos.y);
#endif
SetCursorPos(newPos.x, newPos.y);
return 1; // Suppress the original message
}
}
}
return CallNextHookEx(nullptr, nCode, wParam, lParam);
}
};
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CursorWrap();
}