mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 01:36:31 +02:00
[Cursor Wrap] Update edge wrap model, update simulator, add cursor logging, add settings support to ModuleLoader (#45915)
This PR adds new options for disabling wrap, updates the wrapping model, extends the simulator and cursor logging. <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [ ] Closes: #45116 - [ ] Closes: #44955 - [ ] Closes: #44827 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **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 The PR adds a new option for disabling cursor wrapping, exposing three options: None - wrapping is not disabled, Ctrl key - if this is pressed then wrapping is disabled, Shift key - if this is pressed then wrapping is disabled, this would enable a user to temporarily disable wrapping if they wanted to get close to a monitor edge without wrapping (auto-hide status bar for example). The cursor wrap edge model has been updated to mirror Windows monitor-to-monitor cursor movement, this should ensure there aren't any non-wrappable edges. A new test tool has been added 'CursorLog' this is a monitor aware, dpi/scaling aware Win32 application that captures mouse movement across monitors to a log file, the log contains one line per mouse movement which includes: Monitor, x, y, scale, dpi. The wrapping simulator has been updated to include the new wrapping model and support mouse cursor log playback. ## Validation Steps Performed The updated CursorWrap has been tested on a single monitor (laptop) and multi-monitor desktop PC with monitors being offset to test edge/wrapping behavior. --------- Co-authored-by: Mike Hall <mikehall@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: vanzue <vanzue@outlook.com>
This commit is contained in:
11
.github/actions/spell-check/expect.txt
vendored
11
.github/actions/spell-check/expect.txt
vendored
@@ -67,6 +67,7 @@ ARPINSTALLLOCATION
|
||||
ARPPRODUCTICON
|
||||
ARRAYSIZE
|
||||
ARROWKEYS
|
||||
arrowshape
|
||||
asf
|
||||
AShortcut
|
||||
ASingle
|
||||
@@ -537,6 +538,8 @@ HIBYTE
|
||||
hicon
|
||||
HIDEWINDOW
|
||||
Hif
|
||||
highlightbackground
|
||||
highlightthickness
|
||||
HIMAGELIST
|
||||
himl
|
||||
hinst
|
||||
@@ -627,6 +630,7 @@ inetcpl
|
||||
Infobar
|
||||
INFOEXAMPLE
|
||||
Infotip
|
||||
initialfile
|
||||
INITDIALOG
|
||||
INITGUID
|
||||
INITTOLOGFONTSTRUCT
|
||||
@@ -809,6 +813,7 @@ Metadatas
|
||||
metafile
|
||||
metapackage
|
||||
mfc
|
||||
mfalse
|
||||
Mgmt
|
||||
Microwaved
|
||||
midl
|
||||
@@ -867,6 +872,7 @@ msrc
|
||||
msstore
|
||||
msvcp
|
||||
MTND
|
||||
mtrue
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
muxc
|
||||
@@ -1018,6 +1024,8 @@ OWNDC
|
||||
OWNERDRAWFIXED
|
||||
Packagemanager
|
||||
PACL
|
||||
padx
|
||||
pady
|
||||
PAINTSTRUCT
|
||||
PALETTEWINDOW
|
||||
PARENTNOTIFY
|
||||
@@ -1449,6 +1457,7 @@ STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
SUBMODULEUPDATE
|
||||
sug
|
||||
Superbar
|
||||
sut
|
||||
svchost
|
||||
@@ -2033,6 +2042,7 @@ metadatamatters
|
||||
middleclickaction
|
||||
MIIM
|
||||
mikeclayton
|
||||
mikehall
|
||||
minimizebox
|
||||
modelcontextprotocol
|
||||
mousehighlighter
|
||||
@@ -2153,6 +2163,7 @@ taskbar
|
||||
TESTONLY
|
||||
TEXTBOXNEWLINE
|
||||
textextractor
|
||||
textvariable
|
||||
tgamma
|
||||
THEMECHANGED
|
||||
thickframe
|
||||
|
||||
3
.github/actions/spell-check/patterns.txt
vendored
3
.github/actions/spell-check/patterns.txt
vendored
@@ -289,3 +289,6 @@ St&yle
|
||||
|
||||
# Microsoft Store URLs and product IDs
|
||||
ms-windows-store://\S+
|
||||
|
||||
# ANSI color codes
|
||||
(?:\\(?:u00|x)1[Bb]|\\03[1-7]|\x1b|\\u\{1[Bb]\})\[\d+(?:;\d+)*m
|
||||
|
||||
@@ -13,13 +13,11 @@
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Debug'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)'=='Release'" Label="Configuration">
|
||||
<ConfigurationType>DynamicLibrary</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
@@ -112,12 +110,8 @@
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="COMPLETE_REWRITE_SUMMARY.md" />
|
||||
<None Include="CRITICAL_BUG_ANALYSIS.md" />
|
||||
<None Include="CURSOR_WRAP_FIX_ANALYSIS.md" />
|
||||
<None Include="DEBUG_GUIDE.md" />
|
||||
<None Include="CursorWrapTests\WrapSimulator\test_new_algorithm.py" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="VERTICAL_WRAP_BUG_FIX.md" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
|
||||
@@ -163,6 +163,39 @@ void CursorWrapCore::UpdateMonitorInfo()
|
||||
Logger::info(L"======= UPDATE MONITOR INFO END =======");
|
||||
}
|
||||
|
||||
void CursorWrapCore::ResetWrapState()
|
||||
{
|
||||
m_hasPreviousPosition = false;
|
||||
m_hasLastWrapDestination = false;
|
||||
m_previousPosition = { LONG_MIN, LONG_MIN };
|
||||
m_lastWrapDestination = { LONG_MIN, LONG_MIN };
|
||||
}
|
||||
|
||||
CursorDirection CursorWrapCore::CalculateDirection(const POINT& currentPos) const
|
||||
{
|
||||
CursorDirection dir = { 0, 0 };
|
||||
if (m_hasPreviousPosition)
|
||||
{
|
||||
dir.dx = currentPos.x - m_previousPosition.x;
|
||||
dir.dy = currentPos.y - m_previousPosition.y;
|
||||
}
|
||||
return dir;
|
||||
}
|
||||
|
||||
bool CursorWrapCore::IsWithinWrapThreshold(const POINT& currentPos) const
|
||||
{
|
||||
if (!m_hasLastWrapDestination)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
int dx = currentPos.x - m_lastWrapDestination.x;
|
||||
int dy = currentPos.y - m_lastWrapDestination.y;
|
||||
int distanceSquared = dx * dx + dy * dy;
|
||||
|
||||
return distanceSquared <= (WRAP_DISTANCE_THRESHOLD * WRAP_DISTANCE_THRESHOLD);
|
||||
}
|
||||
|
||||
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor)
|
||||
{
|
||||
// Check if wrapping should be disabled on single monitor
|
||||
@@ -176,6 +209,8 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
|
||||
loggedOnce = true;
|
||||
}
|
||||
#endif
|
||||
m_previousPosition = currentPos;
|
||||
m_hasPreviousPosition = true;
|
||||
return currentPos;
|
||||
}
|
||||
|
||||
@@ -185,9 +220,31 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
|
||||
#ifdef _DEBUG
|
||||
OutputDebugStringW(L"[CursorWrap] [DRAG] Left mouse button down - skipping wrap\n");
|
||||
#endif
|
||||
m_previousPosition = currentPos;
|
||||
m_hasPreviousPosition = true;
|
||||
return currentPos;
|
||||
}
|
||||
|
||||
// Check distance threshold to prevent rapid oscillation
|
||||
if (IsWithinWrapThreshold(currentPos))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
OutputDebugStringW(L"[CursorWrap] [THRESHOLD] Cursor within wrap threshold - skipping wrap\n");
|
||||
#endif
|
||||
m_previousPosition = currentPos;
|
||||
m_hasPreviousPosition = true;
|
||||
return currentPos;
|
||||
}
|
||||
|
||||
// Clear wrap destination threshold once cursor moves away
|
||||
if (m_hasLastWrapDestination && !IsWithinWrapThreshold(currentPos))
|
||||
{
|
||||
m_hasLastWrapDestination = false;
|
||||
}
|
||||
|
||||
// Calculate cursor movement direction
|
||||
CursorDirection direction = CalculateDirection(currentPos);
|
||||
|
||||
// Convert int wrapMode to WrapMode enum
|
||||
WrapMode mode = static_cast<WrapMode>(wrapMode);
|
||||
|
||||
@@ -195,6 +252,7 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
|
||||
{
|
||||
std::wostringstream oss;
|
||||
oss << L"[CursorWrap] [MOVE] Cursor at (" << currentPos.x << L", " << currentPos.y << L")";
|
||||
oss << L" direction=(" << direction.dx << L", " << direction.dy << L")";
|
||||
|
||||
// Get current monitor and identify which one
|
||||
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
|
||||
@@ -229,9 +287,9 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
|
||||
// Get current monitor
|
||||
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
|
||||
|
||||
// Check if cursor is on an outer edge (filtered by wrap mode)
|
||||
// Check if cursor is on an outer edge (filtered by wrap mode and direction)
|
||||
EdgeType edgeType;
|
||||
if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode))
|
||||
if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode, &direction))
|
||||
{
|
||||
#ifdef _DEBUG
|
||||
static bool lastWasNotOuter = false;
|
||||
@@ -241,6 +299,8 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
|
||||
lastWasNotOuter = true;
|
||||
}
|
||||
#endif
|
||||
m_previousPosition = currentPos;
|
||||
m_hasPreviousPosition = true;
|
||||
return currentPos; // Not on an outer edge
|
||||
}
|
||||
|
||||
@@ -278,5 +338,16 @@ POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapD
|
||||
}
|
||||
#endif
|
||||
|
||||
// Update tracking state
|
||||
m_previousPosition = currentPos;
|
||||
m_hasPreviousPosition = true;
|
||||
|
||||
// Store wrap destination for threshold checking
|
||||
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
|
||||
{
|
||||
m_lastWrapDestination = newPos;
|
||||
m_hasLastWrapDestination = true;
|
||||
}
|
||||
|
||||
return newPos;
|
||||
}
|
||||
|
||||
@@ -8,6 +8,24 @@
|
||||
#include <string>
|
||||
#include "MonitorTopology.h"
|
||||
|
||||
// Distance threshold to prevent rapid back-and-forth wrapping (in pixels)
|
||||
constexpr int WRAP_DISTANCE_THRESHOLD = 50;
|
||||
|
||||
// Cursor movement direction
|
||||
struct CursorDirection
|
||||
{
|
||||
int dx; // Horizontal movement (positive = right, negative = left)
|
||||
int dy; // Vertical movement (positive = down, negative = up)
|
||||
|
||||
bool IsMovingLeft() const { return dx < 0; }
|
||||
bool IsMovingRight() const { return dx > 0; }
|
||||
bool IsMovingUp() const { return dy < 0; }
|
||||
bool IsMovingDown() const { return dy > 0; }
|
||||
|
||||
// Returns true if horizontal movement is dominant
|
||||
bool IsPrimarilyHorizontal() const { return abs(dx) >= abs(dy); }
|
||||
};
|
||||
|
||||
// Core cursor wrapping engine
|
||||
class CursorWrapCore
|
||||
{
|
||||
@@ -25,11 +43,28 @@ public:
|
||||
size_t GetMonitorCount() const { return m_monitors.size(); }
|
||||
const MonitorTopology& GetTopology() const { return m_topology; }
|
||||
|
||||
// Reset wrap state (call when disabling/re-enabling)
|
||||
void ResetWrapState();
|
||||
|
||||
private:
|
||||
#ifdef _DEBUG
|
||||
std::wstring GenerateTopologyJSON() const;
|
||||
#endif
|
||||
|
||||
// Calculate movement direction from previous position
|
||||
CursorDirection CalculateDirection(const POINT& currentPos) const;
|
||||
|
||||
// Check if cursor is within threshold distance of last wrap position
|
||||
bool IsWithinWrapThreshold(const POINT& currentPos) const;
|
||||
|
||||
std::vector<MonitorInfo> m_monitors;
|
||||
MonitorTopology m_topology;
|
||||
|
||||
// Movement tracking for direction-based edge priority
|
||||
POINT m_previousPosition = { LONG_MIN, LONG_MIN };
|
||||
bool m_hasPreviousPosition = false;
|
||||
|
||||
// Wrap stability: prevent rapid oscillation
|
||||
POINT m_lastWrapDestination = { LONG_MIN, LONG_MIN };
|
||||
bool m_hasLastWrapDestination = false;
|
||||
};
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
<Solution>
|
||||
<Configurations>
|
||||
<Platform Name="x64" />
|
||||
<Platform Name="x86" />
|
||||
</Configurations>
|
||||
<Project Path="CursorLog/CursorLog.vcxproj" Id="646f6684-9f11-42cd-8b35-b2954404f985" />
|
||||
</Solution>
|
||||
@@ -0,0 +1,196 @@
|
||||
// CursorLog.cpp : Monitors mouse position and logs to file with monitor/DPI info
|
||||
//
|
||||
|
||||
#include <iostream>
|
||||
#include <fstream>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <Windows.h>
|
||||
#include <ShellScalingApi.h>
|
||||
|
||||
#pragma comment(lib, "Shcore.lib")
|
||||
|
||||
// Global variables
|
||||
std::ofstream g_outputFile;
|
||||
HHOOK g_mouseHook = nullptr;
|
||||
POINT g_lastPosition = { LONG_MIN, LONG_MIN };
|
||||
DWORD g_mainThreadId = 0;
|
||||
|
||||
// Get monitor information for a given point
|
||||
std::string GetMonitorInfo(POINT pt, UINT* dpiX, UINT* dpiY)
|
||||
{
|
||||
HMONITOR hMonitor = MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
|
||||
if (!hMonitor)
|
||||
return "Unknown";
|
||||
|
||||
MONITORINFOEX monitorInfo = {};
|
||||
monitorInfo.cbSize = sizeof(MONITORINFOEX);
|
||||
GetMonitorInfo(hMonitor, &monitorInfo);
|
||||
|
||||
// Get DPI for this monitor
|
||||
if (SUCCEEDED(GetDpiForMonitor(hMonitor, MDT_EFFECTIVE_DPI, dpiX, dpiY)))
|
||||
{
|
||||
// DPI retrieved successfully
|
||||
}
|
||||
else
|
||||
{
|
||||
*dpiX = 96;
|
||||
*dpiY = 96;
|
||||
}
|
||||
|
||||
// Convert device name to string using proper wide-to-narrow conversion
|
||||
std::wstring deviceName(monitorInfo.szDevice);
|
||||
int sizeNeeded = WideCharToMultiByte(CP_UTF8, 0, deviceName.c_str(), static_cast<int>(deviceName.length()), nullptr, 0, nullptr, nullptr);
|
||||
std::string result(sizeNeeded, 0);
|
||||
WideCharToMultiByte(CP_UTF8, 0, deviceName.c_str(), static_cast<int>(deviceName.length()), &result[0], sizeNeeded, nullptr, nullptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
// Calculate scale factor from DPI
|
||||
constexpr double GetScaleFactor(UINT dpi)
|
||||
{
|
||||
return static_cast<double>(dpi) / 96.0;
|
||||
}
|
||||
|
||||
// Low-level mouse hook callback
|
||||
LRESULT CALLBACK LowLevelMouseProc(int nCode, WPARAM wParam, LPARAM lParam)
|
||||
{
|
||||
if (nCode == HC_ACTION && wParam == WM_MOUSEMOVE)
|
||||
{
|
||||
MSLLHOOKSTRUCT* mouseStruct = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
|
||||
POINT pt = mouseStruct->pt;
|
||||
|
||||
// Only log if position changed
|
||||
if (pt.x != g_lastPosition.x || pt.y != g_lastPosition.y)
|
||||
{
|
||||
g_lastPosition = pt;
|
||||
|
||||
UINT dpiX = 96, dpiY = 96;
|
||||
std::string monitorName = GetMonitorInfo(pt, &dpiX, &dpiY);
|
||||
double scale = GetScaleFactor(dpiX);
|
||||
|
||||
if (g_outputFile.is_open())
|
||||
{
|
||||
g_outputFile << monitorName
|
||||
<< "," << pt.x
|
||||
<< "," << pt.y
|
||||
<< "," << dpiX
|
||||
<< "," << static_cast<int>(scale * 100) << "%"
|
||||
<< "\n";
|
||||
g_outputFile.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return CallNextHookEx(g_mouseHook, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
// Console control handler for clean shutdown
|
||||
BOOL WINAPI ConsoleHandler(DWORD ctrlType)
|
||||
{
|
||||
if (ctrlType == CTRL_C_EVENT || ctrlType == CTRL_CLOSE_EVENT)
|
||||
{
|
||||
std::cout << "\nShutting down..." << std::endl;
|
||||
|
||||
if (g_mouseHook)
|
||||
{
|
||||
UnhookWindowsHookEx(g_mouseHook);
|
||||
g_mouseHook = nullptr;
|
||||
}
|
||||
|
||||
if (g_outputFile.is_open())
|
||||
{
|
||||
g_outputFile.close();
|
||||
}
|
||||
|
||||
// Post quit message to the main thread to exit the message loop
|
||||
PostThreadMessage(g_mainThreadId, WM_QUIT, 0, 0);
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
// Set DPI awareness FIRST, before any other Windows API calls
|
||||
SetProcessDpiAwarenessContext(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2);
|
||||
|
||||
// Store main thread ID for clean shutdown
|
||||
g_mainThreadId = GetCurrentThreadId();
|
||||
|
||||
// Check command line arguments
|
||||
if (argc != 2)
|
||||
{
|
||||
std::cerr << "Usage: CursorLog.exe <output_path_and_filename>" << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::filesystem::path outputPath(argv[1]);
|
||||
std::filesystem::path parentPath = outputPath.parent_path();
|
||||
|
||||
// Validate the directory exists
|
||||
if (!parentPath.empty() && !std::filesystem::exists(parentPath))
|
||||
{
|
||||
std::cerr << "Error: The directory '" << parentPath.string() << "' does not exist." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Check if file exists and prompt for overwrite
|
||||
if (std::filesystem::exists(outputPath))
|
||||
{
|
||||
std::cout << "File '" << outputPath.string() << "' already exists. Overwrite? (y/n): ";
|
||||
char response;
|
||||
std::cin >> response;
|
||||
|
||||
if (response != 'y' && response != 'Y')
|
||||
{
|
||||
std::cout << "Operation cancelled." << std::endl;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
// Open output file
|
||||
g_outputFile.open(outputPath, std::ios::out | std::ios::trunc);
|
||||
if (!g_outputFile.is_open())
|
||||
{
|
||||
std::cerr << "Error: Unable to create or open file '" << outputPath.string() << "'." << std::endl;
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::cout << "Logging mouse position to: " << outputPath.string() << std::endl;
|
||||
std::cout << "Press Ctrl+C to stop..." << std::endl;
|
||||
|
||||
// Set up console control handler
|
||||
SetConsoleCtrlHandler(ConsoleHandler, TRUE);
|
||||
|
||||
// Install low-level mouse hook
|
||||
g_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, LowLevelMouseProc, nullptr, 0);
|
||||
if (!g_mouseHook)
|
||||
{
|
||||
std::cerr << "Error: Failed to install mouse hook. Error code: " << GetLastError() << std::endl;
|
||||
g_outputFile.close();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Message loop - required for low-level hooks
|
||||
MSG msg;
|
||||
while (GetMessage(&msg, nullptr, 0, 0))
|
||||
{
|
||||
TranslateMessage(&msg);
|
||||
DispatchMessage(&msg);
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if (g_mouseHook)
|
||||
{
|
||||
UnhookWindowsHookEx(g_mouseHook);
|
||||
}
|
||||
|
||||
if (g_outputFile.is_open())
|
||||
{
|
||||
g_outputFile.close();
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup Label="ProjectConfigurations">
|
||||
<ProjectConfiguration Include="Debug|Win32">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|Win32">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>Win32</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Debug|x64">
|
||||
<Configuration>Debug</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
<ProjectConfiguration Include="Release|x64">
|
||||
<Configuration>Release</Configuration>
|
||||
<Platform>x64</Platform>
|
||||
</ProjectConfiguration>
|
||||
</ItemGroup>
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>18.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
<ProjectGuid>{646f6684-9f11-42cd-8b35-b2954404f985}</ProjectGuid>
|
||||
<RootNamespace>CursorLog</RootNamespace>
|
||||
<WindowsTargetPlatformVersion>10.0</WindowsTargetPlatformVersion>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.Default.props" />
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>true</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'" Label="Configuration">
|
||||
<ConfigurationType>Application</ConfigurationType>
|
||||
<UseDebugLibraries>false</UseDebugLibraries>
|
||||
<PlatformToolset>v145</PlatformToolset>
|
||||
<WholeProgramOptimization>true</WholeProgramOptimization>
|
||||
<CharacterSet>Unicode</CharacterSet>
|
||||
</PropertyGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.props" />
|
||||
<ImportGroup Label="ExtensionSettings">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="Shared">
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<ImportGroup Label="PropertySheets" Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<Import Project="$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props" Condition="exists('$(UserRootDir)\Microsoft.Cpp.$(Platform).user.props')" Label="LocalAppDataPlatform" />
|
||||
</ImportGroup>
|
||||
<PropertyGroup Label="UserMacros" />
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|Win32'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>WIN32;NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Debug|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>_DEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemDefinitionGroup Condition="'$(Configuration)|$(Platform)'=='Release|x64'">
|
||||
<ClCompile>
|
||||
<WarningLevel>Level3</WarningLevel>
|
||||
<FunctionLevelLinking>true</FunctionLevelLinking>
|
||||
<IntrinsicFunctions>true</IntrinsicFunctions>
|
||||
<SDLCheck>true</SDLCheck>
|
||||
<PreprocessorDefinitions>NDEBUG;_CONSOLE;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<ConformanceMode>true</ConformanceMode>
|
||||
<LanguageStandard>stdcpp20</LanguageStandard><PrecompiledHeader>NotUsing</PrecompiledHeader>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<SubSystem>Console</SubSystem>
|
||||
<GenerateDebugInformation>true</GenerateDebugInformation>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="CursorLog.cpp" />
|
||||
</ItemGroup>
|
||||
<Import Project="$(VCTargetsPath)\Microsoft.Cpp.targets" />
|
||||
<ImportGroup Label="ExtensionTargets">
|
||||
</ImportGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="4.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<ItemGroup>
|
||||
<Filter Include="Source Files">
|
||||
<UniqueIdentifier>{4FC737F1-C7A5-4376-A066-2A32D752A2FF}</UniqueIdentifier>
|
||||
<Extensions>cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Header Files">
|
||||
<UniqueIdentifier>{93995380-89BD-4b04-88EB-625FBE52EBFB}</UniqueIdentifier>
|
||||
<Extensions>h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd</Extensions>
|
||||
</Filter>
|
||||
<Filter Include="Resource Files">
|
||||
<UniqueIdentifier>{67DA6AB6-F800-4c08-8B7A-83BB121AAD01}</UniqueIdentifier>
|
||||
<Extensions>rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms</Extensions>
|
||||
</Filter>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="CursorLog.cpp">
|
||||
<Filter>Source Files</Filter>
|
||||
</ClCompile>
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -0,0 +1,287 @@
|
||||
# CursorWrap Simulator
|
||||
|
||||
A Python visualization tool that displays monitor layouts and shows which edges will wrap to other monitors using the exact same logic as the PowerToys CursorWrap implementation.
|
||||
|
||||
## Purpose
|
||||
|
||||
This tool helps you:
|
||||
- Visualize your multi-monitor setup
|
||||
- Identify which screen edges are "outer edges" (edges that don't connect to another monitor)
|
||||
- See where cursor wrapping will occur when you move the cursor to an outer edge
|
||||
- **Find problem areas** where edges have NO wrap destination (shown in red)
|
||||
|
||||
## Requirements
|
||||
|
||||
- Python 3.6+
|
||||
- Tkinter (included with standard Python on Windows)
|
||||
|
||||
## Usage
|
||||
|
||||
### Command Line
|
||||
|
||||
```bash
|
||||
python wrap_simulator.py <path_to_monitor_layout.json>
|
||||
```
|
||||
|
||||
### Without Arguments
|
||||
|
||||
```bash
|
||||
python wrap_simulator.py
|
||||
```
|
||||
|
||||
This opens the application with no layout loaded. Use the "Load JSON" button to select a file.
|
||||
|
||||
## JSON File Format
|
||||
|
||||
The monitor layout JSON file should have this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"captured_at": "2026-02-16T08:50:34+00:00",
|
||||
"computer_name": "MY-PC",
|
||||
"user_name": "User",
|
||||
"monitor_count": 3,
|
||||
"monitors": [
|
||||
{
|
||||
"left": 0,
|
||||
"top": 0,
|
||||
"right": 2560,
|
||||
"bottom": 1440,
|
||||
"width": 2560,
|
||||
"height": 1440,
|
||||
"dpi": 96,
|
||||
"scaling_percent": 100.0,
|
||||
"primary": true,
|
||||
"device_name": "DISPLAY1"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
## Understanding the Visualization
|
||||
|
||||
### Monitor Display
|
||||
- **Gray rectangles**: Individual monitors
|
||||
- **Orange border**: Primary monitor
|
||||
- **Labels**: Show monitor index, device name, and resolution
|
||||
|
||||
### Edge Bars (Outside Monitor Boundaries)
|
||||
|
||||
Colored bars are drawn outside each **outer edge** (edges not adjacent to another monitor):
|
||||
|
||||
| Color | Meaning |
|
||||
|-------|---------|
|
||||
| **Yellow** | Edge segment has a wrap destination ✓ |
|
||||
| **Red with stripes** | NO wrap destination - Problem area! ⚠️ |
|
||||
|
||||
The bar outline color indicates the edge type:
|
||||
- Red = Left edge
|
||||
- Teal = Right edge
|
||||
- Blue = Top edge
|
||||
- Green = Bottom edge
|
||||
|
||||
### Interactive Features
|
||||
|
||||
1. **Hover over edge segments**:
|
||||
- See wrap destination info in the status bar
|
||||
- Green arrow shows where the cursor would wrap to
|
||||
- Green dashed rectangle highlights the destination
|
||||
|
||||
2. **Click on edge segments**:
|
||||
- Detailed information appears in the info panel
|
||||
- Shows full problem analysis with reason codes
|
||||
- Explains why wrapping does/doesn't occur
|
||||
- Provides suggestions for fixing problems
|
||||
|
||||
|
||||
3. **Wrap Mode Selection**:
|
||||
- **Both**: Wrap in all directions (default)
|
||||
- **Vertical Only**: Only top/bottom edges wrap
|
||||
- **Horizontal Only**: Only left/right edges wrap
|
||||
|
||||
4. **Export Analysis**:
|
||||
- Click "Export Analysis" to save detailed diagnostic data
|
||||
- Exports to JSON format for use in algorithm development
|
||||
- Includes all problem segments with reason codes and suggestions
|
||||
|
||||
5. **Edge Test Simulation** (NEW):
|
||||
- Click "🧪 Test Edges" to start automated edge testing
|
||||
- Visually animates cursor movement along ALL outer edges
|
||||
- Shows wrap destination for each test point with colored lines:
|
||||
- **Red circle**: Source position on outer edge
|
||||
- **Green circle**: Wrap destination
|
||||
- **Green dashed line**: Connection showing wrap path
|
||||
- **Red X**: No wrap destination (problem area)
|
||||
- Use "New Algorithm" checkbox to toggle between:
|
||||
- **NEW**: Projection-based algorithm (eliminates dead zones)
|
||||
- **OLD**: Direct overlap only (may have dead zones)
|
||||
- Results summary shows per-edge coverage statistics
|
||||
|
||||
## Problem Analysis
|
||||
|
||||
When a segment has no wrap destination, the tool provides detailed analysis:
|
||||
|
||||
### Problem Reason Codes
|
||||
|
||||
| Code | Description |
|
||||
|------|-------------|
|
||||
| `WRAP_MODE_DISABLED` | Edge type disabled by current wrap mode setting |
|
||||
| `NO_OPPOSITE_OUTER_EDGES` | No outer edges of the opposite type exist at all |
|
||||
| `NO_OVERLAPPING_RANGE` | Opposite edges exist but don't cover this coordinate range |
|
||||
| `SINGLE_MONITOR` | Only one monitor - nowhere to wrap to |
|
||||
|
||||
### Diagnostic Details
|
||||
|
||||
For `NO_OVERLAPPING_RANGE` problems, the tool shows:
|
||||
- Distance to the nearest valid wrap destination
|
||||
- List of available opposite edges sorted by distance
|
||||
- Whether the gap is above/below or left/right of the segment
|
||||
- Suggested fixes (extend monitors or adjust positions)
|
||||
|
||||
## Sample Files
|
||||
|
||||
Included sample layouts:
|
||||
|
||||
- `sample_layout.json` - 3 monitors in a row with one offset
|
||||
- `sample_staggered.json` - 3 monitors with staggered vertical positions (shows problem areas)
|
||||
- `sample_with_gap.json` - 2 monitors with a gap between them
|
||||
|
||||
## Exported Analysis Format
|
||||
|
||||
The "Export Analysis" button generates a JSON file with this structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"export_timestamp": "2026-02-16T08:50:34+00:00",
|
||||
"wrap_mode": "BOTH",
|
||||
"monitor_count": 3,
|
||||
"monitors": [...],
|
||||
"outer_edges": [...],
|
||||
"problem_segments": [
|
||||
{
|
||||
"source": {
|
||||
"monitor_index": 0,
|
||||
"monitor_name": "DISPLAY1",
|
||||
"edge_type": "TOP",
|
||||
"edge_position": 200,
|
||||
"segment_range": {"start": 0, "end": 200},
|
||||
"segment_length_px": 200
|
||||
},
|
||||
"analysis": {
|
||||
"reason_code": "NO_OVERLAPPING_RANGE",
|
||||
"description": "No BOTTOM outer edge overlaps...",
|
||||
"suggestion": "To fix: Either extend...",
|
||||
"details": {
|
||||
"gap_to_nearest": 200,
|
||||
"available_opposite_edges": [...]
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"total_outer_edges": 8,
|
||||
"total_problem_segments": 4,
|
||||
"total_problem_pixels": 800,
|
||||
"problems_by_reason": {"NO_OVERLAPPING_RANGE": 4},
|
||||
"has_problems": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## How CursorWrap Logic Works
|
||||
|
||||
### Original Algorithm (v1)
|
||||
|
||||
1. **Outer Edge Detection**: An edge is "outer" if no other monitor's opposite edge is within 50 pixels AND has sufficient vertical/horizontal overlap
|
||||
|
||||
2. **Wrap Destination**: When cursor reaches an outer edge:
|
||||
- Find the opposite type outer edge (Left→Right, Top→Bottom, etc.)
|
||||
- The destination must overlap with the cursor's perpendicular position
|
||||
- Cursor warps to the furthest matching outer edge
|
||||
|
||||
3. **Problem Areas**: If no opposite outer edge overlaps with a portion of an outer edge, that segment has no wrap destination - the cursor will simply stop at that edge.
|
||||
|
||||
### Enhanced Algorithm (v2) - With Projection
|
||||
|
||||
The enhanced algorithm eliminates dead zones by projecting cursor positions to valid destinations:
|
||||
|
||||
1. **Direct Overlap**: If an opposite outer edge directly overlaps the cursor's perpendicular coordinate, use it (same as v1)
|
||||
|
||||
2. **Nearest Edge Projection**: If no direct overlap exists:
|
||||
- Find the nearest opposite outer edge by coordinate distance
|
||||
- Calculate a projected position using offset-from-boundary approach
|
||||
- The projection preserves relative position similar to how Windows handles monitor transitions
|
||||
|
||||
3. **No Dead Zones**: Every point on every outer edge will have a valid wrap destination
|
||||
|
||||
### Testing the Algorithm
|
||||
|
||||
Use the included test script to validate both algorithms:
|
||||
|
||||
```bash
|
||||
python test_new_algorithm.py [layout_file.json]
|
||||
```
|
||||
|
||||
This compares the old algorithm (with dead zones) against the new algorithm (with projection) and reports coverage.
|
||||
|
||||
## Cursor Log Playback
|
||||
|
||||
The simulator can play back recorded cursor movement logs to visualize how the cursor moves across monitors.
|
||||
|
||||
### Loading a Cursor Log
|
||||
|
||||
1. Click "Load Log" to select a cursor movement log file
|
||||
2. Use the playback controls:
|
||||
- **▶ Play / ⏸ Pause**: Start or pause playback
|
||||
- **⏹ Stop**: Stop and reset to beginning
|
||||
- **⏮ Reset**: Reset to beginning without stopping
|
||||
- **Speed slider**: Adjust playback speed (10-500ms between frames)
|
||||
|
||||
### Log File Format
|
||||
|
||||
The cursor log file is CSV format with the following columns:
|
||||
|
||||
```
|
||||
display_name,x,y,dpi,scaling%
|
||||
```
|
||||
|
||||
Example:
|
||||
```csv
|
||||
\\.\DISPLAY1,1234,567,96,100%
|
||||
\\.\DISPLAY2,2560,720,144,150%
|
||||
\\.\DISPLAY3,-500,800,96,100%
|
||||
```
|
||||
|
||||
- **display_name**: Windows display name (e.g., `\\.\DISPLAY1`)
|
||||
- **x, y**: Screen coordinates
|
||||
- **dpi**: Display DPI
|
||||
- **scaling%**: Display scaling percentage (with or without % sign)
|
||||
|
||||
Lines starting with `#` are treated as comments and ignored.
|
||||
|
||||
### Playback Visualization
|
||||
|
||||
- **Green cursor**: Normal movement within a monitor
|
||||
- **Red cursor with burst effect**: Monitor transition detected
|
||||
- **Blue trail**: Recent cursor movement path (fades over time)
|
||||
- **Dashed red arrow**: Shows transition path between monitors
|
||||
|
||||
The playback automatically slows down when a monitor transition is detected, making it easier to observe wrap behavior.
|
||||
|
||||
### Sample Log File
|
||||
|
||||
A sample cursor log file `sample_cursor_log.csv` is included that demonstrates cursor movement across a three-monitor setup.
|
||||
|
||||
## Architecture
|
||||
|
||||
The Python implementation mirrors the C++ code structure:
|
||||
|
||||
- `MonitorTopology` class: Manages edge-based monitor layout
|
||||
- `MonitorEdge` dataclass: Represents a single edge of a monitor
|
||||
- `EdgeSegment` dataclass: A portion of an edge with wrap info
|
||||
- `CursorLogEntry` dataclass: A single cursor movement log entry
|
||||
- `WrapSimulatorApp`: Tkinter GUI application
|
||||
|
||||
## Integration with PowerToys
|
||||
|
||||
This tool is designed to validate and debug the CursorWrap feature. The JSON files can be generated by the debug build of CursorWrap or created manually for testing specific configurations.
|
||||
@@ -0,0 +1,110 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Test script to validate the new projection-based wrapping algorithm.
|
||||
"""
|
||||
|
||||
import json
|
||||
import sys
|
||||
from wrap_simulator import MonitorTopology, MonitorInfo, WrapMode
|
||||
|
||||
def test_layout(layout_file: str):
|
||||
"""Test a monitor layout with both old and new algorithms."""
|
||||
|
||||
# Load the layout
|
||||
with open(layout_file, 'r') as f:
|
||||
layout = json.load(f)
|
||||
|
||||
# Create monitor info objects
|
||||
monitors = []
|
||||
for i, m in enumerate(layout['monitors']):
|
||||
monitors.append(MonitorInfo(
|
||||
left=m['left'], top=m['top'], right=m['right'], bottom=m['bottom'],
|
||||
width=m['width'], height=m['height'], dpi=m.get('dpi', 96),
|
||||
scaling_percent=m.get('scaling_percent', 100), primary=m.get('primary', False),
|
||||
device_name=m.get('device_name', f'DISPLAY{i+1}'), monitor_id=i
|
||||
))
|
||||
|
||||
# Initialize topology
|
||||
topology = MonitorTopology()
|
||||
topology.initialize(monitors)
|
||||
|
||||
print(f"Layout: {layout_file}")
|
||||
print(f"Monitors: {len(monitors)}")
|
||||
print(f"Outer edges: {len(topology.outer_edges)}")
|
||||
|
||||
# Validate with OLD algorithm
|
||||
print("\n--- OLD Algorithm (may have dead zones) ---")
|
||||
old_problems = 0
|
||||
old_problem_details = []
|
||||
for edge in topology.outer_edges:
|
||||
segments = topology.get_edge_segments_with_wrap_info(edge, WrapMode.BOTH)
|
||||
for seg in segments:
|
||||
if not seg.has_wrap_destination:
|
||||
length = seg.end - seg.start
|
||||
old_problems += length
|
||||
detail = f"Mon {edge.monitor_index} {edge.edge_type.name} [{seg.start}-{seg.end}] ({length}px)"
|
||||
old_problem_details.append(detail)
|
||||
print(f" PROBLEM: {detail}")
|
||||
print(f"Total problematic pixels: {old_problems}")
|
||||
|
||||
# Validate with NEW algorithm
|
||||
print("\n--- NEW Algorithm (with projection) ---")
|
||||
result = topology.validate_all_edges_have_destinations(WrapMode.BOTH)
|
||||
print(f"Total edge length: {result['total_edge_length']}px")
|
||||
print(f"Covered: {result['covered_length']}px ({result['coverage_percent']:.1f}%)")
|
||||
print(f"Uncovered: {result['uncovered_length']}px")
|
||||
print(f"Fully covered: {result['is_fully_covered']}")
|
||||
|
||||
if result['problem_areas']:
|
||||
for prob in result['problem_areas']:
|
||||
print(f" PROBLEM: {prob}")
|
||||
|
||||
# Summary
|
||||
print("\n--- COMPARISON ---")
|
||||
print(f"Old algorithm dead zones: {old_problems}px")
|
||||
print(f"New algorithm dead zones: {result['uncovered_length']}px")
|
||||
if old_problems > 0 and result['uncovered_length'] == 0:
|
||||
print("SUCCESS: New algorithm eliminates all dead zones!")
|
||||
elif result['uncovered_length'] > 0:
|
||||
print("WARNING: New algorithm still has dead zones")
|
||||
else:
|
||||
print("Both algorithms have no dead zones for this layout")
|
||||
|
||||
return result['is_fully_covered']
|
||||
|
||||
|
||||
def main():
|
||||
layout_files = [
|
||||
'mikehall_monitor_layout.json',
|
||||
'sample_layout.json',
|
||||
'sample_staggered.json',
|
||||
]
|
||||
|
||||
# Allow specifying layout on command line
|
||||
if len(sys.argv) > 1:
|
||||
layout_files = sys.argv[1:]
|
||||
|
||||
all_passed = True
|
||||
for layout_file in layout_files:
|
||||
try:
|
||||
print(f"\n{'='*60}")
|
||||
passed = test_layout(layout_file)
|
||||
if not passed:
|
||||
all_passed = False
|
||||
except FileNotFoundError:
|
||||
print(f"File not found: {layout_file}")
|
||||
except Exception as e:
|
||||
print(f"Error testing {layout_file}: {e}")
|
||||
all_passed = False
|
||||
|
||||
print(f"\n{'='*60}")
|
||||
if all_passed:
|
||||
print("ALL TESTS PASSED")
|
||||
else:
|
||||
print("SOME TESTS FAILED")
|
||||
|
||||
return 0 if all_passed else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,6 +4,7 @@
|
||||
|
||||
#include "pch.h"
|
||||
#include "MonitorTopology.h"
|
||||
#include "CursorWrapCore.h" // For CursorDirection struct
|
||||
#include "../../../common/logger/logger.h"
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
@@ -13,6 +14,7 @@ void MonitorTopology::Initialize(const std::vector<MonitorInfo>& monitors)
|
||||
Logger::info(L"======= TOPOLOGY INITIALIZATION START =======");
|
||||
Logger::info(L"Initializing edge-based topology for {} monitors", monitors.size());
|
||||
|
||||
|
||||
m_monitors = monitors;
|
||||
m_outerEdges.clear();
|
||||
m_edgeMap.clear();
|
||||
@@ -163,10 +165,80 @@ bool MonitorTopology::EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEd
|
||||
int overlapStart = max(edge1.start, edge2.start);
|
||||
int overlapEnd = min(edge1.end, edge2.end);
|
||||
|
||||
|
||||
return overlapEnd > overlapStart + tolerance;
|
||||
}
|
||||
|
||||
bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const
|
||||
EdgeType MonitorTopology::PrioritizeEdgeByDirection(const std::vector<EdgeType>& candidates,
|
||||
const CursorDirection* direction) const
|
||||
{
|
||||
if (candidates.empty())
|
||||
{
|
||||
return EdgeType::Left; // Should not happen, but return a default
|
||||
}
|
||||
|
||||
if (candidates.size() == 1 || direction == nullptr)
|
||||
{
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
// Prioritize based on movement direction
|
||||
// If moving primarily horizontally, prefer horizontal edges (Left/Right)
|
||||
// If moving primarily vertically, prefer vertical edges (Top/Bottom)
|
||||
|
||||
if (direction->IsPrimarilyHorizontal())
|
||||
{
|
||||
// Prefer Left if moving left, Right if moving right
|
||||
if (direction->IsMovingLeft())
|
||||
{
|
||||
for (EdgeType edge : candidates)
|
||||
{
|
||||
if (edge == EdgeType::Left) return edge;
|
||||
}
|
||||
}
|
||||
else if (direction->IsMovingRight())
|
||||
{
|
||||
for (EdgeType edge : candidates)
|
||||
{
|
||||
if (edge == EdgeType::Right) return edge;
|
||||
}
|
||||
}
|
||||
// Fall back to any horizontal edge
|
||||
for (EdgeType edge : candidates)
|
||||
{
|
||||
if (edge == EdgeType::Left || edge == EdgeType::Right) return edge;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Prefer Top if moving up, Bottom if moving down
|
||||
if (direction->IsMovingUp())
|
||||
{
|
||||
for (EdgeType edge : candidates)
|
||||
{
|
||||
if (edge == EdgeType::Top) return edge;
|
||||
}
|
||||
}
|
||||
else if (direction->IsMovingDown())
|
||||
{
|
||||
for (EdgeType edge : candidates)
|
||||
{
|
||||
if (edge == EdgeType::Bottom) return edge;
|
||||
}
|
||||
}
|
||||
// Fall back to any vertical edge
|
||||
for (EdgeType edge : candidates)
|
||||
{
|
||||
if (edge == EdgeType::Top || edge == EdgeType::Bottom) return edge;
|
||||
}
|
||||
}
|
||||
|
||||
// Default to first candidate
|
||||
return candidates[0];
|
||||
}
|
||||
|
||||
bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType,
|
||||
WrapMode wrapMode, const CursorDirection* direction) const
|
||||
{
|
||||
RECT monitorRect;
|
||||
if (!GetMonitorRect(monitor, monitorRect))
|
||||
@@ -248,13 +320,40 @@ bool MonitorTopology::IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, Ed
|
||||
return false;
|
||||
}
|
||||
|
||||
// Try each candidate edge and return first with valid wrap destination
|
||||
// Prioritize candidates by movement direction at corners
|
||||
EdgeType prioritizedEdge = PrioritizeEdgeByDirection(candidateEdges, direction);
|
||||
|
||||
// Get the source edge info
|
||||
auto sourceIt = m_edgeMap.find({monitorIndex, prioritizedEdge});
|
||||
if (sourceIt == m_edgeMap.end())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Use the new FindNearestOppositeEdge which handles non-overlapping regions
|
||||
int cursorCoord = (prioritizedEdge == EdgeType::Left || prioritizedEdge == EdgeType::Right)
|
||||
? cursorPos.y : cursorPos.x;
|
||||
OppositeEdgeResult result = FindNearestOppositeEdge(prioritizedEdge, cursorCoord, sourceIt->second);
|
||||
|
||||
if (result.found)
|
||||
{
|
||||
outEdgeType = prioritizedEdge;
|
||||
return true;
|
||||
}
|
||||
|
||||
// If prioritized edge didn't work, try other candidates
|
||||
for (EdgeType candidate : candidateEdges)
|
||||
{
|
||||
MonitorEdge oppositeEdge = FindOppositeOuterEdge(candidate,
|
||||
(candidate == EdgeType::Left || candidate == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||
if (candidate == prioritizedEdge) continue;
|
||||
|
||||
if (oppositeEdge.monitorIndex >= 0)
|
||||
auto it = m_edgeMap.find({monitorIndex, candidate});
|
||||
if (it == m_edgeMap.end()) continue;
|
||||
|
||||
int coord = (candidate == EdgeType::Left || candidate == EdgeType::Right)
|
||||
? cursorPos.y : cursorPos.x;
|
||||
OppositeEdgeResult altResult = FindNearestOppositeEdge(candidate, coord, it->second);
|
||||
|
||||
if (altResult.found)
|
||||
{
|
||||
outEdgeType = candidate;
|
||||
return true;
|
||||
@@ -281,15 +380,13 @@ POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cur
|
||||
|
||||
const MonitorEdge& fromEdge = it->second;
|
||||
|
||||
// Calculate relative position on current edge (0.0 to 1.0)
|
||||
double relativePos = GetRelativePosition(fromEdge,
|
||||
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||
// Get cursor coordinate perpendicular to the edge
|
||||
int cursorCoord = (edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x;
|
||||
|
||||
// Find opposite outer edge
|
||||
MonitorEdge oppositeEdge = FindOppositeOuterEdge(edgeType,
|
||||
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||
// Use the new FindNearestOppositeEdge which handles non-overlapping regions
|
||||
OppositeEdgeResult oppositeResult = FindNearestOppositeEdge(edgeType, cursorCoord, fromEdge);
|
||||
|
||||
if (oppositeEdge.monitorIndex < 0)
|
||||
if (!oppositeResult.found)
|
||||
{
|
||||
// No opposite edge found, wrap within same monitor
|
||||
RECT monitorRect;
|
||||
@@ -321,15 +418,35 @@ POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cur
|
||||
|
||||
if (edgeType == EdgeType::Left || edgeType == EdgeType::Right)
|
||||
{
|
||||
// Horizontal edge -> vertical movement
|
||||
result.x = oppositeEdge.position;
|
||||
result.y = GetAbsolutePosition(oppositeEdge, relativePos);
|
||||
// Horizontal wrapping (Left<->Right edges)
|
||||
result.x = oppositeResult.edge.position;
|
||||
|
||||
if (oppositeResult.requiresProjection)
|
||||
{
|
||||
// Use the pre-calculated projected coordinate for non-overlapping regions
|
||||
result.y = oppositeResult.projectedCoordinate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Overlapping region - preserve Y coordinate
|
||||
result.y = cursorPos.y;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Vertical edge -> horizontal movement
|
||||
result.y = oppositeEdge.position;
|
||||
result.x = GetAbsolutePosition(oppositeEdge, relativePos);
|
||||
// Vertical wrapping (Top<->Bottom edges)
|
||||
result.y = oppositeResult.edge.position;
|
||||
|
||||
if (oppositeResult.requiresProjection)
|
||||
{
|
||||
// Use the pre-calculated projected coordinate for non-overlapping regions
|
||||
result.x = oppositeResult.projectedCoordinate;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Overlapping region - preserve X coordinate
|
||||
result.x = cursorPos.x;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
@@ -387,6 +504,170 @@ MonitorEdge MonitorTopology::FindOppositeOuterEdge(EdgeType fromEdge, int relati
|
||||
return result;
|
||||
}
|
||||
|
||||
OppositeEdgeResult MonitorTopology::FindNearestOppositeEdge(EdgeType fromEdge, int cursorCoordinate,
|
||||
const MonitorEdge& sourceEdge) const
|
||||
{
|
||||
OppositeEdgeResult result;
|
||||
result.found = false;
|
||||
result.requiresProjection = false;
|
||||
result.projectedCoordinate = 0;
|
||||
result.edge.monitorIndex = -1;
|
||||
|
||||
EdgeType targetType;
|
||||
bool findMax; // true = find max position (furthest right/bottom), false = find min (furthest left/top)
|
||||
|
||||
switch (fromEdge)
|
||||
{
|
||||
case EdgeType::Left:
|
||||
targetType = EdgeType::Right;
|
||||
findMax = true;
|
||||
break;
|
||||
case EdgeType::Right:
|
||||
targetType = EdgeType::Left;
|
||||
findMax = false;
|
||||
break;
|
||||
case EdgeType::Top:
|
||||
targetType = EdgeType::Bottom;
|
||||
findMax = true;
|
||||
break;
|
||||
case EdgeType::Bottom:
|
||||
targetType = EdgeType::Top;
|
||||
findMax = false;
|
||||
break;
|
||||
default:
|
||||
return result; // Invalid edge type
|
||||
}
|
||||
|
||||
// First, try to find an edge that directly overlaps the cursor coordinate
|
||||
MonitorEdge directMatch = FindOppositeOuterEdge(fromEdge, cursorCoordinate);
|
||||
if (directMatch.monitorIndex >= 0)
|
||||
{
|
||||
result.found = true;
|
||||
result.requiresProjection = false;
|
||||
result.edge = directMatch;
|
||||
result.projectedCoordinate = cursorCoordinate; // Not used, but set for completeness
|
||||
return result;
|
||||
}
|
||||
|
||||
// No direct overlap - find the nearest opposite edge by coordinate distance
|
||||
// This handles the "dead zone" case where cursor is in a non-overlapping region
|
||||
|
||||
int bestDistance = INT_MAX;
|
||||
MonitorEdge bestEdge = { .monitorIndex = -1 };
|
||||
int bestProjectedCoord = 0;
|
||||
|
||||
for (const auto& edge : m_outerEdges)
|
||||
{
|
||||
if (edge.type != targetType)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Calculate distance from cursor coordinate to this edge's range
|
||||
int distance = 0;
|
||||
int projectedCoord = 0;
|
||||
|
||||
if (cursorCoordinate < edge.start)
|
||||
{
|
||||
// Cursor is before the edge's start - project to edge start with offset
|
||||
distance = edge.start - cursorCoordinate;
|
||||
projectedCoord = edge.start; // Clamp to edge start
|
||||
}
|
||||
else if (cursorCoordinate > edge.end)
|
||||
{
|
||||
// Cursor is after the edge's end - project to edge end with offset
|
||||
distance = cursorCoordinate - edge.end;
|
||||
projectedCoord = edge.end; // Clamp to edge end
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cursor overlaps - this shouldn't happen since we checked direct match
|
||||
distance = 0;
|
||||
projectedCoord = cursorCoordinate;
|
||||
}
|
||||
|
||||
// Choose the best edge: prefer closer edges, and among equals prefer extreme position
|
||||
bool isBetter = false;
|
||||
if (distance < bestDistance)
|
||||
{
|
||||
isBetter = true;
|
||||
}
|
||||
else if (distance == bestDistance && bestEdge.monitorIndex >= 0)
|
||||
{
|
||||
// Same distance - prefer the extreme position (furthest in wrap direction)
|
||||
if ((findMax && edge.position > bestEdge.position) ||
|
||||
(!findMax && edge.position < bestEdge.position))
|
||||
{
|
||||
isBetter = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (isBetter)
|
||||
{
|
||||
bestDistance = distance;
|
||||
bestEdge = edge;
|
||||
bestProjectedCoord = projectedCoord;
|
||||
}
|
||||
}
|
||||
|
||||
if (bestEdge.monitorIndex >= 0)
|
||||
{
|
||||
result.found = true;
|
||||
result.requiresProjection = true;
|
||||
result.edge = bestEdge;
|
||||
|
||||
// Calculate projected position using offset-from-boundary approach
|
||||
result.projectedCoordinate = CalculateProjectedPosition(cursorCoordinate, sourceEdge, bestEdge);
|
||||
|
||||
Logger::trace(L"FindNearestOppositeEdge: Non-overlapping wrap from {} to Mon {} edge, cursor={}, projected={}",
|
||||
static_cast<int>(fromEdge), bestEdge.monitorIndex, cursorCoordinate, result.projectedCoordinate);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
int MonitorTopology::CalculateProjectedPosition(int cursorCoordinate, const MonitorEdge& sourceEdge,
|
||||
const MonitorEdge& targetEdge) const
|
||||
{
|
||||
// Windows behavior for non-overlapping regions:
|
||||
// When cursor is in a region that doesn't overlap with the target edge,
|
||||
// clamp to the nearest boundary of the target edge.
|
||||
// This matches observed Windows cursor transition behavior.
|
||||
|
||||
// Find the shared boundary region between source and target edges
|
||||
int sharedStart = max(sourceEdge.start, targetEdge.start);
|
||||
int sharedEnd = min(sourceEdge.end, targetEdge.end);
|
||||
|
||||
if (cursorCoordinate >= sharedStart && cursorCoordinate <= sharedEnd)
|
||||
{
|
||||
// Cursor is in shared region - preserve the coordinate exactly
|
||||
return cursorCoordinate;
|
||||
}
|
||||
|
||||
// For non-overlapping regions, clamp to the nearest boundary of the target edge
|
||||
// This matches Windows behavior where the cursor is projected to the closest
|
||||
// valid point on the destination edge
|
||||
int projectedCoord;
|
||||
|
||||
if (cursorCoordinate < sharedStart)
|
||||
{
|
||||
// Cursor is BEFORE the shared region (e.g., above shared area)
|
||||
// Clamp to the start of the target edge (with small offset to stay within bounds)
|
||||
projectedCoord = targetEdge.start + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Cursor is AFTER the shared region (e.g., below shared area)
|
||||
// Clamp to the end of the target edge (with small offset to stay within bounds)
|
||||
projectedCoord = targetEdge.end - 1;
|
||||
}
|
||||
|
||||
// Final bounds check
|
||||
projectedCoord = max(targetEdge.start, min(projectedCoord, targetEdge.end - 1));
|
||||
|
||||
return projectedCoord;
|
||||
}
|
||||
|
||||
double MonitorTopology::GetRelativePosition(const MonitorEdge& edge, int coordinate) const
|
||||
{
|
||||
if (edge.end == edge.start)
|
||||
@@ -411,6 +692,7 @@ int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativ
|
||||
return static_cast<int>(result);
|
||||
}
|
||||
|
||||
|
||||
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
|
||||
{
|
||||
std::vector<GapInfo> gaps;
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
#include <vector>
|
||||
#include <map>
|
||||
|
||||
// Forward declaration
|
||||
struct CursorDirection;
|
||||
|
||||
// Monitor information structure
|
||||
struct MonitorInfo
|
||||
{
|
||||
@@ -44,6 +47,15 @@ struct MonitorEdge
|
||||
bool isOuter; // True if no adjacent monitor touches this edge
|
||||
};
|
||||
|
||||
// Result of finding an opposite edge, including projection info for non-overlapping regions
|
||||
struct OppositeEdgeResult
|
||||
{
|
||||
MonitorEdge edge;
|
||||
bool found; // True if an opposite edge was found
|
||||
bool requiresProjection; // True if cursor position needs to be projected (non-overlapping region)
|
||||
int projectedCoordinate; // The calculated coordinate on the target edge
|
||||
};
|
||||
|
||||
// Monitor topology helper - manages edge-based monitor layout
|
||||
struct MonitorTopology
|
||||
{
|
||||
@@ -51,7 +63,9 @@ struct MonitorTopology
|
||||
|
||||
// Check if cursor is on an outer edge of the given monitor
|
||||
// wrapMode filters which edges are considered (Both, VerticalOnly, HorizontalOnly)
|
||||
bool IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType, WrapMode wrapMode) const;
|
||||
// direction is used to prioritize edges at corners based on cursor movement
|
||||
bool IsOnOuterEdge(HMONITOR monitor, const POINT& cursorPos, EdgeType& outEdgeType,
|
||||
WrapMode wrapMode, const CursorDirection* direction = nullptr) const;
|
||||
|
||||
// Get the wrap destination point for a cursor on an outer edge
|
||||
POINT GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const;
|
||||
@@ -95,12 +109,26 @@ private:
|
||||
// Check if two edges are adjacent (within tolerance)
|
||||
bool EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance = 50) const;
|
||||
|
||||
// Find the opposite outer edge for wrapping
|
||||
// Find the opposite outer edge for wrapping (original method - for overlapping regions)
|
||||
MonitorEdge FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const;
|
||||
|
||||
// Find the nearest opposite outer edge, including projection for non-overlapping regions
|
||||
// This implements Windows-like behavior for cursor transitions
|
||||
OppositeEdgeResult FindNearestOppositeEdge(EdgeType fromEdge, int cursorCoordinate,
|
||||
const MonitorEdge& sourceEdge) const;
|
||||
|
||||
// Calculate projected position for cursor in non-overlapping region
|
||||
// Returns the coordinate on the destination edge using offset-from-boundary approach
|
||||
int CalculateProjectedPosition(int cursorCoordinate, const MonitorEdge& sourceEdge,
|
||||
const MonitorEdge& targetEdge) const;
|
||||
|
||||
// Calculate relative position along an edge (0.0 to 1.0)
|
||||
double GetRelativePosition(const MonitorEdge& edge, int coordinate) const;
|
||||
|
||||
// Convert relative position to absolute coordinate on target edge
|
||||
int GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const;
|
||||
|
||||
// Prioritize edge candidates based on cursor movement direction
|
||||
EdgeType PrioritizeEdgeByDirection(const std::vector<EdgeType>& candidates,
|
||||
const CursorDirection* direction) const;
|
||||
};
|
||||
|
||||
@@ -54,6 +54,7 @@ namespace
|
||||
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_ACTIVATION_MODE[] = L"activation_mode";
|
||||
const wchar_t JSON_KEY_DISABLE_ON_SINGLE_MONITOR[] = L"disable_cursor_wrap_on_single_monitor";
|
||||
}
|
||||
|
||||
@@ -83,6 +84,7 @@ private:
|
||||
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
|
||||
int m_activationMode = 0; // 0=Always (default), 1=HoldingCtrl (disables wrap), 2=HoldingShift (disables wrap)
|
||||
|
||||
// Mouse hook
|
||||
HHOOK m_mouseHook = nullptr;
|
||||
@@ -430,6 +432,21 @@ private:
|
||||
Logger::warn("Failed to initialize CursorWrap wrap mode from settings. Will use default value (0=Both)");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Parse activation mode
|
||||
auto propertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES);
|
||||
if (propertiesObject.HasKey(JSON_KEY_ACTIVATION_MODE))
|
||||
{
|
||||
auto activationModeObject = propertiesObject.GetNamedObject(JSON_KEY_ACTIVATION_MODE);
|
||||
m_activationMode = static_cast<int>(activationModeObject.GetNamedNumber(JSON_KEY_VALUE));
|
||||
}
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize CursorWrap activation mode from settings. Will use default value (0=Always)");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Parse disable on single monitor
|
||||
@@ -672,6 +689,26 @@ private:
|
||||
|
||||
if (g_cursorWrapInstance && g_cursorWrapInstance->m_hookActive)
|
||||
{
|
||||
// Check activation mode to determine if wrapping should be disabled
|
||||
// 0=Always, 1=HoldingCtrl (disables wrap when Ctrl held), 2=HoldingShift (disables wrap when Shift held)
|
||||
int activationMode = g_cursorWrapInstance->m_activationMode;
|
||||
bool disableByKey = false;
|
||||
|
||||
if (activationMode == 1) // HoldingCtrl - disable wrap when Ctrl is held
|
||||
{
|
||||
disableByKey = (GetAsyncKeyState(VK_CONTROL) & 0x8000) != 0;
|
||||
}
|
||||
else if (activationMode == 2) // HoldingShift - disable wrap when Shift is held
|
||||
{
|
||||
disableByKey = (GetAsyncKeyState(VK_SHIFT) & 0x8000) != 0;
|
||||
}
|
||||
|
||||
if (disableByKey)
|
||||
{
|
||||
// Key is held, do not wrap - let normal behavior happen
|
||||
return CallNextHookEx(nullptr, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
POINT newPos = g_cursorWrapInstance->m_core.HandleMouseMove(
|
||||
currentPos,
|
||||
g_cursorWrapInstance->m_disableWrapDuringDrag,
|
||||
|
||||
@@ -25,6 +25,9 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
[JsonPropertyName("wrap_mode")]
|
||||
public IntProperty WrapMode { get; set; }
|
||||
|
||||
[JsonPropertyName("activation_mode")]
|
||||
public IntProperty ActivationMode { get; set; }
|
||||
|
||||
[JsonPropertyName("disable_cursor_wrap_on_single_monitor")]
|
||||
public BoolProperty DisableCursorWrapOnSingleMonitor { get; set; }
|
||||
|
||||
@@ -34,6 +37,7 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
AutoActivate = new BoolProperty(false);
|
||||
DisableWrapDuringDrag = new BoolProperty(true);
|
||||
WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
|
||||
ActivationMode = new IntProperty(0); // 0=Always (default), 1=HoldingCtrl, 2=HoldingShift
|
||||
DisableCursorWrapOnSingleMonitor = new BoolProperty(false);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,6 +56,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
settingsUpgraded = true;
|
||||
}
|
||||
|
||||
// Add ActivationMode property if it doesn't exist (for users upgrading from older versions)
|
||||
if (Properties.ActivationMode == null)
|
||||
{
|
||||
Properties.ActivationMode = new IntProperty(0); // Default to Always (0=Always, 1=HoldingCtrl, 2=HoldingShift)
|
||||
settingsUpgraded = true;
|
||||
}
|
||||
|
||||
// Add DisableCursorWrapOnSingleMonitor property if it doesn't exist (for users upgrading from older versions)
|
||||
if (Properties.DisableCursorWrapOnSingleMonitor == null)
|
||||
{
|
||||
|
||||
@@ -54,6 +54,13 @@
|
||||
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_HorizontalOnly" />
|
||||
</ComboBox>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard Name="MouseUtilsCursorWrapActivationMode" x:Uid="MouseUtils_CursorWrap_ActivationMode">
|
||||
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.CursorWrapActivationMode, Mode=TwoWay}">
|
||||
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_ActivationMode_Always" />
|
||||
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_ActivationMode_HoldingCtrl" />
|
||||
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_ActivationMode_HoldingShift" />
|
||||
</ComboBox>
|
||||
</tkcontrols:SettingsCard>
|
||||
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
|
||||
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableOnSingleMonitor" IsChecked="{x:Bind ViewModel.CursorWrapDisableOnSingleMonitor, Mode=TwoWay}" />
|
||||
</tkcontrols:SettingsCard>
|
||||
|
||||
@@ -2504,6 +2504,26 @@ From there, simply click on one of the supported files in the File Explorer and
|
||||
<data name="MouseUtils_CursorWrap_WrapMode_Both.Content" xml:space="preserve">
|
||||
<value>Vertical and horizontal</value>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_ActivationMode.Header" xml:space="preserve">
|
||||
<value>Wrapping activation</value>
|
||||
<comment>CursorWrap: Label for activation mode dropdown</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_ActivationMode.Description" xml:space="preserve">
|
||||
<value>Control when cursor wrapping occurs as the pointer reaches the screen edge.</value>
|
||||
<comment>CursorWrap: Description for activation mode dropdown</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_ActivationMode_Always.Content" xml:space="preserve">
|
||||
<value>Always</value>
|
||||
<comment>CursorWrap: Activation mode - always wrap</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_ActivationMode_HoldingCtrl.Content" xml:space="preserve">
|
||||
<value>Holding Ctrl</value>
|
||||
<comment>CursorWrap: Activation mode - disable wrap when Ctrl held</comment>
|
||||
</data>
|
||||
<data name="MouseUtils_CursorWrap_ActivationMode_HoldingShift.Content" xml:space="preserve">
|
||||
<value>Holding Shift</value>
|
||||
<comment>CursorWrap: Activation mode - disable wrap when Shift held</comment>
|
||||
</data>
|
||||
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Text" xml:space="preserve">
|
||||
<value>Mouse Pointer Crosshairs</value>
|
||||
<comment>Mouse as in the hardware peripheral.</comment>
|
||||
|
||||
@@ -116,6 +116,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
// Null-safe access in case property wasn't upgraded yet - default to 0 (Both)
|
||||
_cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0;
|
||||
|
||||
// Null-safe access in case property wasn't upgraded yet - default to 0 (Always)
|
||||
_cursorWrapActivationMode = CursorWrapSettingsConfig.Properties.ActivationMode?.Value ?? 0;
|
||||
|
||||
// Null-safe access in case property wasn't upgraded yet - default to false
|
||||
_cursorWrapDisableOnSingleMonitor = CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor?.Value ?? false;
|
||||
|
||||
@@ -1110,6 +1113,34 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
public int CursorWrapActivationMode
|
||||
{
|
||||
get
|
||||
{
|
||||
return _cursorWrapActivationMode;
|
||||
}
|
||||
|
||||
set
|
||||
{
|
||||
if (value != _cursorWrapActivationMode)
|
||||
{
|
||||
_cursorWrapActivationMode = value;
|
||||
|
||||
// Ensure the property exists before setting value
|
||||
if (CursorWrapSettingsConfig.Properties.ActivationMode == null)
|
||||
{
|
||||
CursorWrapSettingsConfig.Properties.ActivationMode = new IntProperty(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
CursorWrapSettingsConfig.Properties.ActivationMode.Value = value;
|
||||
}
|
||||
|
||||
NotifyCursorWrapPropertyChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool CursorWrapDisableOnSingleMonitor
|
||||
{
|
||||
get
|
||||
@@ -1210,6 +1241,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
||||
private bool _cursorWrapAutoActivate;
|
||||
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
|
||||
private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly
|
||||
private int _cursorWrapActivationMode; // 0=Always, 1=HoldingCtrl (disables wrap), 2=HoldingShift (disables wrap)
|
||||
private bool _cursorWrapDisableOnSingleMonitor; // Disable cursor wrap when only one monitor is connected
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <filesystem>
|
||||
#include <cwctype>
|
||||
#include <Shlobj.h>
|
||||
|
||||
SettingsLoader::SettingsLoader()
|
||||
@@ -180,3 +181,723 @@ std::wstring SettingsLoader::LoadSettings(const std::wstring& moduleName, const
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::FindSettingsFilePath(const std::wstring& moduleName, const std::wstring& moduleDllPath)
|
||||
{
|
||||
const std::wstring powerToysPrefix = L"PowerToys.";
|
||||
|
||||
std::vector<std::wstring> moduleNameVariants;
|
||||
moduleNameVariants.push_back(moduleName);
|
||||
|
||||
if (moduleName.find(powerToysPrefix) != 0)
|
||||
{
|
||||
moduleNameVariants.push_back(powerToysPrefix + moduleName);
|
||||
}
|
||||
else
|
||||
{
|
||||
moduleNameVariants.push_back(moduleName.substr(powerToysPrefix.length()));
|
||||
}
|
||||
|
||||
// Try module directory first
|
||||
if (!moduleDllPath.empty())
|
||||
{
|
||||
std::filesystem::path dllPath(moduleDllPath);
|
||||
std::filesystem::path dllDirectory = dllPath.parent_path();
|
||||
std::wstring localSettingsPath = (dllDirectory / L"settings.json").wstring();
|
||||
|
||||
if (std::filesystem::exists(localSettingsPath))
|
||||
{
|
||||
return localSettingsPath;
|
||||
}
|
||||
}
|
||||
|
||||
// Try standard locations
|
||||
for (const auto& variant : moduleNameVariants)
|
||||
{
|
||||
std::wstring settingsPath = GetSettingsPath(variant);
|
||||
|
||||
if (std::filesystem::exists(settingsPath))
|
||||
{
|
||||
return settingsPath;
|
||||
}
|
||||
|
||||
// Case-insensitive search
|
||||
std::wstring root = GetPowerToysSettingsRoot();
|
||||
if (!root.empty() && std::filesystem::exists(root))
|
||||
{
|
||||
try
|
||||
{
|
||||
for (const auto& entry : std::filesystem::directory_iterator(root))
|
||||
{
|
||||
if (entry.is_directory())
|
||||
{
|
||||
std::wstring dirName = entry.path().filename().wstring();
|
||||
if (_wcsicmp(dirName.c_str(), variant.c_str()) == 0)
|
||||
{
|
||||
std::wstring actualSettingsPath = entry.path().wstring() + L"\\settings.json";
|
||||
if (std::filesystem::exists(actualSettingsPath))
|
||||
{
|
||||
return actualSettingsPath;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (...) {}
|
||||
}
|
||||
}
|
||||
|
||||
return L"";
|
||||
}
|
||||
|
||||
void SettingsLoader::DisplaySettingsInfo(const std::wstring& moduleName, const std::wstring& moduleDllPath)
|
||||
{
|
||||
std::wcout << L"\n";
|
||||
std::wcout << L"\033[1;36m"; // Cyan bold
|
||||
std::wcout << L"+----------------------------------------------------------------+\n";
|
||||
std::wcout << L"| MODULE SETTINGS INFO |\n";
|
||||
std::wcout << L"+----------------------------------------------------------------+\n";
|
||||
std::wcout << L"\033[0m";
|
||||
|
||||
std::wcout << L"\n\033[1mModule:\033[0m " << moduleName << L"\n";
|
||||
|
||||
std::wstring settingsPath = FindSettingsFilePath(moduleName, moduleDllPath);
|
||||
|
||||
if (settingsPath.empty())
|
||||
{
|
||||
std::wcout << L"\033[1;33mSettings file:\033[0m Not found\n";
|
||||
std::wcout << L"\nNo settings file found for this module.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::wcout << L"\033[1mSettings file:\033[0m " << settingsPath << L"\n\n";
|
||||
|
||||
std::wstring settingsJson = ReadFileContents(settingsPath);
|
||||
if (settingsJson.empty())
|
||||
{
|
||||
std::wcout << L"Unable to read settings file.\n";
|
||||
return;
|
||||
}
|
||||
|
||||
std::wcout << L"\033[1;32mCurrent Settings:\033[0m\n";
|
||||
std::wcout << L"-----------------------------------------------------------------\n";
|
||||
|
||||
DisplayJsonProperties(settingsJson, 0);
|
||||
|
||||
std::wcout << L"-----------------------------------------------------------------\n\n";
|
||||
}
|
||||
|
||||
void SettingsLoader::DisplayJsonProperties(const std::wstring& settingsJson, int indent)
|
||||
{
|
||||
// Simple JSON parser for display - handles the PowerToys settings format
|
||||
// Format: { "properties": { "key": { "value": ... }, ... } }
|
||||
// Also handles hotkey settings: { "key": { "win": true, "alt": true, "code": 85 } }
|
||||
|
||||
std::string json(settingsJson.begin(), settingsJson.end());
|
||||
|
||||
// Find "properties" section
|
||||
size_t propsStart = json.find("\"properties\"");
|
||||
if (propsStart == std::string::npos)
|
||||
{
|
||||
// If no properties section, just display the raw JSON
|
||||
std::wcout << settingsJson << L"\n";
|
||||
return;
|
||||
}
|
||||
|
||||
// Find the opening brace after "properties":
|
||||
size_t braceStart = json.find('{', propsStart + 12);
|
||||
if (braceStart == std::string::npos) return;
|
||||
|
||||
// Parse each property
|
||||
size_t pos = braceStart + 1;
|
||||
int braceCount = 1;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
// Skip whitespace
|
||||
while (pos < json.size() && std::isspace(json[pos])) pos++;
|
||||
|
||||
// Look for property name
|
||||
if (json[pos] == '"')
|
||||
{
|
||||
size_t nameStart = pos + 1;
|
||||
size_t nameEnd = json.find('"', nameStart);
|
||||
if (nameEnd == std::string::npos) break;
|
||||
|
||||
std::string propName = json.substr(nameStart, nameEnd - nameStart);
|
||||
|
||||
// Skip to the value object
|
||||
pos = json.find('{', nameEnd);
|
||||
if (pos == std::string::npos) break;
|
||||
|
||||
size_t objStart = pos;
|
||||
|
||||
// Check if this is a hotkey object (has "win", "code" etc. but no "value")
|
||||
if (IsHotkeyObject(json, objStart))
|
||||
{
|
||||
// Parse hotkey and display
|
||||
size_t objEnd;
|
||||
std::string hotkeyStr = ParseHotkeyObject(json, objStart, objEnd);
|
||||
|
||||
std::wstring wPropName(propName.begin(), propName.end());
|
||||
std::wstring wHotkeyStr(hotkeyStr.begin(), hotkeyStr.end());
|
||||
|
||||
std::wcout << L" \033[1;34m" << wPropName << L"\033[0m: ";
|
||||
std::wcout << L"\033[1;36m" << wHotkeyStr << L"\033[0m\n";
|
||||
|
||||
pos = objEnd + 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Regular property with "value" key
|
||||
int innerBraceCount = 1;
|
||||
pos++;
|
||||
|
||||
std::string valueStr = "";
|
||||
bool foundValue = false;
|
||||
|
||||
while (pos < json.size() && innerBraceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') innerBraceCount++;
|
||||
else if (json[pos] == '}') innerBraceCount--;
|
||||
else if (json[pos] == '"' && !foundValue)
|
||||
{
|
||||
size_t keyStart = pos + 1;
|
||||
size_t keyEnd = json.find('"', keyStart);
|
||||
if (keyEnd != std::string::npos)
|
||||
{
|
||||
std::string key = json.substr(keyStart, keyEnd - keyStart);
|
||||
if (key == "value")
|
||||
{
|
||||
// Find the colon and then the value
|
||||
size_t colonPos = json.find(':', keyEnd);
|
||||
if (colonPos != std::string::npos)
|
||||
{
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < json.size() && std::isspace(json[valStart])) valStart++;
|
||||
|
||||
// Determine value type and extract
|
||||
if (json[valStart] == '"')
|
||||
{
|
||||
size_t valEnd = json.find('"', valStart + 1);
|
||||
if (valEnd != std::string::npos)
|
||||
{
|
||||
valueStr = json.substr(valStart + 1, valEnd - valStart - 1);
|
||||
foundValue = true;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// Number or boolean
|
||||
size_t valEnd = valStart;
|
||||
while (valEnd < json.size() && json[valEnd] != ',' && json[valEnd] != '}' && !std::isspace(json[valEnd]))
|
||||
{
|
||||
valEnd++;
|
||||
}
|
||||
valueStr = json.substr(valStart, valEnd - valStart);
|
||||
foundValue = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
pos = keyEnd + 1;
|
||||
continue;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
// Print the property
|
||||
std::wstring wPropName(propName.begin(), propName.end());
|
||||
std::wstring wValueStr(valueStr.begin(), valueStr.end());
|
||||
|
||||
std::wcout << L" \033[1;34m" << wPropName << L"\033[0m: ";
|
||||
|
||||
// Color-code based on value type
|
||||
if (valueStr == "true")
|
||||
{
|
||||
std::wcout << L"\033[1;32mtrue\033[0m";
|
||||
}
|
||||
else if (valueStr == "false")
|
||||
{
|
||||
std::wcout << L"\033[1;31mfalse\033[0m";
|
||||
}
|
||||
else if (!valueStr.empty() && (std::isdigit(valueStr[0]) || valueStr[0] == '-'))
|
||||
{
|
||||
std::wcout << L"\033[1;33m" << wValueStr << L"\033[0m";
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcout << L"\033[1;35m\"" << wValueStr << L"\"\033[0m";
|
||||
}
|
||||
std::wcout << L"\n";
|
||||
}
|
||||
else if (json[pos] == '{')
|
||||
{
|
||||
braceCount++;
|
||||
pos++;
|
||||
}
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
pos++;
|
||||
}
|
||||
else
|
||||
{
|
||||
pos++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
std::wstring SettingsLoader::GetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key)
|
||||
{
|
||||
std::wstring settingsPath = FindSettingsFilePath(moduleName, moduleDllPath);
|
||||
if (settingsPath.empty()) return L"";
|
||||
|
||||
std::wstring settingsJson = ReadFileContents(settingsPath);
|
||||
if (settingsJson.empty()) return L"";
|
||||
|
||||
// Simple JSON parser to find the specific key
|
||||
std::string json(settingsJson.begin(), settingsJson.end());
|
||||
std::string searchKey(key.begin(), key.end());
|
||||
|
||||
// Look for "properties" -> key -> "value"
|
||||
std::string searchPattern = "\"" + searchKey + "\"";
|
||||
size_t keyPos = json.find(searchPattern);
|
||||
if (keyPos == std::string::npos) return L"";
|
||||
|
||||
// Find "value" within this property's object
|
||||
size_t objStart = json.find('{', keyPos);
|
||||
if (objStart == std::string::npos) return L"";
|
||||
|
||||
size_t valueKeyPos = json.find("\"value\"", objStart);
|
||||
if (valueKeyPos == std::string::npos) return L"";
|
||||
|
||||
// Find the colon and extract value
|
||||
size_t colonPos = json.find(':', valueKeyPos);
|
||||
if (colonPos == std::string::npos) return L"";
|
||||
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < json.size() && std::isspace(json[valStart])) valStart++;
|
||||
|
||||
std::string valueStr;
|
||||
if (json[valStart] == '"')
|
||||
{
|
||||
size_t valEnd = json.find('"', valStart + 1);
|
||||
if (valEnd != std::string::npos)
|
||||
{
|
||||
valueStr = json.substr(valStart + 1, valEnd - valStart - 1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
size_t valEnd = valStart;
|
||||
while (valEnd < json.size() && json[valEnd] != ',' && json[valEnd] != '}' && !std::isspace(json[valEnd]))
|
||||
{
|
||||
valEnd++;
|
||||
}
|
||||
valueStr = json.substr(valStart, valEnd - valStart);
|
||||
}
|
||||
|
||||
return std::wstring(valueStr.begin(), valueStr.end());
|
||||
}
|
||||
|
||||
bool SettingsLoader::SetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key, const std::wstring& value)
|
||||
{
|
||||
std::wstring settingsPath = FindSettingsFilePath(moduleName, moduleDllPath);
|
||||
if (settingsPath.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Settings file not found\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring settingsJson = ReadFileContents(settingsPath);
|
||||
if (settingsJson.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Unable to read settings file\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string json(settingsJson.begin(), settingsJson.end());
|
||||
std::string searchKey(key.begin(), key.end());
|
||||
std::string newValue(value.begin(), value.end());
|
||||
|
||||
// Find the property
|
||||
std::string searchPattern = "\"" + searchKey + "\"";
|
||||
size_t keyPos = json.find(searchPattern);
|
||||
if (keyPos == std::string::npos)
|
||||
{
|
||||
// Setting not found - prompt user to add it
|
||||
std::wcout << L"\033[1;33mWarning:\033[0m Setting '" << key << L"' not found in settings file.\n";
|
||||
std::wcout << L"This could be a new setting or a typo.\n\n";
|
||||
|
||||
if (PromptYesNo(L"Do you want to add this as a new setting?"))
|
||||
{
|
||||
std::string modifiedJson = AddNewProperty(json, searchKey, newValue);
|
||||
if (modifiedJson.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Failed to add new property to settings file\n";
|
||||
return false;
|
||||
}
|
||||
|
||||
std::wstring newJson(modifiedJson.begin(), modifiedJson.end());
|
||||
if (WriteFileContents(settingsPath, newJson))
|
||||
{
|
||||
std::wcout << L"\033[1;32m+\033[0m New setting '" << key << L"' added with value: " << value << L"\n";
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcerr << L"Error: Failed to write settings file\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcout << L"Operation cancelled.\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Find "value" within this property's object
|
||||
size_t objStart = json.find('{', keyPos);
|
||||
if (objStart == std::string::npos) return false;
|
||||
|
||||
size_t valueKeyPos = json.find("\"value\"", objStart);
|
||||
if (valueKeyPos == std::string::npos) return false;
|
||||
|
||||
// Find the colon and the existing value
|
||||
size_t colonPos = json.find(':', valueKeyPos);
|
||||
if (colonPos == std::string::npos) return false;
|
||||
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < json.size() && std::isspace(json[valStart])) valStart++;
|
||||
|
||||
size_t valEnd;
|
||||
bool isString = (json[valStart] == '"');
|
||||
|
||||
if (isString)
|
||||
{
|
||||
valEnd = json.find('"', valStart + 1);
|
||||
if (valEnd != std::string::npos) valEnd++; // Include closing quote
|
||||
}
|
||||
else
|
||||
{
|
||||
valEnd = valStart;
|
||||
while (valEnd < json.size() && json[valEnd] != ',' && json[valEnd] != '}' && !std::isspace(json[valEnd]))
|
||||
{
|
||||
valEnd++;
|
||||
}
|
||||
}
|
||||
|
||||
// Determine if new value should be quoted
|
||||
bool newValueNeedsQuotes = false;
|
||||
if (newValue != "true" && newValue != "false")
|
||||
{
|
||||
// Check if it's a number
|
||||
bool isNumber = !newValue.empty();
|
||||
for (char c : newValue)
|
||||
{
|
||||
if (!std::isdigit(c) && c != '.' && c != '-')
|
||||
{
|
||||
isNumber = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
newValueNeedsQuotes = !isNumber;
|
||||
}
|
||||
|
||||
std::string replacement;
|
||||
if (newValueNeedsQuotes)
|
||||
{
|
||||
replacement = "\"" + newValue + "\"";
|
||||
}
|
||||
else
|
||||
{
|
||||
replacement = newValue;
|
||||
}
|
||||
|
||||
// Replace the value
|
||||
json = json.substr(0, valStart) + replacement + json.substr(valEnd);
|
||||
|
||||
// Write back
|
||||
std::wstring newJson(json.begin(), json.end());
|
||||
if (WriteFileContents(settingsPath, newJson))
|
||||
{
|
||||
std::wcout << L"\033[1;32m?\033[0m Setting '" << key << L"' updated to: " << value << L"\n";
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcerr << L"Error: Failed to write settings file\n";
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool SettingsLoader::WriteFileContents(const std::wstring& filePath, const std::wstring& contents) const
|
||||
{
|
||||
std::ofstream file(filePath, std::ios::binary);
|
||||
if (!file.is_open())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
std::string utf8Contents(contents.begin(), contents.end());
|
||||
file << utf8Contents;
|
||||
file.close();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SettingsLoader::PromptYesNo(const std::wstring& prompt)
|
||||
{
|
||||
std::wcout << prompt << L" [y/N]: ";
|
||||
std::wcout.flush();
|
||||
|
||||
std::wstring input;
|
||||
std::getline(std::wcin, input);
|
||||
|
||||
// Trim whitespace
|
||||
while (!input.empty() && iswspace(input.front())) input.erase(input.begin());
|
||||
while (!input.empty() && iswspace(input.back())) input.pop_back();
|
||||
|
||||
// Check for yes responses
|
||||
return !input.empty() && (input[0] == L'y' || input[0] == L'Y');
|
||||
}
|
||||
|
||||
std::string SettingsLoader::AddNewProperty(const std::string& json, const std::string& key, const std::string& value)
|
||||
{
|
||||
// Find the "properties" section
|
||||
size_t propsPos = json.find("\"properties\"");
|
||||
if (propsPos == std::string::npos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Find the opening brace of properties object
|
||||
size_t propsStart = json.find('{', propsPos);
|
||||
if (propsStart == std::string::npos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Find the closing brace of properties object
|
||||
int braceCount = 1;
|
||||
size_t pos = propsStart + 1;
|
||||
size_t propsEnd = std::string::npos;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') braceCount++;
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
if (braceCount == 0) propsEnd = pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (propsEnd == std::string::npos)
|
||||
{
|
||||
return "";
|
||||
}
|
||||
|
||||
// Determine if new value should be quoted
|
||||
bool needsQuotes = false;
|
||||
if (value != "true" && value != "false")
|
||||
{
|
||||
bool isNumber = !value.empty();
|
||||
for (char c : value)
|
||||
{
|
||||
if (!std::isdigit(c) && c != '.' && c != '-')
|
||||
{
|
||||
isNumber = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
needsQuotes = !isNumber;
|
||||
}
|
||||
|
||||
// Build the new property JSON
|
||||
// Format: "key": { "value": ... }
|
||||
std::string valueJson = needsQuotes ? ("\"" + value + "\"") : value;
|
||||
std::string newProperty = ",\n \"" + key + "\": {\n \"value\": " + valueJson + "\n }";
|
||||
|
||||
// Check if properties object is empty (only whitespace between braces)
|
||||
std::string propsContent = json.substr(propsStart + 1, propsEnd - propsStart - 1);
|
||||
bool isEmpty = true;
|
||||
for (char c : propsContent)
|
||||
{
|
||||
if (!std::isspace(c))
|
||||
{
|
||||
isEmpty = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Insert the new property before the closing brace of properties
|
||||
std::string result;
|
||||
if (isEmpty)
|
||||
{
|
||||
// No leading comma for empty properties
|
||||
newProperty = "\n \"" + key + "\": {\n \"value\": " + valueJson + "\n }\n ";
|
||||
}
|
||||
|
||||
result = json.substr(0, propsEnd) + newProperty + json.substr(propsEnd);
|
||||
return result;
|
||||
}
|
||||
|
||||
bool SettingsLoader::IsHotkeyObject(const std::string& json, size_t objStart)
|
||||
{
|
||||
// A hotkey object has "win", "alt", "ctrl", "shift", and "code" fields
|
||||
// Find the end of this object
|
||||
int braceCount = 1;
|
||||
size_t pos = objStart + 1;
|
||||
size_t objEnd = objStart;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') braceCount++;
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
if (braceCount == 0) objEnd = pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (objEnd <= objStart) return false;
|
||||
|
||||
std::string objContent = json.substr(objStart, objEnd - objStart + 1);
|
||||
|
||||
// Check for hotkey-specific fields
|
||||
return (objContent.find("\"win\"") != std::string::npos ||
|
||||
objContent.find("\"code\"") != std::string::npos) &&
|
||||
objContent.find("\"value\"") == std::string::npos;
|
||||
}
|
||||
|
||||
std::string SettingsLoader::ParseHotkeyObject(const std::string& json, size_t objStart, size_t& objEnd)
|
||||
{
|
||||
// Find the end of this object
|
||||
int braceCount = 1;
|
||||
size_t pos = objStart + 1;
|
||||
objEnd = objStart;
|
||||
|
||||
while (pos < json.size() && braceCount > 0)
|
||||
{
|
||||
if (json[pos] == '{') braceCount++;
|
||||
else if (json[pos] == '}')
|
||||
{
|
||||
braceCount--;
|
||||
if (braceCount == 0) objEnd = pos;
|
||||
}
|
||||
pos++;
|
||||
}
|
||||
|
||||
if (objEnd <= objStart) return "";
|
||||
|
||||
std::string objContent = json.substr(objStart, objEnd - objStart + 1);
|
||||
|
||||
// Parse hotkey fields
|
||||
bool win = false, ctrl = false, alt = false, shift = false;
|
||||
int code = 0;
|
||||
|
||||
// Helper to find boolean value
|
||||
auto findBool = [&objContent](const std::string& key) -> bool {
|
||||
size_t keyPos = objContent.find("\"" + key + "\"");
|
||||
if (keyPos == std::string::npos) return false;
|
||||
size_t colonPos = objContent.find(':', keyPos);
|
||||
if (colonPos == std::string::npos) return false;
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < objContent.size() && std::isspace(objContent[valStart])) valStart++;
|
||||
return objContent.substr(valStart, 4) == "true";
|
||||
};
|
||||
|
||||
// Helper to find integer value
|
||||
auto findInt = [&objContent](const std::string& key) -> int {
|
||||
size_t keyPos = objContent.find("\"" + key + "\"");
|
||||
if (keyPos == std::string::npos) return 0;
|
||||
size_t colonPos = objContent.find(':', keyPos);
|
||||
if (colonPos == std::string::npos) return 0;
|
||||
size_t valStart = colonPos + 1;
|
||||
while (valStart < objContent.size() && std::isspace(objContent[valStart])) valStart++;
|
||||
size_t valEnd = valStart;
|
||||
while (valEnd < objContent.size() && (std::isdigit(objContent[valEnd]) || objContent[valEnd] == '-'))
|
||||
valEnd++;
|
||||
if (valEnd > valStart)
|
||||
return std::stoi(objContent.substr(valStart, valEnd - valStart));
|
||||
return 0;
|
||||
};
|
||||
|
||||
win = findBool("win");
|
||||
ctrl = findBool("ctrl");
|
||||
alt = findBool("alt");
|
||||
shift = findBool("shift");
|
||||
code = findInt("code");
|
||||
|
||||
// Build hotkey string
|
||||
std::string result;
|
||||
if (win) result += "Win+";
|
||||
if (ctrl) result += "Ctrl+";
|
||||
if (alt) result += "Alt+";
|
||||
if (shift) result += "Shift+";
|
||||
|
||||
// Convert virtual key code to key name
|
||||
if (code > 0)
|
||||
{
|
||||
if (code >= 'A' && code <= 'Z')
|
||||
{
|
||||
result += static_cast<char>(code);
|
||||
}
|
||||
else if (code >= '0' && code <= '9')
|
||||
{
|
||||
result += static_cast<char>(code);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Common VK codes
|
||||
switch (code)
|
||||
{
|
||||
case 0x20: result += "Space"; break;
|
||||
case 0x0D: result += "Enter"; break;
|
||||
case 0x1B: result += "Escape"; break;
|
||||
case 0x09: result += "Tab"; break;
|
||||
case 0x08: result += "Backspace"; break;
|
||||
case 0x2E: result += "Delete"; break;
|
||||
case 0x24: result += "Home"; break;
|
||||
case 0x23: result += "End"; break;
|
||||
case 0x21: result += "PageUp"; break;
|
||||
case 0x22: result += "PageDown"; break;
|
||||
case 0x25: result += "Left"; break;
|
||||
case 0x26: result += "Up"; break;
|
||||
case 0x27: result += "Right"; break;
|
||||
case 0x28: result += "Down"; break;
|
||||
case 0x70: case 0x71: case 0x72: case 0x73: case 0x74: case 0x75:
|
||||
case 0x76: case 0x77: case 0x78: case 0x79: case 0x7A: case 0x7B:
|
||||
result += "F" + std::to_string(code - 0x70 + 1);
|
||||
break;
|
||||
case 0xC0: result += "`"; break;
|
||||
case 0xBD: result += "-"; break;
|
||||
case 0xBB: result += "="; break;
|
||||
case 0xDB: result += "["; break;
|
||||
case 0xDD: result += "]"; break;
|
||||
case 0xDC: result += "\\"; break;
|
||||
case 0xBA: result += ";"; break;
|
||||
case 0xDE: result += "'"; break;
|
||||
case 0xBC: result += ","; break;
|
||||
case 0xBE: result += "."; break;
|
||||
case 0xBF: result += "/"; break;
|
||||
default:
|
||||
result += "VK_0x" + std::to_string(code);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Remove trailing + if no key code
|
||||
if (!result.empty() && result.back() == '+')
|
||||
{
|
||||
result.pop_back();
|
||||
}
|
||||
|
||||
return result.empty() ? "(not set)" : result;
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@
|
||||
|
||||
#include <Windows.h>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
|
||||
/// <summary>
|
||||
/// Utility class for discovering and loading PowerToy module settings
|
||||
@@ -31,6 +33,40 @@ public:
|
||||
/// <returns>Full path to the settings.json file</returns>
|
||||
std::wstring GetSettingsPath(const std::wstring& moduleName) const;
|
||||
|
||||
/// <summary>
|
||||
/// Display settings information for a module
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
void DisplaySettingsInfo(const std::wstring& moduleName, const std::wstring& moduleDllPath);
|
||||
|
||||
/// <summary>
|
||||
/// Get a specific setting value
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
/// <param name="key">Setting key to retrieve</param>
|
||||
/// <returns>Value as string, or empty if not found</returns>
|
||||
std::wstring GetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key);
|
||||
|
||||
/// <summary>
|
||||
/// Set a specific setting value
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
/// <param name="key">Setting key to set</param>
|
||||
/// <param name="value">Value to set</param>
|
||||
/// <returns>True if successful</returns>
|
||||
bool SetSettingValue(const std::wstring& moduleName, const std::wstring& moduleDllPath, const std::wstring& key, const std::wstring& value);
|
||||
|
||||
/// <summary>
|
||||
/// Find the actual settings file path (handles case-insensitivity)
|
||||
/// </summary>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="moduleDllPath">Path to the module DLL</param>
|
||||
/// <returns>Actual path to settings.json, or empty if not found</returns>
|
||||
std::wstring FindSettingsFilePath(const std::wstring& moduleName, const std::wstring& moduleDllPath);
|
||||
|
||||
private:
|
||||
/// <summary>
|
||||
/// Get the PowerToys root settings directory
|
||||
@@ -44,4 +80,52 @@ private:
|
||||
/// <param name="filePath">Path to the file</param>
|
||||
/// <returns>File contents as a string</returns>
|
||||
std::wstring ReadFileContents(const std::wstring& filePath) const;
|
||||
|
||||
/// <summary>
|
||||
/// Write a string to a text file
|
||||
/// </summary>
|
||||
/// <param name="filePath">Path to the file</param>
|
||||
/// <param name="contents">Contents to write</param>
|
||||
/// <returns>True if successful</returns>
|
||||
bool WriteFileContents(const std::wstring& filePath, const std::wstring& contents) const;
|
||||
|
||||
/// <summary>
|
||||
/// Parse settings properties from JSON and display them
|
||||
/// </summary>
|
||||
/// <param name="settingsJson">JSON string containing settings</param>
|
||||
/// <param name="indent">Indentation level</param>
|
||||
void DisplayJsonProperties(const std::wstring& settingsJson, int indent = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Parse a hotkey object from JSON and format it as a string (e.g., "Win+Alt+U")
|
||||
/// </summary>
|
||||
/// <param name="json">JSON string</param>
|
||||
/// <param name="objStart">Start position of the hotkey object</param>
|
||||
/// <param name="objEnd">Output: end position of the hotkey object</param>
|
||||
/// <returns>Formatted hotkey string, or empty if not a valid hotkey</returns>
|
||||
std::string ParseHotkeyObject(const std::string& json, size_t objStart, size_t& objEnd);
|
||||
|
||||
/// <summary>
|
||||
/// Check if a JSON object appears to be a hotkey settings object
|
||||
/// </summary>
|
||||
/// <param name="json">JSON string</param>
|
||||
/// <param name="objStart">Start position of the object</param>
|
||||
/// <returns>True if this looks like a hotkey object</returns>
|
||||
bool IsHotkeyObject(const std::string& json, size_t objStart);
|
||||
|
||||
/// <summary>
|
||||
/// Prompt user for yes/no confirmation
|
||||
/// </summary>
|
||||
/// <param name="prompt">The question to ask</param>
|
||||
/// <returns>True if user answered yes</returns>
|
||||
bool PromptYesNo(const std::wstring& prompt);
|
||||
|
||||
/// <summary>
|
||||
/// Add a new property to the JSON settings file
|
||||
/// </summary>
|
||||
/// <param name="json">The JSON string to modify</param>
|
||||
/// <param name="key">The property key to add</param>
|
||||
/// <param name="value">The value to set</param>
|
||||
/// <returns>Modified JSON string, or empty if failed</returns>
|
||||
std::string AddNewProperty(const std::string& json, const std::string& key, const std::string& value);
|
||||
};
|
||||
|
||||
@@ -7,6 +7,8 @@
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <filesystem>
|
||||
#include <vector>
|
||||
#include <utility>
|
||||
#include "ModuleLoader.h"
|
||||
#include "SettingsLoader.h"
|
||||
#include "HotkeyManager.h"
|
||||
@@ -17,9 +19,15 @@ namespace
|
||||
void PrintUsage()
|
||||
{
|
||||
std::wcout << L"PowerToys Module Loader - Standalone utility for loading and testing PowerToy modules\n\n";
|
||||
std::wcout << L"Usage: ModuleLoader.exe <module_dll_path>\n\n";
|
||||
std::wcout << L"Usage: ModuleLoader.exe <module_dll_path> [options]\n\n";
|
||||
std::wcout << L"Arguments:\n";
|
||||
std::wcout << L" module_dll_path Path to the PowerToy module DLL (e.g., CursorWrap.dll)\n\n";
|
||||
std::wcout << L"Options:\n";
|
||||
std::wcout << L" --info Display current module settings and exit\n";
|
||||
std::wcout << L" --get <key> Get a specific setting value and exit\n";
|
||||
std::wcout << L" --set <key>=<val> Set a setting value (can be used multiple times)\n";
|
||||
std::wcout << L" --no-run Apply settings changes without running the module\n";
|
||||
std::wcout << L" --help Show this help message\n\n";
|
||||
std::wcout << L"Behavior:\n";
|
||||
std::wcout << L" - Automatically discovers settings from %%LOCALAPPDATA%%\\Microsoft\\PowerToys\\<ModuleName>\\settings.json\n";
|
||||
std::wcout << L" - Loads and enables the module\n";
|
||||
@@ -27,10 +35,12 @@ namespace
|
||||
std::wcout << L" - Runs until Ctrl+C is pressed\n\n";
|
||||
std::wcout << L"Examples:\n";
|
||||
std::wcout << L" ModuleLoader.exe x64\\Debug\\modules\\CursorWrap.dll\n";
|
||||
std::wcout << L" ModuleLoader.exe \"C:\\Program Files\\PowerToys\\modules\\MouseHighlighter.dll\"\n\n";
|
||||
std::wcout << L" ModuleLoader.exe CursorWrap.dll --info\n";
|
||||
std::wcout << L" ModuleLoader.exe CursorWrap.dll --get wrap_mode\n";
|
||||
std::wcout << L" ModuleLoader.exe CursorWrap.dll --set wrap_mode=1\n";
|
||||
std::wcout << L" ModuleLoader.exe CursorWrap.dll --set auto_activate=true --no-run\n\n";
|
||||
std::wcout << L"Notes:\n";
|
||||
std::wcout << L" - Only non-UI modules are supported\n";
|
||||
std::wcout << L" - Module must have a valid settings.json file\n";
|
||||
std::wcout << L" - Debug output is written to module's log directory\n";
|
||||
}
|
||||
|
||||
@@ -68,13 +78,151 @@ namespace
|
||||
|
||||
return filename;
|
||||
}
|
||||
|
||||
struct CommandLineOptions
|
||||
{
|
||||
std::wstring dllPath;
|
||||
bool showInfo = false;
|
||||
bool showHelp = false;
|
||||
bool noRun = false;
|
||||
std::wstring getKey;
|
||||
std::vector<std::pair<std::wstring, std::wstring>> setValues;
|
||||
};
|
||||
|
||||
CommandLineOptions ParseCommandLine(int argc, wchar_t* argv[])
|
||||
{
|
||||
CommandLineOptions options;
|
||||
|
||||
for (int i = 1; i < argc; i++)
|
||||
{
|
||||
std::wstring arg = argv[i];
|
||||
|
||||
if (arg == L"--help" || arg == L"-h" || arg == L"/?")
|
||||
{
|
||||
options.showHelp = true;
|
||||
}
|
||||
else if (arg == L"--info")
|
||||
{
|
||||
options.showInfo = true;
|
||||
}
|
||||
else if (arg == L"--no-run")
|
||||
{
|
||||
options.noRun = true;
|
||||
}
|
||||
else if (arg == L"--get" && i + 1 < argc)
|
||||
{
|
||||
options.getKey = argv[++i];
|
||||
}
|
||||
else if (arg == L"--set" && i + 1 < argc)
|
||||
{
|
||||
std::wstring setValue = argv[++i];
|
||||
size_t eqPos = setValue.find(L'=');
|
||||
if (eqPos != std::wstring::npos)
|
||||
{
|
||||
std::wstring key = setValue.substr(0, eqPos);
|
||||
std::wstring value = setValue.substr(eqPos + 1);
|
||||
options.setValues.push_back({key, value});
|
||||
}
|
||||
else
|
||||
{
|
||||
std::wcerr << L"Warning: Invalid --set format. Use --set key=value\n";
|
||||
}
|
||||
}
|
||||
else if (arg[0] != L'-' && options.dllPath.empty())
|
||||
{
|
||||
options.dllPath = arg;
|
||||
}
|
||||
}
|
||||
|
||||
return options;
|
||||
}
|
||||
}
|
||||
|
||||
int wmain(int argc, wchar_t* argv[])
|
||||
{
|
||||
std::wcout << L"PowerToys Module Loader v1.0\n";
|
||||
// Enable UTF-8 console output for box-drawing characters
|
||||
SetConsoleOutputCP(CP_UTF8);
|
||||
|
||||
// Enable virtual terminal processing for ANSI escape codes (colors)
|
||||
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||
DWORD dwMode = 0;
|
||||
if (GetConsoleMode(hOut, &dwMode))
|
||||
{
|
||||
SetConsoleMode(hOut, dwMode | ENABLE_VIRTUAL_TERMINAL_PROCESSING);
|
||||
}
|
||||
|
||||
std::wcout << L"PowerToys Module Loader v1.1\n";
|
||||
std::wcout << L"=============================\n\n";
|
||||
|
||||
// Parse command-line arguments
|
||||
auto options = ParseCommandLine(argc, argv);
|
||||
|
||||
if (options.showHelp)
|
||||
{
|
||||
PrintUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (options.dllPath.empty())
|
||||
{
|
||||
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Validate DLL exists
|
||||
if (!std::filesystem::exists(options.dllPath))
|
||||
{
|
||||
std::wcerr << L"Error: Module DLL not found: " << options.dllPath << L"\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Extract module name from DLL path
|
||||
std::wstring moduleName = ExtractModuleName(options.dllPath);
|
||||
|
||||
// Create settings loader
|
||||
SettingsLoader settingsLoader;
|
||||
|
||||
// Handle --info option
|
||||
if (options.showInfo)
|
||||
{
|
||||
settingsLoader.DisplaySettingsInfo(moduleName, options.dllPath);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle --get option
|
||||
if (!options.getKey.empty())
|
||||
{
|
||||
std::wstring value = settingsLoader.GetSettingValue(moduleName, options.dllPath, options.getKey);
|
||||
if (value.empty())
|
||||
{
|
||||
std::wcerr << L"Setting '" << options.getKey << L"' not found.\n";
|
||||
return 1;
|
||||
}
|
||||
std::wcout << options.getKey << L"=" << value << L"\n";
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Handle --set options
|
||||
if (!options.setValues.empty())
|
||||
{
|
||||
bool allSuccess = true;
|
||||
for (const auto& [key, value] : options.setValues)
|
||||
{
|
||||
if (!settingsLoader.SetSettingValue(moduleName, options.dllPath, key, value))
|
||||
{
|
||||
allSuccess = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (options.noRun)
|
||||
{
|
||||
return allSuccess ? 0 : 1;
|
||||
}
|
||||
|
||||
std::wcout << L"\n";
|
||||
}
|
||||
|
||||
// Check if PowerToys.exe is running
|
||||
HANDLE hSnapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
|
||||
if (hSnapshot != INVALID_HANDLE_VALUE)
|
||||
@@ -99,26 +247,22 @@ int wmain(int argc, wchar_t* argv[])
|
||||
if (powerToysRunning)
|
||||
{
|
||||
// Display warning with VT100 colors
|
||||
// Yellow background (43m), black text (30m), bold (1m)
|
||||
std::wcout << L"\033[1;43;30m WARNING \033[0m PowerToys.exe is currently running!\n\n";
|
||||
|
||||
// Red text for important message
|
||||
std::wcout << L"\033[1;31m";
|
||||
std::wcout << L"Running ModuleLoader while PowerToys is active may cause conflicts:\n";
|
||||
std::wcout << L" - Duplicate hotkey registrations\n";
|
||||
std::wcout << L" - Conflicting module instances\n";
|
||||
std::wcout << L" - Unexpected behavior\n";
|
||||
std::wcout << L"\033[0m\n"; // Reset color
|
||||
std::wcout << L"\033[0m\n";
|
||||
|
||||
// Cyan text for recommendation
|
||||
std::wcout << L"\033[1;36m";
|
||||
std::wcout << L"RECOMMENDATION: Exit PowerToys before continuing.\n";
|
||||
std::wcout << L"\033[0m\n"; // Reset color
|
||||
std::wcout << L"\033[0m\n";
|
||||
|
||||
// Yellow text for prompt
|
||||
std::wcout << L"\033[1;33m";
|
||||
std::wcout << L"Do you want to continue anyway? (y/N): ";
|
||||
std::wcout << L"\033[0m"; // Reset color
|
||||
std::wcout << L"\033[0m";
|
||||
|
||||
wchar_t response = L'\0';
|
||||
std::wcin >> response;
|
||||
@@ -133,35 +277,14 @@ int wmain(int argc, wchar_t* argv[])
|
||||
}
|
||||
}
|
||||
|
||||
// Parse command-line arguments
|
||||
if (argc < 2)
|
||||
{
|
||||
std::wcerr << L"Error: Missing required argument <module_dll_path>\n\n";
|
||||
PrintUsage();
|
||||
return 1;
|
||||
}
|
||||
|
||||
const std::wstring dllPath = argv[1];
|
||||
|
||||
// Validate DLL exists
|
||||
if (!std::filesystem::exists(dllPath))
|
||||
{
|
||||
std::wcerr << L"Error: Module DLL not found: " << dllPath << L"\n";
|
||||
return 1;
|
||||
}
|
||||
|
||||
std::wcout << L"Loading module: " << dllPath << L"\n";
|
||||
|
||||
// Extract module name from DLL path
|
||||
std::wstring moduleName = ExtractModuleName(dllPath);
|
||||
std::wcout << L"Loading module: " << options.dllPath << L"\n";
|
||||
std::wcout << L"Detected module name: " << moduleName << L"\n\n";
|
||||
|
||||
try
|
||||
{
|
||||
// Load settings for the module
|
||||
std::wcout << L"Loading settings...\n";
|
||||
SettingsLoader settingsLoader;
|
||||
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, dllPath);
|
||||
std::wstring settingsJson = settingsLoader.LoadSettings(moduleName, options.dllPath);
|
||||
|
||||
if (settingsJson.empty())
|
||||
{
|
||||
@@ -175,7 +298,7 @@ int wmain(int argc, wchar_t* argv[])
|
||||
// Load the module DLL
|
||||
std::wcout << L"Loading module DLL...\n";
|
||||
ModuleLoader moduleLoader;
|
||||
if (!moduleLoader.Load(dllPath))
|
||||
if (!moduleLoader.Load(options.dllPath))
|
||||
{
|
||||
std::wcerr << L"Error: Failed to load module DLL\n";
|
||||
return 1;
|
||||
|
||||
Reference in New Issue
Block a user