mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-23 19:49:43 +01:00
## Summary of the Pull Request Update coordinate mapping across monitors --- ### Testing instructions #### Single monitor - Enable CursorWrap - the cursor should wrap around the top/bottom and left/right edges of the display. - Single monitor - Add a second monitor: - If you have a USB monitor, add the monitor to your PC, CursorWrap should detect the new monitor and wrapping should occur from the edges of the monitor depending on monitor layout - for example, if the monitor is added to the left of the main display then the cursor should move freely between the left edge of the main monitor to the right edge of the added monitor - the cursor should wrap from the left edge of the added monitor to the right edge of the main monitor (same for top/bottom if the new monitor is added above/below the main monitor). #### Multi monitor - If you have a static multi-monitor layout cursor should wrap for outer edges of the monitor setup, for example, if you have three monitors in a layout of [1][0][2] then the cursor should wrap from the right edge of [2] to the left edge of [1], top/bottom should wrap on each monitor. #### Issues - If you detect any issues then run the Capture-MonitorLayout.ps1 file, this will generate a JSON file with your monitor layout, attach the file to a new comment. --- <!-- 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 ## Validation Steps Performed validated on three monitor setup and laptop with an external monitor --------- Co-authored-by: Niels Laute <niels.laute@live.nl> Co-authored-by: Leilei Zhang <leilzh@microsoft.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1271 lines
50 KiB
C++
1271 lines
50 KiB
C++
#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 "resource.h"
|
|
#include "CursorWrapTests.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";
|
|
}
|
|
|
|
// 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>";
|
|
|
|
// Mouse hook data structure
|
|
struct MonitorInfo
|
|
{
|
|
RECT rect;
|
|
bool isPrimary;
|
|
int monitorId; // Add monitor ID for easier debugging
|
|
};
|
|
|
|
// Add structure for logical monitor grid position
|
|
struct LogicalPosition
|
|
{
|
|
int row;
|
|
int col;
|
|
bool isValid;
|
|
};
|
|
|
|
// Add monitor topology helper
|
|
struct MonitorTopology
|
|
{
|
|
std::vector<std::vector<HMONITOR>> grid; // 3x3 grid of monitors
|
|
std::map<HMONITOR, LogicalPosition> monitorToPosition;
|
|
std::map<std::pair<int, int>, HMONITOR> positionToMonitor;
|
|
|
|
void Initialize(const std::vector<MonitorInfo>& monitors);
|
|
LogicalPosition GetPosition(HMONITOR monitor) const;
|
|
HMONITOR GetMonitorAt(int row, int col) const;
|
|
HMONITOR FindAdjacentMonitor(HMONITOR current, int deltaRow, int deltaCol) const;
|
|
};
|
|
|
|
// 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
|
|
|
|
// Mouse hook
|
|
HHOOK m_mouseHook = nullptr;
|
|
std::atomic<bool> m_hookActive{ false };
|
|
|
|
// Monitor information
|
|
std::vector<MonitorInfo> m_monitors;
|
|
MonitorTopology m_topology;
|
|
|
|
// 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 };
|
|
|
|
public:
|
|
// Constructor
|
|
CursorWrap()
|
|
{
|
|
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::cursorWrapLoggerName);
|
|
init_settings();
|
|
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 && 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);
|
|
|
|
StartMouseHook();
|
|
Logger::info("CursorWrap enabled - mouse hook started");
|
|
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
}
|
|
|
|
private:
|
|
void ToggleMouseHook()
|
|
{
|
|
// Toggle cursor wrapping.
|
|
if (m_hookActive)
|
|
{
|
|
StopMouseHook();
|
|
}
|
|
else
|
|
{
|
|
StartMouseHook();
|
|
#ifdef _DEBUG
|
|
// Run comprehensive tests when hook is started in debug builds
|
|
RunComprehensiveTests();
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// 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)");
|
|
}
|
|
}
|
|
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 UpdateMonitorInfo()
|
|
{
|
|
m_monitors.clear();
|
|
|
|
EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) -> BOOL {
|
|
auto* self = reinterpret_cast<CursorWrap*>(lParam);
|
|
|
|
MONITORINFO mi{};
|
|
mi.cbSize = sizeof(MONITORINFO);
|
|
if (GetMonitorInfo(hMonitor, &mi))
|
|
{
|
|
MonitorInfo info{};
|
|
info.rect = mi.rcMonitor;
|
|
info.isPrimary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0;
|
|
info.monitorId = static_cast<int>(self->m_monitors.size());
|
|
self->m_monitors.push_back(info);
|
|
}
|
|
|
|
return TRUE;
|
|
}, reinterpret_cast<LPARAM>(this));
|
|
|
|
// Initialize monitor topology
|
|
m_topology.Initialize(m_monitors);
|
|
}
|
|
|
|
void StartMouseHook()
|
|
{
|
|
if (m_mouseHook || m_hookActive)
|
|
{
|
|
Logger::info("CursorWrap mouse hook already active");
|
|
return;
|
|
}
|
|
|
|
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
|
|
}
|
|
}
|
|
|
|
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->HandleMouseMove(currentPos);
|
|
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);
|
|
}
|
|
|
|
// Helper method to check if there's a monitor adjacent in coordinate space (not grid)
|
|
bool HasAdjacentMonitorInCoordinateSpace(const RECT& currentMonitorRect, int direction)
|
|
{
|
|
// direction: 0=left, 1=right, 2=top, 3=bottom
|
|
const int tolerance = 50; // Allow small gaps
|
|
|
|
for (const auto& monitor : m_monitors)
|
|
{
|
|
bool isAdjacent = false;
|
|
|
|
switch (direction)
|
|
{
|
|
case 0: // Left - check if another monitor's right edge touches/overlaps our left edge
|
|
isAdjacent = (abs(monitor.rect.right - currentMonitorRect.left) <= tolerance) &&
|
|
(monitor.rect.bottom > currentMonitorRect.top + tolerance) &&
|
|
(monitor.rect.top < currentMonitorRect.bottom - tolerance);
|
|
break;
|
|
|
|
case 1: // Right - check if another monitor's left edge touches/overlaps our right edge
|
|
isAdjacent = (abs(monitor.rect.left - currentMonitorRect.right) <= tolerance) &&
|
|
(monitor.rect.bottom > currentMonitorRect.top + tolerance) &&
|
|
(monitor.rect.top < currentMonitorRect.bottom - tolerance);
|
|
break;
|
|
|
|
case 2: // Top - check if another monitor's bottom edge touches/overlaps our top edge
|
|
isAdjacent = (abs(monitor.rect.bottom - currentMonitorRect.top) <= tolerance) &&
|
|
(monitor.rect.right > currentMonitorRect.left + tolerance) &&
|
|
(monitor.rect.left < currentMonitorRect.right - tolerance);
|
|
break;
|
|
|
|
case 3: // Bottom - check if another monitor's top edge touches/overlaps our bottom edge
|
|
isAdjacent = (abs(monitor.rect.top - currentMonitorRect.bottom) <= tolerance) &&
|
|
(monitor.rect.right > currentMonitorRect.left + tolerance) &&
|
|
(monitor.rect.left < currentMonitorRect.right - tolerance);
|
|
break;
|
|
}
|
|
|
|
if (isAdjacent)
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: Found adjacent monitor in coordinate space (direction {})", direction);
|
|
#endif
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
// *** COMPLETELY REWRITTEN CURSOR WRAPPING LOGIC ***
|
|
// Implements vertical scrolling to bottom/top of vertical stack as requested
|
|
// Only wraps when there's NO adjacent monitor in the coordinate space
|
|
POINT HandleMouseMove(const POINT& currentPos)
|
|
{
|
|
POINT newPos = currentPos;
|
|
|
|
// Check if we should skip wrapping during drag if the setting is enabled
|
|
if (m_disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000))
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: Left mouse button is down and disable_wrap_during_drag is enabled - skipping wrap");
|
|
#endif
|
|
return currentPos; // Return unchanged position (no wrapping)
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: ======= HANDLE MOUSE MOVE START =======");
|
|
Logger::info(L"CursorWrap DEBUG: Input position ({}, {})", currentPos.x, currentPos.y);
|
|
#endif
|
|
|
|
// Find which monitor the cursor is currently on
|
|
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
|
|
MONITORINFO currentMonitorInfo{};
|
|
currentMonitorInfo.cbSize = sizeof(MONITORINFO);
|
|
GetMonitorInfo(currentMonitor, ¤tMonitorInfo);
|
|
|
|
LogicalPosition currentLogicalPos = m_topology.GetPosition(currentMonitor);
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: Current monitor bounds: Left={}, Top={}, Right={}, Bottom={}",
|
|
currentMonitorInfo.rcMonitor.left, currentMonitorInfo.rcMonitor.top,
|
|
currentMonitorInfo.rcMonitor.right, currentMonitorInfo.rcMonitor.bottom);
|
|
Logger::info(L"CursorWrap DEBUG: Logical position: Row={}, Col={}, Valid={}",
|
|
currentLogicalPos.row, currentLogicalPos.col, currentLogicalPos.isValid);
|
|
#endif
|
|
|
|
bool wrapped = false;
|
|
|
|
// *** VERTICAL WRAPPING LOGIC - CONFIRMED WORKING ***
|
|
// Move to bottom of vertical stack when hitting top edge
|
|
// Only wrap if there's NO adjacent monitor in the coordinate space
|
|
if (currentPos.y <= currentMonitorInfo.rcMonitor.top)
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: TOP EDGE DETECTED =======");
|
|
#endif
|
|
|
|
// Check if there's an adjacent monitor above in coordinate space
|
|
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 2))
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists above (Windows will handle)");
|
|
#endif
|
|
return currentPos; // Let Windows handle natural cursor movement
|
|
}
|
|
|
|
// Find the bottom-most monitor in the vertical stack (same column)
|
|
HMONITOR bottomMonitor = nullptr;
|
|
|
|
if (currentLogicalPos.isValid) {
|
|
// Search down from current position to find the bottom-most monitor in same column
|
|
for (int row = 2; row >= 0; row--) { // Start from bottom and work up
|
|
HMONITOR candidateMonitor = m_topology.GetMonitorAt(row, currentLogicalPos.col);
|
|
if (candidateMonitor) {
|
|
bottomMonitor = candidateMonitor;
|
|
break; // Found the bottom-most monitor
|
|
}
|
|
}
|
|
}
|
|
|
|
if (bottomMonitor && bottomMonitor != currentMonitor) {
|
|
// *** MOVE TO BOTTOM OF VERTICAL STACK ***
|
|
MONITORINFO bottomInfo{};
|
|
bottomInfo.cbSize = sizeof(MONITORINFO);
|
|
GetMonitorInfo(bottomMonitor, &bottomInfo);
|
|
|
|
// Calculate relative X position to maintain cursor X alignment
|
|
double relativeX = static_cast<double>(currentPos.x - currentMonitorInfo.rcMonitor.left) /
|
|
(currentMonitorInfo.rcMonitor.right - currentMonitorInfo.rcMonitor.left);
|
|
|
|
int targetWidth = bottomInfo.rcMonitor.right - bottomInfo.rcMonitor.left;
|
|
newPos.x = bottomInfo.rcMonitor.left + static_cast<int>(relativeX * targetWidth);
|
|
newPos.y = bottomInfo.rcMonitor.bottom - 1; // Bottom edge of bottom monitor
|
|
|
|
// Clamp X to target monitor bounds
|
|
newPos.x = max(bottomInfo.rcMonitor.left, min(newPos.x, bottomInfo.rcMonitor.right - 1));
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP SUCCESS - Moved to bottom of vertical stack");
|
|
Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y);
|
|
#endif
|
|
} else {
|
|
// *** NO OTHER MONITOR IN VERTICAL STACK - WRAP WITHIN CURRENT MONITOR ***
|
|
newPos.y = currentMonitorInfo.rcMonitor.bottom - 1;
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP - No other monitor in stack, wrapping within current monitor");
|
|
#endif
|
|
}
|
|
}
|
|
else if (currentPos.y >= currentMonitorInfo.rcMonitor.bottom - 1)
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: ======= VERTICAL WRAP: BOTTOM EDGE DETECTED =======");
|
|
#endif
|
|
|
|
// Check if there's an adjacent monitor below in coordinate space
|
|
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 3))
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists below (Windows will handle)");
|
|
#endif
|
|
return currentPos; // Let Windows handle natural cursor movement
|
|
}
|
|
|
|
// Find the top-most monitor in the vertical stack (same column)
|
|
HMONITOR topMonitor = nullptr;
|
|
|
|
if (currentLogicalPos.isValid) {
|
|
// Search up from current position to find the top-most monitor in same column
|
|
for (int row = 0; row <= 2; row++) { // Start from top and work down
|
|
HMONITOR candidateMonitor = m_topology.GetMonitorAt(row, currentLogicalPos.col);
|
|
if (candidateMonitor) {
|
|
topMonitor = candidateMonitor;
|
|
break; // Found the top-most monitor
|
|
}
|
|
}
|
|
}
|
|
|
|
if (topMonitor && topMonitor != currentMonitor) {
|
|
// *** MOVE TO TOP OF VERTICAL STACK ***
|
|
MONITORINFO topInfo{};
|
|
topInfo.cbSize = sizeof(MONITORINFO);
|
|
GetMonitorInfo(topMonitor, &topInfo);
|
|
|
|
// Calculate relative X position to maintain cursor X alignment
|
|
double relativeX = static_cast<double>(currentPos.x - currentMonitorInfo.rcMonitor.left) /
|
|
(currentMonitorInfo.rcMonitor.right - currentMonitorInfo.rcMonitor.left);
|
|
|
|
int targetWidth = topInfo.rcMonitor.right - topInfo.rcMonitor.left;
|
|
newPos.x = topInfo.rcMonitor.left + static_cast<int>(relativeX * targetWidth);
|
|
newPos.y = topInfo.rcMonitor.top; // Top edge of top monitor
|
|
|
|
// Clamp X to target monitor bounds
|
|
newPos.x = max(topInfo.rcMonitor.left, min(newPos.x, topInfo.rcMonitor.right - 1));
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP SUCCESS - Moved to top of vertical stack");
|
|
Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y);
|
|
#endif
|
|
} else {
|
|
// *** NO OTHER MONITOR IN VERTICAL STACK - WRAP WITHIN CURRENT MONITOR ***
|
|
newPos.y = currentMonitorInfo.rcMonitor.top;
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: VERTICAL WRAP - No other monitor in stack, wrapping within current monitor");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// *** FIXED HORIZONTAL WRAPPING LOGIC ***
|
|
// Move to opposite end of horizontal stack when hitting left/right edge
|
|
// Only wrap if there's NO adjacent monitor in the coordinate space (let Windows handle natural transitions)
|
|
if (!wrapped && currentPos.x <= currentMonitorInfo.rcMonitor.left)
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: LEFT EDGE DETECTED =======");
|
|
#endif
|
|
|
|
// Check if there's an adjacent monitor to the left in coordinate space
|
|
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 0))
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the left (Windows will handle)");
|
|
#endif
|
|
return currentPos; // Let Windows handle natural cursor movement
|
|
}
|
|
|
|
// Find the right-most monitor in the horizontal stack (same row)
|
|
HMONITOR rightMonitor = nullptr;
|
|
|
|
if (currentLogicalPos.isValid) {
|
|
// Search right from current position to find the right-most monitor in same row
|
|
for (int col = 2; col >= 0; col--) { // Start from right and work left
|
|
HMONITOR candidateMonitor = m_topology.GetMonitorAt(currentLogicalPos.row, col);
|
|
if (candidateMonitor) {
|
|
rightMonitor = candidateMonitor;
|
|
break; // Found the right-most monitor
|
|
}
|
|
}
|
|
}
|
|
|
|
if (rightMonitor && rightMonitor != currentMonitor) {
|
|
// *** MOVE TO RIGHT END OF HORIZONTAL STACK ***
|
|
MONITORINFO rightInfo{};
|
|
rightInfo.cbSize = sizeof(MONITORINFO);
|
|
GetMonitorInfo(rightMonitor, &rightInfo);
|
|
|
|
// Calculate relative Y position to maintain cursor Y alignment
|
|
double relativeY = static_cast<double>(currentPos.y - currentMonitorInfo.rcMonitor.top) /
|
|
(currentMonitorInfo.rcMonitor.bottom - currentMonitorInfo.rcMonitor.top);
|
|
|
|
int targetHeight = rightInfo.rcMonitor.bottom - rightInfo.rcMonitor.top;
|
|
newPos.y = rightInfo.rcMonitor.top + static_cast<int>(relativeY * targetHeight);
|
|
newPos.x = rightInfo.rcMonitor.right - 1; // Right edge of right monitor
|
|
|
|
// Clamp Y to target monitor bounds
|
|
newPos.y = max(rightInfo.rcMonitor.top, min(newPos.y, rightInfo.rcMonitor.bottom - 1));
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP SUCCESS - Moved to right end of horizontal stack");
|
|
Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y);
|
|
#endif
|
|
} else {
|
|
// *** NO OTHER MONITOR IN HORIZONTAL STACK - WRAP WITHIN CURRENT MONITOR ***
|
|
newPos.x = currentMonitorInfo.rcMonitor.right - 1;
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP - No other monitor in stack, wrapping within current monitor");
|
|
#endif
|
|
}
|
|
}
|
|
else if (!wrapped && currentPos.x >= currentMonitorInfo.rcMonitor.right - 1)
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: ======= HORIZONTAL WRAP: RIGHT EDGE DETECTED =======");
|
|
#endif
|
|
|
|
// Check if there's an adjacent monitor to the right in coordinate space
|
|
if (HasAdjacentMonitorInCoordinateSpace(currentMonitorInfo.rcMonitor, 1))
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: SKIPPING WRAP - Adjacent monitor exists to the right (Windows will handle)");
|
|
#endif
|
|
return currentPos; // Let Windows handle natural cursor movement
|
|
}
|
|
|
|
// Find the left-most monitor in the horizontal stack (same row)
|
|
HMONITOR leftMonitor = nullptr;
|
|
|
|
if (currentLogicalPos.isValid) {
|
|
// Search left from current position to find the left-most monitor in same row
|
|
for (int col = 0; col <= 2; col++) { // Start from left and work right
|
|
HMONITOR candidateMonitor = m_topology.GetMonitorAt(currentLogicalPos.row, col);
|
|
if (candidateMonitor) {
|
|
leftMonitor = candidateMonitor;
|
|
break; // Found the left-most monitor
|
|
}
|
|
}
|
|
}
|
|
|
|
if (leftMonitor && leftMonitor != currentMonitor) {
|
|
// *** MOVE TO LEFT END OF HORIZONTAL STACK ***
|
|
MONITORINFO leftInfo{};
|
|
leftInfo.cbSize = sizeof(MONITORINFO);
|
|
GetMonitorInfo(leftMonitor, &leftInfo);
|
|
|
|
// Calculate relative Y position to maintain cursor Y alignment
|
|
double relativeY = static_cast<double>(currentPos.y - currentMonitorInfo.rcMonitor.top) /
|
|
(currentMonitorInfo.rcMonitor.bottom - currentMonitorInfo.rcMonitor.top);
|
|
|
|
int targetHeight = leftInfo.rcMonitor.bottom - leftInfo.rcMonitor.top;
|
|
newPos.y = leftInfo.rcMonitor.top + static_cast<int>(relativeY * targetHeight);
|
|
newPos.x = leftInfo.rcMonitor.left; // Left edge of left monitor
|
|
|
|
// Clamp Y to target monitor bounds
|
|
newPos.y = max(leftInfo.rcMonitor.top, min(newPos.y, leftInfo.rcMonitor.bottom - 1));
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP SUCCESS - Moved to left end of horizontal stack");
|
|
Logger::info(L"CursorWrap DEBUG: New position: ({}, {})", newPos.x, newPos.y);
|
|
#endif
|
|
} else {
|
|
// *** NO OTHER MONITOR IN HORIZONTAL STACK - WRAP WITHIN CURRENT MONITOR ***
|
|
newPos.x = currentMonitorInfo.rcMonitor.left;
|
|
wrapped = true;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: HORIZONTAL WRAP - No other monitor in stack, wrapping within current monitor");
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
if (wrapped)
|
|
{
|
|
Logger::info(L"CursorWrap DEBUG: ======= WRAP RESULT =======");
|
|
Logger::info(L"CursorWrap DEBUG: Original: ({}, {}) -> New: ({}, {})",
|
|
currentPos.x, currentPos.y, newPos.x, newPos.y);
|
|
}
|
|
else
|
|
{
|
|
Logger::info(L"CursorWrap DEBUG: No wrapping performed - cursor not at edge");
|
|
}
|
|
Logger::info(L"CursorWrap DEBUG: ======= HANDLE MOUSE MOVE END =======");
|
|
#endif
|
|
|
|
return newPos;
|
|
}
|
|
|
|
// Add test method for monitor topology validation
|
|
void RunMonitorTopologyTests()
|
|
{
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap: Running monitor topology tests...");
|
|
|
|
// Test all 9 possible monitor positions in 3x3 grid
|
|
const char* gridNames[3][3] = {
|
|
{"TL", "TC", "TR"}, // Top-Left, Top-Center, Top-Right
|
|
{"ML", "MC", "MR"}, // Middle-Left, Middle-Center, Middle-Right
|
|
{"BL", "BC", "BR"} // Bottom-Left, Bottom-Center, Bottom-Right
|
|
};
|
|
|
|
for (int row = 0; row < 3; row++)
|
|
{
|
|
for (int col = 0; col < 3; col++)
|
|
{
|
|
HMONITOR monitor = m_topology.GetMonitorAt(row, col);
|
|
if (monitor)
|
|
{
|
|
std::string gridName(gridNames[row][col]);
|
|
std::wstring wGridName(gridName.begin(), gridName.end());
|
|
Logger::info(L"CursorWrap TEST: Monitor at [{}][{}] ({}) exists",
|
|
row, col, wGridName.c_str());
|
|
|
|
// Test adjacent monitor finding
|
|
HMONITOR up = m_topology.FindAdjacentMonitor(monitor, -1, 0);
|
|
HMONITOR down = m_topology.FindAdjacentMonitor(monitor, 1, 0);
|
|
HMONITOR left = m_topology.FindAdjacentMonitor(monitor, 0, -1);
|
|
HMONITOR right = m_topology.FindAdjacentMonitor(monitor, 0, 1);
|
|
|
|
Logger::info(L"CursorWrap TEST: Adjacent monitors - Up: {}, Down: {}, Left: {}, Right: {}",
|
|
up ? L"YES" : L"NO", down ? L"YES" : L"NO",
|
|
left ? L"YES" : L"NO", right ? L"YES" : L"NO");
|
|
}
|
|
}
|
|
}
|
|
|
|
Logger::info(L"CursorWrap: Monitor topology tests completed.");
|
|
#endif
|
|
}
|
|
|
|
// Add method to trigger test suite (can be called via hotkey in debug builds)
|
|
void RunComprehensiveTests()
|
|
{
|
|
#ifdef _DEBUG
|
|
RunMonitorTopologyTests();
|
|
|
|
// Test cursor wrapping scenarios
|
|
Logger::info(L"CursorWrap: Testing cursor wrapping scenarios...");
|
|
|
|
// Simulate cursor positions at each monitor edge and verify expected behavior
|
|
for (const auto& monitor : m_monitors)
|
|
{
|
|
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
|
LogicalPosition pos = m_topology.GetPosition(hMonitor);
|
|
|
|
if (pos.isValid)
|
|
{
|
|
Logger::info(L"CursorWrap TEST: Testing monitor at position [{}][{}]", pos.row, pos.col);
|
|
|
|
// Test top edge
|
|
POINT topEdge = {(monitor.rect.left + monitor.rect.right) / 2, monitor.rect.top};
|
|
POINT newPos = HandleMouseMove(topEdge);
|
|
Logger::info(L"CursorWrap TEST: Top edge ({}, {}) -> ({}, {})",
|
|
topEdge.x, topEdge.y, newPos.x, newPos.y);
|
|
|
|
// Test bottom edge
|
|
POINT bottomEdge = {(monitor.rect.left + monitor.rect.right) / 2, monitor.rect.bottom - 1};
|
|
newPos = HandleMouseMove(bottomEdge);
|
|
Logger::info(L"CursorWrap TEST: Bottom edge ({}, {}) -> ({}, {})",
|
|
bottomEdge.x, bottomEdge.y, newPos.x, newPos.y);
|
|
|
|
// Test left edge
|
|
POINT leftEdge = {monitor.rect.left, (monitor.rect.top + monitor.rect.bottom) / 2};
|
|
newPos = HandleMouseMove(leftEdge);
|
|
Logger::info(L"CursorWrap TEST: Left edge ({}, {}) -> ({}, {})",
|
|
leftEdge.x, leftEdge.y, newPos.x, newPos.y);
|
|
|
|
// Test right edge
|
|
POINT rightEdge = {monitor.rect.right - 1, (monitor.rect.top + monitor.rect.bottom) / 2};
|
|
newPos = HandleMouseMove(rightEdge);
|
|
Logger::info(L"CursorWrap TEST: Right edge ({}, {}) -> ({}, {})",
|
|
rightEdge.x, rightEdge.y, newPos.x, newPos.y);
|
|
}
|
|
}
|
|
|
|
Logger::info(L"CursorWrap: Comprehensive tests completed.");
|
|
#endif
|
|
}
|
|
};
|
|
|
|
// Implementation of MonitorTopology methods
|
|
void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
|
|
{
|
|
// Clear existing data
|
|
grid.assign(3, std::vector<HMONITOR>(3, nullptr));
|
|
monitorToPosition.clear();
|
|
positionToMonitor.clear();
|
|
|
|
if (monitors.empty()) return;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: ======= TOPOLOGY INITIALIZATION START =======");
|
|
Logger::info(L"CursorWrap DEBUG: Initializing topology for {} monitors", monitors.size());
|
|
for (const auto& monitor : monitors)
|
|
{
|
|
Logger::info(L"CursorWrap DEBUG: Monitor {}: bounds=({},{},{},{}), isPrimary={}",
|
|
monitor.monitorId, monitor.rect.left, monitor.rect.top,
|
|
monitor.rect.right, monitor.rect.bottom, monitor.isPrimary);
|
|
}
|
|
#endif
|
|
|
|
// Special handling for 2 monitors - use physical position, not discovery order
|
|
if (monitors.size() == 2)
|
|
{
|
|
// Determine if arrangement is horizontal or vertical by comparing centers
|
|
POINT center0 = {(monitors[0].rect.left + monitors[0].rect.right) / 2,
|
|
(monitors[0].rect.top + monitors[0].rect.bottom) / 2};
|
|
POINT center1 = {(monitors[1].rect.left + monitors[1].rect.right) / 2,
|
|
(monitors[1].rect.top + monitors[1].rect.bottom) / 2};
|
|
|
|
int xDiff = abs(center0.x - center1.x);
|
|
int yDiff = abs(center0.y - center1.y);
|
|
|
|
bool isHorizontal = xDiff > yDiff;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: Monitor centers: M0=({}, {}), M1=({}, {})",
|
|
center0.x, center0.y, center1.x, center1.y);
|
|
Logger::info(L"CursorWrap DEBUG: Differences: X={}, Y={}, IsHorizontal={}",
|
|
xDiff, yDiff, isHorizontal);
|
|
#endif
|
|
|
|
if (isHorizontal)
|
|
{
|
|
// Horizontal arrangement - place in middle row [1,0] and [1,2]
|
|
for (const auto& monitor : monitors)
|
|
{
|
|
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
|
POINT center = {(monitor.rect.left + monitor.rect.right) / 2,
|
|
(monitor.rect.top + monitor.rect.bottom) / 2};
|
|
|
|
int row = 1; // Middle row
|
|
int col = (center.x < (center0.x + center1.x) / 2) ? 0 : 2; // Left or right based on center
|
|
|
|
grid[row][col] = hMonitor;
|
|
monitorToPosition[hMonitor] = {row, col, true};
|
|
positionToMonitor[{row, col}] = hMonitor;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: Monitor {} (horizontal) placed at grid[{}][{}]",
|
|
monitor.monitorId, row, col);
|
|
#endif
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// *** VERTICAL ARRANGEMENT - CRITICAL LOGIC ***
|
|
// Sort monitors by Y coordinate to determine vertical order
|
|
std::vector<std::pair<int, MonitorInfo>> sortedMonitors;
|
|
for (int i = 0; i < 2; i++) {
|
|
sortedMonitors.push_back({i, monitors[i]});
|
|
}
|
|
|
|
// Sort by Y coordinate (top to bottom)
|
|
std::sort(sortedMonitors.begin(), sortedMonitors.end(),
|
|
[](const std::pair<int, MonitorInfo>& a, const std::pair<int, MonitorInfo>& b) {
|
|
int centerA = (a.second.rect.top + a.second.rect.bottom) / 2;
|
|
int centerB = (b.second.rect.top + b.second.rect.bottom) / 2;
|
|
return centerA < centerB; // Top first
|
|
});
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: VERTICAL ARRANGEMENT DETECTED");
|
|
Logger::info(L"CursorWrap DEBUG: Top monitor: ID={}, Y-center={}",
|
|
sortedMonitors[0].second.monitorId,
|
|
(sortedMonitors[0].second.rect.top + sortedMonitors[0].second.rect.bottom) / 2);
|
|
Logger::info(L"CursorWrap DEBUG: Bottom monitor: ID={}, Y-center={}",
|
|
sortedMonitors[1].second.monitorId,
|
|
(sortedMonitors[1].second.rect.top + sortedMonitors[1].second.rect.bottom) / 2);
|
|
#endif
|
|
|
|
// Place monitors in grid based on sorted order
|
|
for (int i = 0; i < 2; i++) {
|
|
const auto& monitorPair = sortedMonitors[i];
|
|
const auto& monitor = monitorPair.second;
|
|
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
|
|
|
int col = 1; // Middle column for vertical arrangement
|
|
int row = (i == 0) ? 0 : 2; // Top monitor at row 0, bottom at row 2
|
|
|
|
grid[row][col] = hMonitor;
|
|
monitorToPosition[hMonitor] = {row, col, true};
|
|
positionToMonitor[{row, col}] = hMonitor;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: Monitor {} (vertical) placed at grid[{}][{}] - {} position",
|
|
monitor.monitorId, row, col, (i == 0) ? L"TOP" : L"BOTTOM");
|
|
#endif
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// For more than 2 monitors, use edge-based alignment algorithm
|
|
// This ensures monitors with aligned edges (e.g., top edges at same Y) are grouped in same row
|
|
|
|
// Helper lambda to check if two ranges overlap or are adjacent (with tolerance)
|
|
auto rangesOverlapOrTouch = [](int start1, int end1, int start2, int end2, int tolerance = 50) -> bool {
|
|
// Check if ranges overlap or are within tolerance distance
|
|
return (start1 <= end2 + tolerance) && (start2 <= end1 + tolerance);
|
|
};
|
|
|
|
// Sort monitors by horizontal position (left edge) for column assignment
|
|
std::vector<const MonitorInfo*> monitorsByX;
|
|
for (const auto& monitor : monitors) {
|
|
monitorsByX.push_back(&monitor);
|
|
}
|
|
std::sort(monitorsByX.begin(), monitorsByX.end(), [](const MonitorInfo* a, const MonitorInfo* b) {
|
|
return a->rect.left < b->rect.left;
|
|
});
|
|
|
|
// Sort monitors by vertical position (top edge) for row assignment
|
|
std::vector<const MonitorInfo*> monitorsByY;
|
|
for (const auto& monitor : monitors) {
|
|
monitorsByY.push_back(&monitor);
|
|
}
|
|
std::sort(monitorsByY.begin(), monitorsByY.end(), [](const MonitorInfo* a, const MonitorInfo* b) {
|
|
return a->rect.top < b->rect.top;
|
|
});
|
|
|
|
// Assign rows based on vertical overlap - monitors that overlap vertically should be in same row
|
|
std::map<const MonitorInfo*, int> monitorToRow;
|
|
int currentRow = 0;
|
|
|
|
for (size_t i = 0; i < monitorsByY.size(); i++) {
|
|
const auto* monitor = monitorsByY[i];
|
|
|
|
// Check if this monitor overlaps vertically with any monitor already assigned to current row
|
|
bool foundOverlap = false;
|
|
for (size_t j = 0; j < i; j++) {
|
|
const auto* other = monitorsByY[j];
|
|
if (monitorToRow[other] == currentRow) {
|
|
// Check vertical overlap
|
|
if (rangesOverlapOrTouch(monitor->rect.top, monitor->rect.bottom,
|
|
other->rect.top, other->rect.bottom)) {
|
|
monitorToRow[monitor] = currentRow;
|
|
foundOverlap = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!foundOverlap) {
|
|
// Start new row if no overlap found and we have room
|
|
if (currentRow < 2 && i < monitorsByY.size() - 1) {
|
|
currentRow++;
|
|
}
|
|
monitorToRow[monitor] = currentRow;
|
|
}
|
|
}
|
|
|
|
// Assign columns based on horizontal position (left-to-right order)
|
|
// Monitors are already sorted by X coordinate (left edge)
|
|
std::map<const MonitorInfo*, int> monitorToCol;
|
|
|
|
// For horizontal arrangement, distribute monitors evenly across columns
|
|
if (monitorsByX.size() == 1) {
|
|
// Single monitor - place in middle column
|
|
monitorToCol[monitorsByX[0]] = 1;
|
|
}
|
|
else if (monitorsByX.size() == 2) {
|
|
// Two monitors - place at opposite ends for wrapping
|
|
monitorToCol[monitorsByX[0]] = 0; // Leftmost monitor
|
|
monitorToCol[monitorsByX[1]] = 2; // Rightmost monitor
|
|
}
|
|
else {
|
|
// Three or more monitors - distribute across grid
|
|
for (size_t i = 0; i < monitorsByX.size() && i < 3; i++) {
|
|
monitorToCol[monitorsByX[i]] = static_cast<int>(i);
|
|
}
|
|
// If more than 3 monitors, place extras in rightmost column
|
|
for (size_t i = 3; i < monitorsByX.size(); i++) {
|
|
monitorToCol[monitorsByX[i]] = 2;
|
|
}
|
|
}
|
|
|
|
// Place monitors in grid using the computed row/column assignments
|
|
for (const auto& monitor : monitors)
|
|
{
|
|
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
|
int row = monitorToRow[&monitor];
|
|
int col = monitorToCol[&monitor];
|
|
|
|
grid[row][col] = hMonitor;
|
|
monitorToPosition[hMonitor] = {row, col, true};
|
|
positionToMonitor[{row, col}] = hMonitor;
|
|
|
|
#ifdef _DEBUG
|
|
Logger::info(L"CursorWrap DEBUG: Monitor {} placed at grid[{}][{}] (left={}, top={}, right={}, bottom={})",
|
|
monitor.monitorId, row, col,
|
|
monitor.rect.left, monitor.rect.top, monitor.rect.right, monitor.rect.bottom);
|
|
#endif
|
|
}
|
|
}
|
|
|
|
#ifdef _DEBUG
|
|
// *** CRITICAL: Print topology map using OutputDebugString for debug builds ***
|
|
Logger::info(L"CursorWrap DEBUG: ======= FINAL TOPOLOGY MAP =======");
|
|
OutputDebugStringA("CursorWrap TOPOLOGY MAP:\n");
|
|
for (int r = 0; r < 3; r++)
|
|
{
|
|
std::string rowStr = " ";
|
|
for (int c = 0; c < 3; c++)
|
|
{
|
|
if (grid[r][c])
|
|
{
|
|
// Find monitor ID for this handle
|
|
int monitorId = -1;
|
|
for (const auto& monitor : monitors)
|
|
{
|
|
HMONITOR handle = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
|
if (handle == grid[r][c])
|
|
{
|
|
monitorId = monitor.monitorId + 1; // Convert to 1-based for display
|
|
break;
|
|
}
|
|
}
|
|
rowStr += std::to_string(monitorId) + " ";
|
|
}
|
|
else
|
|
{
|
|
rowStr += ". ";
|
|
}
|
|
}
|
|
rowStr += "\n";
|
|
OutputDebugStringA(rowStr.c_str());
|
|
|
|
// Also log to PowerToys logger
|
|
std::wstring wRowStr(rowStr.begin(), rowStr.end());
|
|
Logger::info(wRowStr.c_str());
|
|
}
|
|
OutputDebugStringA("======= END TOPOLOGY MAP =======\n");
|
|
|
|
// Additional validation logging
|
|
Logger::info(L"CursorWrap DEBUG: ======= GRID POSITION VALIDATION =======");
|
|
for (const auto& monitor : monitors)
|
|
{
|
|
HMONITOR hMonitor = MonitorFromRect(&monitor.rect, MONITOR_DEFAULTTONEAREST);
|
|
LogicalPosition pos = GetPosition(hMonitor);
|
|
if (pos.isValid)
|
|
{
|
|
Logger::info(L"CursorWrap DEBUG: Monitor {} -> grid[{}][{}]", monitor.monitorId, pos.row, pos.col);
|
|
OutputDebugStringA(("Monitor " + std::to_string(monitor.monitorId) + " -> grid[" + std::to_string(pos.row) + "][" + std::to_string(pos.col) + "]\n").c_str());
|
|
|
|
// Test adjacent finding
|
|
HMONITOR up = FindAdjacentMonitor(hMonitor, -1, 0);
|
|
HMONITOR down = FindAdjacentMonitor(hMonitor, 1, 0);
|
|
HMONITOR left = FindAdjacentMonitor(hMonitor, 0, -1);
|
|
HMONITOR right = FindAdjacentMonitor(hMonitor, 0, 1);
|
|
|
|
Logger::info(L"CursorWrap DEBUG: Monitor {} adjacents - Up: {}, Down: {}, Left: {}, Right: {}",
|
|
monitor.monitorId, up ? L"YES" : L"NO", down ? L"YES" : L"NO",
|
|
left ? L"YES" : L"NO", right ? L"YES" : L"NO");
|
|
}
|
|
}
|
|
Logger::info(L"CursorWrap DEBUG: ======= TOPOLOGY INITIALIZATION COMPLETE =======");
|
|
#endif
|
|
}
|
|
|
|
LogicalPosition MonitorTopology::GetPosition(HMONITOR monitor) const
|
|
{
|
|
auto it = monitorToPosition.find(monitor);
|
|
if (it != monitorToPosition.end())
|
|
{
|
|
return it->second;
|
|
}
|
|
return {-1, -1, false};
|
|
}
|
|
|
|
HMONITOR MonitorTopology::GetMonitorAt(int row, int col) const
|
|
{
|
|
if (row >= 0 && row < 3 && col >= 0 && col < 3)
|
|
{
|
|
return grid[row][col];
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
HMONITOR MonitorTopology::FindAdjacentMonitor(HMONITOR current, int deltaRow, int deltaCol) const
|
|
{
|
|
LogicalPosition currentPos = GetPosition(current);
|
|
if (!currentPos.isValid) return nullptr;
|
|
|
|
int newRow = currentPos.row + deltaRow;
|
|
int newCol = currentPos.col + deltaCol;
|
|
|
|
return GetMonitorAt(newRow, newCol);
|
|
}
|
|
|
|
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
|
{
|
|
return new CursorWrap();
|
|
}
|