mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-04-03 09:46:54 +02:00
CursorWrap improvements (#44936)
## Summary of the Pull Request - Updated engine for better multi-monitor support. - Closing the laptop lid will now update the monitor topology - New settings/dropdown to support wrapping on horizontal, vertical, or both <img width="1103" height="643" alt="image" src="https://github.com/user-attachments/assets/ff4f0835-a8ca-4603-9441-123b71747d5c" /> <!-- Please review the items on the PR checklist before submitting--> ## PR Checklist - [x] Closes: #44820 - [x] Closes: #44864 - [x] Closes: #44952 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments Feedback for CursorWrap shows that users want the ability to constrain wrapping for horizontal only, vertical only, or both (default behavior). This PR adds a new dropdown to CursorWrap settings to enable a user to select the appropriate wrapping model. ## Validation Steps Performed Local build and running on Surface Laptop 7 Pro - will also validate on a multi-monitor setup. --------- Co-authored-by: vanzue <vanzue@outlook.com>
This commit is contained in:
57
.github/actions/spell-check/expect.txt
vendored
57
.github/actions/spell-check/expect.txt
vendored
@@ -22,7 +22,6 @@ ADate
|
|||||||
ADDSTRING
|
ADDSTRING
|
||||||
ADDUNDORECORD
|
ADDUNDORECORD
|
||||||
ADifferent
|
ADifferent
|
||||||
adjacents
|
|
||||||
ADMINS
|
ADMINS
|
||||||
adml
|
adml
|
||||||
admx
|
admx
|
||||||
@@ -219,10 +218,11 @@ CIELCh
|
|||||||
cim
|
cim
|
||||||
CImage
|
CImage
|
||||||
cla
|
cla
|
||||||
claude
|
|
||||||
CLASSDC
|
CLASSDC
|
||||||
|
classguid
|
||||||
classmethod
|
classmethod
|
||||||
CLASSNOTAVAILABLE
|
CLASSNOTAVAILABLE
|
||||||
|
claude
|
||||||
CLEARTYPE
|
CLEARTYPE
|
||||||
clickable
|
clickable
|
||||||
clickonce
|
clickonce
|
||||||
@@ -261,7 +261,6 @@ colorhistory
|
|||||||
colorhistorylimit
|
colorhistorylimit
|
||||||
COLORKEY
|
COLORKEY
|
||||||
colorref
|
colorref
|
||||||
Convs
|
|
||||||
comctl
|
comctl
|
||||||
comdlg
|
comdlg
|
||||||
comexp
|
comexp
|
||||||
@@ -282,6 +281,7 @@ CONTEXTHELP
|
|||||||
CONTEXTMENUHANDLER
|
CONTEXTMENUHANDLER
|
||||||
contractversion
|
contractversion
|
||||||
CONTROLPARENT
|
CONTROLPARENT
|
||||||
|
Convs
|
||||||
copiedcolorrepresentation
|
copiedcolorrepresentation
|
||||||
coppied
|
coppied
|
||||||
copyable
|
copyable
|
||||||
@@ -348,12 +348,14 @@ datareader
|
|||||||
datatracker
|
datatracker
|
||||||
dataversion
|
dataversion
|
||||||
Dayof
|
Dayof
|
||||||
|
dbcc
|
||||||
DBID
|
DBID
|
||||||
DBLCLKS
|
DBLCLKS
|
||||||
DBLEPSILON
|
DBLEPSILON
|
||||||
DBPROP
|
DBPROP
|
||||||
DBPROPIDSET
|
DBPROPIDSET
|
||||||
DBPROPSET
|
DBPROPSET
|
||||||
|
DBT
|
||||||
DCBA
|
DCBA
|
||||||
DCOM
|
DCOM
|
||||||
DComposition
|
DComposition
|
||||||
@@ -371,8 +373,7 @@ DEFAULTICON
|
|||||||
defaultlib
|
defaultlib
|
||||||
DEFAULTONLY
|
DEFAULTONLY
|
||||||
DEFAULTSIZE
|
DEFAULTSIZE
|
||||||
DEFAULTTONEAREST
|
defaulttonearest
|
||||||
Defaulttonearest
|
|
||||||
DEFAULTTONULL
|
DEFAULTTONULL
|
||||||
DEFAULTTOPRIMARY
|
DEFAULTTOPRIMARY
|
||||||
DEFERERASE
|
DEFERERASE
|
||||||
@@ -394,14 +395,19 @@ DESKTOPVERTRES
|
|||||||
devblogs
|
devblogs
|
||||||
devdocs
|
devdocs
|
||||||
devenv
|
devenv
|
||||||
|
DEVICEINTERFACE
|
||||||
|
devicetype
|
||||||
|
DEVINTERFACE
|
||||||
devmgmt
|
devmgmt
|
||||||
DEVMODE
|
DEVMODE
|
||||||
DEVMODEW
|
DEVMODEW
|
||||||
|
DEVNODES
|
||||||
devpal
|
devpal
|
||||||
|
DEVTYP
|
||||||
dfx
|
dfx
|
||||||
DIALOGEX
|
DIALOGEX
|
||||||
digicert
|
|
||||||
diffs
|
diffs
|
||||||
|
digicert
|
||||||
DINORMAL
|
DINORMAL
|
||||||
DISABLEASACTIONKEY
|
DISABLEASACTIONKEY
|
||||||
DISABLENOSCROLL
|
DISABLENOSCROLL
|
||||||
@@ -544,7 +550,6 @@ fdx
|
|||||||
FErase
|
FErase
|
||||||
fesf
|
fesf
|
||||||
FFFF
|
FFFF
|
||||||
FInc
|
|
||||||
Figma
|
Figma
|
||||||
FILEEXPLORER
|
FILEEXPLORER
|
||||||
fileexploreraddons
|
fileexploreraddons
|
||||||
@@ -565,6 +570,7 @@ FILESYSPATH
|
|||||||
Filetime
|
Filetime
|
||||||
FILEVERSION
|
FILEVERSION
|
||||||
FILTERMODE
|
FILTERMODE
|
||||||
|
FInc
|
||||||
findfast
|
findfast
|
||||||
findmymouse
|
findmymouse
|
||||||
FIXEDFILEINFO
|
FIXEDFILEINFO
|
||||||
@@ -666,13 +672,14 @@ HCRYPTPROV
|
|||||||
hcursor
|
hcursor
|
||||||
hcwhite
|
hcwhite
|
||||||
hdc
|
hdc
|
||||||
|
HDEVNOTIFY
|
||||||
hdr
|
hdr
|
||||||
hdrop
|
hdrop
|
||||||
hdwwiz
|
hdwwiz
|
||||||
Helpline
|
Helpline
|
||||||
helptext
|
helptext
|
||||||
HGFE
|
|
||||||
hgdiobj
|
hgdiobj
|
||||||
|
HGFE
|
||||||
hglobal
|
hglobal
|
||||||
hhk
|
hhk
|
||||||
HHmmssfff
|
HHmmssfff
|
||||||
@@ -748,9 +755,9 @@ HWNDPARENT
|
|||||||
HWNDPREV
|
HWNDPREV
|
||||||
hyjiacan
|
hyjiacan
|
||||||
IAI
|
IAI
|
||||||
|
icf
|
||||||
ICONERROR
|
ICONERROR
|
||||||
ICONLOCATION
|
ICONLOCATION
|
||||||
icf
|
|
||||||
IDCANCEL
|
IDCANCEL
|
||||||
IDD
|
IDD
|
||||||
idk
|
idk
|
||||||
@@ -841,8 +848,8 @@ jeli
|
|||||||
jfif
|
jfif
|
||||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||||
jjw
|
jjw
|
||||||
JOBOBJECT
|
|
||||||
jobject
|
jobject
|
||||||
|
JOBOBJECT
|
||||||
jpe
|
jpe
|
||||||
jpnime
|
jpnime
|
||||||
Jsons
|
Jsons
|
||||||
@@ -929,9 +936,9 @@ LOWORD
|
|||||||
lparam
|
lparam
|
||||||
LPBITMAPINFOHEADER
|
LPBITMAPINFOHEADER
|
||||||
LPCFHOOKPROC
|
LPCFHOOKPROC
|
||||||
|
lpch
|
||||||
LPCITEMIDLIST
|
LPCITEMIDLIST
|
||||||
LPCLSID
|
LPCLSID
|
||||||
lpch
|
|
||||||
lpcmi
|
lpcmi
|
||||||
LPCMINVOKECOMMANDINFO
|
LPCMINVOKECOMMANDINFO
|
||||||
LPCREATESTRUCT
|
LPCREATESTRUCT
|
||||||
@@ -947,6 +954,7 @@ LPMONITORINFO
|
|||||||
LPOSVERSIONINFOEXW
|
LPOSVERSIONINFOEXW
|
||||||
LPQUERY
|
LPQUERY
|
||||||
lprc
|
lprc
|
||||||
|
LPrivate
|
||||||
LPSAFEARRAY
|
LPSAFEARRAY
|
||||||
lpstr
|
lpstr
|
||||||
lpsz
|
lpsz
|
||||||
@@ -956,7 +964,6 @@ lptpm
|
|||||||
LPTR
|
LPTR
|
||||||
LPTSTR
|
LPTSTR
|
||||||
lpv
|
lpv
|
||||||
LPrivate
|
|
||||||
LPW
|
LPW
|
||||||
lpwcx
|
lpwcx
|
||||||
lpwndpl
|
lpwndpl
|
||||||
@@ -1000,13 +1007,13 @@ mber
|
|||||||
MBM
|
MBM
|
||||||
MBR
|
MBR
|
||||||
Mbuttondown
|
Mbuttondown
|
||||||
|
mcp
|
||||||
MDICHILD
|
MDICHILD
|
||||||
MDL
|
MDL
|
||||||
mdtext
|
mdtext
|
||||||
mdtxt
|
mdtxt
|
||||||
mdwn
|
mdwn
|
||||||
meme
|
meme
|
||||||
mcp
|
|
||||||
memicmp
|
memicmp
|
||||||
MENUITEMINFO
|
MENUITEMINFO
|
||||||
MENUITEMINFOW
|
MENUITEMINFOW
|
||||||
@@ -1042,8 +1049,8 @@ mmi
|
|||||||
mmsys
|
mmsys
|
||||||
mobileredirect
|
mobileredirect
|
||||||
mockapi
|
mockapi
|
||||||
modelcontextprotocol
|
|
||||||
MODALFRAME
|
MODALFRAME
|
||||||
|
modelcontextprotocol
|
||||||
MODESPRUNED
|
MODESPRUNED
|
||||||
MONITORENUMPROC
|
MONITORENUMPROC
|
||||||
MONITORINFO
|
MONITORINFO
|
||||||
@@ -1087,9 +1094,9 @@ MSLLHOOKSTRUCT
|
|||||||
Mso
|
Mso
|
||||||
msrc
|
msrc
|
||||||
msstore
|
msstore
|
||||||
|
mstsc
|
||||||
msvcp
|
msvcp
|
||||||
MT
|
MT
|
||||||
mstsc
|
|
||||||
MTND
|
MTND
|
||||||
MULTIPLEUSE
|
MULTIPLEUSE
|
||||||
multizone
|
multizone
|
||||||
@@ -1099,11 +1106,11 @@ muxxc
|
|||||||
muxxh
|
muxxh
|
||||||
MVPs
|
MVPs
|
||||||
mvvm
|
mvvm
|
||||||
myorg
|
|
||||||
myrepo
|
|
||||||
MVVMTK
|
MVVMTK
|
||||||
MWBEx
|
MWBEx
|
||||||
MYICON
|
MYICON
|
||||||
|
myorg
|
||||||
|
myrepo
|
||||||
NAMECHANGE
|
NAMECHANGE
|
||||||
namespaceanddescendants
|
namespaceanddescendants
|
||||||
nao
|
nao
|
||||||
@@ -1244,10 +1251,8 @@ opencode
|
|||||||
OPENFILENAME
|
OPENFILENAME
|
||||||
openrdp
|
openrdp
|
||||||
opensource
|
opensource
|
||||||
openxmlformats
|
|
||||||
ollama
|
|
||||||
onnx
|
|
||||||
openurl
|
openurl
|
||||||
|
openxmlformats
|
||||||
OPTIMIZEFORINVOKE
|
OPTIMIZEFORINVOKE
|
||||||
ORPHANEDDIALOGTITLE
|
ORPHANEDDIALOGTITLE
|
||||||
ORSCANS
|
ORSCANS
|
||||||
@@ -1464,7 +1469,6 @@ rbhid
|
|||||||
Rbuttondown
|
Rbuttondown
|
||||||
rclsid
|
rclsid
|
||||||
RCZOOMIT
|
RCZOOMIT
|
||||||
remotedesktop
|
|
||||||
rdp
|
rdp
|
||||||
RDW
|
RDW
|
||||||
READMODE
|
READMODE
|
||||||
@@ -1493,6 +1497,7 @@ remappings
|
|||||||
REMAPSUCCESSFUL
|
REMAPSUCCESSFUL
|
||||||
REMAPUNSUCCESSFUL
|
REMAPUNSUCCESSFUL
|
||||||
Remotable
|
Remotable
|
||||||
|
remotedesktop
|
||||||
remoteip
|
remoteip
|
||||||
Removelnk
|
Removelnk
|
||||||
renamable
|
renamable
|
||||||
@@ -1526,8 +1531,8 @@ RIGHTSCROLLBAR
|
|||||||
riid
|
riid
|
||||||
RKey
|
RKey
|
||||||
RNumber
|
RNumber
|
||||||
rop
|
|
||||||
rollups
|
rollups
|
||||||
|
rop
|
||||||
ROUNDSMALL
|
ROUNDSMALL
|
||||||
ROWSETEXT
|
ROWSETEXT
|
||||||
rpcrt
|
rpcrt
|
||||||
@@ -1766,8 +1771,7 @@ SVGIO
|
|||||||
svgz
|
svgz
|
||||||
SVSI
|
SVSI
|
||||||
SWFO
|
SWFO
|
||||||
SWP
|
swp
|
||||||
Swp
|
|
||||||
SWPNOSIZE
|
SWPNOSIZE
|
||||||
SWPNOZORDER
|
SWPNOZORDER
|
||||||
SWRESTORE
|
SWRESTORE
|
||||||
@@ -1786,8 +1790,7 @@ SYSKEY
|
|||||||
syskeydown
|
syskeydown
|
||||||
SYSKEYUP
|
SYSKEYUP
|
||||||
SYSLIB
|
SYSLIB
|
||||||
SYSMENU
|
sysmenu
|
||||||
Sysmenu
|
|
||||||
systemai
|
systemai
|
||||||
SYSTEMAPPS
|
SYSTEMAPPS
|
||||||
SYSTEMMODAL
|
SYSTEMMODAL
|
||||||
@@ -1891,9 +1894,9 @@ uitests
|
|||||||
UITo
|
UITo
|
||||||
ULONGLONG
|
ULONGLONG
|
||||||
Ultrawide
|
Ultrawide
|
||||||
ums
|
|
||||||
UMax
|
UMax
|
||||||
UMin
|
UMin
|
||||||
|
ums
|
||||||
uncompilable
|
uncompilable
|
||||||
UNCPRIORITY
|
UNCPRIORITY
|
||||||
UNDNAME
|
UNDNAME
|
||||||
|
|||||||
@@ -84,14 +84,17 @@
|
|||||||
</ClCompile>
|
</ClCompile>
|
||||||
</ItemDefinitionGroup>
|
</ItemDefinitionGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ClInclude Include="CursorWrapCore.h" />
|
||||||
<ClInclude Include="CursorWrapTests.h" />
|
<ClInclude Include="CursorWrapTests.h" />
|
||||||
|
<ClInclude Include="MonitorTopology.h" />
|
||||||
<ClInclude Include="pch.h" />
|
<ClInclude Include="pch.h" />
|
||||||
<ClInclude Include="trace.h" />
|
<ClInclude Include="trace.h" />
|
||||||
<ClInclude Include="resource.h" />
|
<ClInclude Include="resource.h" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<ClCompile Include="CursorWrapCore.cpp" />
|
||||||
<ClCompile Include="dllmain.cpp" />
|
<ClCompile Include="dllmain.cpp" />
|
||||||
|
<ClCompile Include="MonitorTopology.cpp" />
|
||||||
<ClCompile Include="pch.cpp">
|
<ClCompile Include="pch.cpp">
|
||||||
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
<PrecompiledHeader Condition="'$(UsePrecompiledHeaders)' != 'false'">Create</PrecompiledHeader>
|
||||||
</ClCompile>
|
</ClCompile>
|
||||||
|
|||||||
268
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
268
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#include "pch.h"
|
||||||
|
#include "CursorWrapCore.h"
|
||||||
|
#include "../../../common/logger/logger.h"
|
||||||
|
#include <sstream>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <ctime>
|
||||||
|
|
||||||
|
CursorWrapCore::CursorWrapCore()
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
std::wstring CursorWrapCore::GenerateTopologyJSON() const
|
||||||
|
{
|
||||||
|
std::wostringstream json;
|
||||||
|
|
||||||
|
// Get current time
|
||||||
|
auto now = std::time(nullptr);
|
||||||
|
std::tm tm{};
|
||||||
|
localtime_s(&tm, &now);
|
||||||
|
|
||||||
|
wchar_t computerName[MAX_COMPUTERNAME_LENGTH + 1] = {0};
|
||||||
|
DWORD size = MAX_COMPUTERNAME_LENGTH + 1;
|
||||||
|
GetComputerNameW(computerName, &size);
|
||||||
|
|
||||||
|
wchar_t userName[256] = {0};
|
||||||
|
size = 256;
|
||||||
|
GetUserNameW(userName, &size);
|
||||||
|
|
||||||
|
json << L"{\n";
|
||||||
|
json << L" \"captured_at\": \"" << std::put_time(&tm, L"%Y-%m-%dT%H:%M:%S%z") << L"\",\n";
|
||||||
|
json << L" \"computer_name\": \"" << computerName << L"\",\n";
|
||||||
|
json << L" \"user_name\": \"" << userName << L"\",\n";
|
||||||
|
json << L" \"monitor_count\": " << m_monitors.size() << L",\n";
|
||||||
|
json << L" \"monitors\": [\n";
|
||||||
|
|
||||||
|
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||||
|
{
|
||||||
|
const auto& monitor = m_monitors[i];
|
||||||
|
|
||||||
|
// Get DPI for this monitor
|
||||||
|
UINT dpiX = 96, dpiY = 96;
|
||||||
|
POINT center = {
|
||||||
|
(monitor.rect.left + monitor.rect.right) / 2,
|
||||||
|
(monitor.rect.top + monitor.rect.bottom) / 2
|
||||||
|
};
|
||||||
|
HMONITOR hMon = MonitorFromPoint(center, MONITOR_DEFAULTTONEAREST);
|
||||||
|
if (hMon)
|
||||||
|
{
|
||||||
|
// Try GetDpiForMonitor (requires linking Shcore.lib)
|
||||||
|
using GetDpiForMonitorFunc = HRESULT (WINAPI *)(HMONITOR, int, UINT*, UINT*);
|
||||||
|
HMODULE shcore = LoadLibraryW(L"Shcore.dll");
|
||||||
|
if (shcore)
|
||||||
|
{
|
||||||
|
auto getDpi = reinterpret_cast<GetDpiForMonitorFunc>(GetProcAddress(shcore, "GetDpiForMonitor"));
|
||||||
|
if (getDpi)
|
||||||
|
{
|
||||||
|
getDpi(hMon, 0, &dpiX, &dpiY); // MDT_EFFECTIVE_DPI = 0
|
||||||
|
}
|
||||||
|
FreeLibrary(shcore);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
int scalingPercent = static_cast<int>((dpiX / 96.0) * 100);
|
||||||
|
|
||||||
|
json << L" {\n";
|
||||||
|
json << L" \"left\": " << monitor.rect.left << L",\n";
|
||||||
|
json << L" \"top\": " << monitor.rect.top << L",\n";
|
||||||
|
json << L" \"right\": " << monitor.rect.right << L",\n";
|
||||||
|
json << L" \"bottom\": " << monitor.rect.bottom << L",\n";
|
||||||
|
json << L" \"width\": " << (monitor.rect.right - monitor.rect.left) << L",\n";
|
||||||
|
json << L" \"height\": " << (monitor.rect.bottom - monitor.rect.top) << L",\n";
|
||||||
|
json << L" \"dpi\": " << dpiX << L",\n";
|
||||||
|
json << L" \"scaling_percent\": " << scalingPercent << L",\n";
|
||||||
|
json << L" \"primary\": " << (monitor.isPrimary ? L"true" : L"false") << L",\n";
|
||||||
|
json << L" \"monitor_id\": " << monitor.monitorId << L"\n";
|
||||||
|
json << L" }";
|
||||||
|
if (i < m_monitors.size() - 1)
|
||||||
|
{
|
||||||
|
json << L",";
|
||||||
|
}
|
||||||
|
json << L"\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
json << L" ]\n";
|
||||||
|
json << L"}";
|
||||||
|
|
||||||
|
return json.str();
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
void CursorWrapCore::UpdateMonitorInfo()
|
||||||
|
{
|
||||||
|
size_t previousMonitorCount = m_monitors.size();
|
||||||
|
Logger::info(L"======= UPDATE MONITOR INFO START =======");
|
||||||
|
Logger::info(L"Previous monitor count: {}", previousMonitorCount);
|
||||||
|
|
||||||
|
m_monitors.clear();
|
||||||
|
|
||||||
|
EnumDisplayMonitors(nullptr, nullptr, [](HMONITOR hMonitor, HDC, LPRECT, LPARAM lParam) -> BOOL {
|
||||||
|
auto* self = reinterpret_cast<CursorWrapCore*>(lParam);
|
||||||
|
|
||||||
|
MONITORINFO mi{};
|
||||||
|
mi.cbSize = sizeof(MONITORINFO);
|
||||||
|
if (GetMonitorInfo(hMonitor, &mi))
|
||||||
|
{
|
||||||
|
MonitorInfo info{};
|
||||||
|
info.hMonitor = hMonitor; // Store handle for direct comparison later
|
||||||
|
info.rect = mi.rcMonitor;
|
||||||
|
info.isPrimary = (mi.dwFlags & MONITORINFOF_PRIMARY) != 0;
|
||||||
|
info.monitorId = static_cast<int>(self->m_monitors.size());
|
||||||
|
self->m_monitors.push_back(info);
|
||||||
|
|
||||||
|
Logger::info(L"Enumerated monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}",
|
||||||
|
info.monitorId, reinterpret_cast<uintptr_t>(hMonitor),
|
||||||
|
mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom,
|
||||||
|
info.isPrimary ? L"yes" : L"no");
|
||||||
|
}
|
||||||
|
|
||||||
|
return TRUE;
|
||||||
|
}, reinterpret_cast<LPARAM>(this));
|
||||||
|
|
||||||
|
if (previousMonitorCount != m_monitors.size())
|
||||||
|
{
|
||||||
|
Logger::info(L"*** MONITOR CONFIGURATION CHANGED: {} -> {} monitors ***",
|
||||||
|
previousMonitorCount, m_monitors.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
m_topology.Initialize(m_monitors);
|
||||||
|
|
||||||
|
// Log monitor configuration summary
|
||||||
|
Logger::info(L"Monitor configuration updated: {} monitor(s)", m_monitors.size());
|
||||||
|
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||||
|
{
|
||||||
|
const auto& m = m_monitors[i];
|
||||||
|
int width = m.rect.right - m.rect.left;
|
||||||
|
int height = m.rect.bottom - m.rect.top;
|
||||||
|
Logger::info(L" Monitor {}: {}x{} at ({}, {}){}",
|
||||||
|
i, width, height, m.rect.left, m.rect.top,
|
||||||
|
m.isPrimary ? L" [PRIMARY]" : L"");
|
||||||
|
}
|
||||||
|
Logger::info(L" Detected {} outer edges for cursor wrapping", m_topology.GetOuterEdges().size());
|
||||||
|
|
||||||
|
// Detect and log monitor gaps
|
||||||
|
auto gaps = m_topology.DetectMonitorGaps();
|
||||||
|
if (!gaps.empty())
|
||||||
|
{
|
||||||
|
Logger::warn(L"Monitor configuration has coordinate gaps that may prevent wrapping:");
|
||||||
|
for (const auto& gap : gaps)
|
||||||
|
{
|
||||||
|
Logger::warn(L" Gap between Monitor {} and Monitor {}: {}px horizontal gap, {}px vertical overlap",
|
||||||
|
gap.monitor1Index, gap.monitor2Index, gap.horizontalGap, gap.verticalOverlap);
|
||||||
|
}
|
||||||
|
Logger::warn(L" If monitors appear snapped in Display Settings but show gaps here:");
|
||||||
|
Logger::warn(L" 1. Try dragging monitors apart and snapping them back together");
|
||||||
|
Logger::warn(L" 2. Update your GPU drivers");
|
||||||
|
}
|
||||||
|
|
||||||
|
Logger::info(L"======= UPDATE MONITOR INFO END =======");
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT CursorWrapCore::HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode)
|
||||||
|
{
|
||||||
|
// Check if wrapping should be disabled during drag
|
||||||
|
if (disableWrapDuringDrag && (GetAsyncKeyState(VK_LBUTTON) & 0x8000))
|
||||||
|
{
|
||||||
|
#ifdef _DEBUG
|
||||||
|
OutputDebugStringW(L"[CursorWrap] [DRAG] Left mouse button down - skipping wrap\n");
|
||||||
|
#endif
|
||||||
|
return currentPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert int wrapMode to WrapMode enum
|
||||||
|
WrapMode mode = static_cast<WrapMode>(wrapMode);
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
{
|
||||||
|
std::wostringstream oss;
|
||||||
|
oss << L"[CursorWrap] [MOVE] Cursor at (" << currentPos.x << L", " << currentPos.y << L")";
|
||||||
|
|
||||||
|
// Get current monitor and identify which one
|
||||||
|
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
|
||||||
|
RECT monitorRect;
|
||||||
|
if (m_topology.GetMonitorRect(currentMonitor, monitorRect))
|
||||||
|
{
|
||||||
|
// Find monitor ID
|
||||||
|
int monitorId = -1;
|
||||||
|
for (const auto& monitor : m_monitors)
|
||||||
|
{
|
||||||
|
if (monitor.rect.left == monitorRect.left &&
|
||||||
|
monitor.rect.top == monitorRect.top &&
|
||||||
|
monitor.rect.right == monitorRect.right &&
|
||||||
|
monitor.rect.bottom == monitorRect.bottom)
|
||||||
|
{
|
||||||
|
monitorId = monitor.monitorId;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
oss << L" on Monitor " << monitorId << L" [" << monitorRect.left << L".." << monitorRect.right
|
||||||
|
<< L", " << monitorRect.top << L".." << monitorRect.bottom << L"]";
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
oss << L" (beyond monitor bounds)";
|
||||||
|
}
|
||||||
|
oss << L"\n";
|
||||||
|
OutputDebugStringW(oss.str().c_str());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Get current monitor
|
||||||
|
HMONITOR currentMonitor = MonitorFromPoint(currentPos, MONITOR_DEFAULTTONEAREST);
|
||||||
|
|
||||||
|
// Check if cursor is on an outer edge (filtered by wrap mode)
|
||||||
|
EdgeType edgeType;
|
||||||
|
if (!m_topology.IsOnOuterEdge(currentMonitor, currentPos, edgeType, mode))
|
||||||
|
{
|
||||||
|
#ifdef _DEBUG
|
||||||
|
static bool lastWasNotOuter = false;
|
||||||
|
if (!lastWasNotOuter)
|
||||||
|
{
|
||||||
|
OutputDebugStringW(L"[CursorWrap] [MOVE] Not on outer edge - no wrapping\n");
|
||||||
|
lastWasNotOuter = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return currentPos; // Not on an outer edge
|
||||||
|
}
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
{
|
||||||
|
const wchar_t* edgeStr = L"Unknown";
|
||||||
|
switch (edgeType)
|
||||||
|
{
|
||||||
|
case EdgeType::Left: edgeStr = L"Left"; break;
|
||||||
|
case EdgeType::Right: edgeStr = L"Right"; break;
|
||||||
|
case EdgeType::Top: edgeStr = L"Top"; break;
|
||||||
|
case EdgeType::Bottom: edgeStr = L"Bottom"; break;
|
||||||
|
}
|
||||||
|
std::wostringstream oss;
|
||||||
|
oss << L"[CursorWrap] [EDGE] Detected outer " << edgeStr << L" edge at (" << currentPos.x << L", " << currentPos.y << L")\n";
|
||||||
|
OutputDebugStringW(oss.str().c_str());
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
// Calculate wrap destination
|
||||||
|
POINT newPos = m_topology.GetWrapDestination(currentMonitor, currentPos, edgeType);
|
||||||
|
|
||||||
|
#ifdef _DEBUG
|
||||||
|
if (newPos.x != currentPos.x || newPos.y != currentPos.y)
|
||||||
|
{
|
||||||
|
std::wostringstream oss;
|
||||||
|
oss << L"[CursorWrap] [WRAP] Position change: (" << currentPos.x << L", " << currentPos.y
|
||||||
|
<< L") -> (" << newPos.x << L", " << newPos.y << L")\n";
|
||||||
|
oss << L"[CursorWrap] [WRAP] Delta: (" << (newPos.x - currentPos.x) << L", " << (newPos.y - currentPos.y) << L")\n";
|
||||||
|
OutputDebugStringW(oss.str().c_str());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
OutputDebugStringW(L"[CursorWrap] [WRAP] No position change (same-monitor wrap?)\n");
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
|
return newPos;
|
||||||
|
}
|
||||||
33
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
33
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <windows.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <string>
|
||||||
|
#include "MonitorTopology.h"
|
||||||
|
|
||||||
|
// Core cursor wrapping engine
|
||||||
|
class CursorWrapCore
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
CursorWrapCore();
|
||||||
|
|
||||||
|
void UpdateMonitorInfo();
|
||||||
|
|
||||||
|
// Handle mouse move with wrap mode filtering
|
||||||
|
// wrapMode: 0=Both, 1=VerticalOnly, 2=HorizontalOnly
|
||||||
|
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode);
|
||||||
|
|
||||||
|
const std::vector<MonitorInfo>& GetMonitors() const { return m_monitors; }
|
||||||
|
const MonitorTopology& GetTopology() const { return m_topology; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
#ifdef _DEBUG
|
||||||
|
std::wstring GenerateTopologyJSON() const;
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::vector<MonitorInfo> m_monitors;
|
||||||
|
MonitorTopology m_topology;
|
||||||
|
};
|
||||||
546
src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp
Normal file
546
src/modules/MouseUtils/CursorWrap/MonitorTopology.cpp
Normal file
@@ -0,0 +1,546 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#include "pch.h"
|
||||||
|
#include "MonitorTopology.h"
|
||||||
|
#include "../../../common/logger/logger.h"
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cmath>
|
||||||
|
|
||||||
|
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();
|
||||||
|
|
||||||
|
if (monitors.empty())
|
||||||
|
{
|
||||||
|
Logger::warn(L"No monitors provided to Initialize");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log monitor details
|
||||||
|
for (size_t i = 0; i < monitors.size(); ++i)
|
||||||
|
{
|
||||||
|
const auto& m = monitors[i];
|
||||||
|
Logger::info(L"Monitor {}: hMonitor={}, rect=({},{},{},{}), primary={}",
|
||||||
|
i, reinterpret_cast<uintptr_t>(m.hMonitor),
|
||||||
|
m.rect.left, m.rect.top, m.rect.right, m.rect.bottom,
|
||||||
|
m.isPrimary ? L"yes" : L"no");
|
||||||
|
}
|
||||||
|
|
||||||
|
BuildEdgeMap();
|
||||||
|
IdentifyOuterEdges();
|
||||||
|
|
||||||
|
Logger::info(L"Found {} outer edges", m_outerEdges.size());
|
||||||
|
for (const auto& edge : m_outerEdges)
|
||||||
|
{
|
||||||
|
const wchar_t* typeStr = L"Unknown";
|
||||||
|
switch (edge.type)
|
||||||
|
{
|
||||||
|
case EdgeType::Left: typeStr = L"Left"; break;
|
||||||
|
case EdgeType::Right: typeStr = L"Right"; break;
|
||||||
|
case EdgeType::Top: typeStr = L"Top"; break;
|
||||||
|
case EdgeType::Bottom: typeStr = L"Bottom"; break;
|
||||||
|
}
|
||||||
|
Logger::info(L"Outer edge: Monitor {} {} at position {}, range [{}, {}]",
|
||||||
|
edge.monitorIndex, typeStr, edge.position, edge.start, edge.end);
|
||||||
|
}
|
||||||
|
Logger::info(L"======= TOPOLOGY INITIALIZATION COMPLETE =======");
|
||||||
|
}
|
||||||
|
|
||||||
|
void MonitorTopology::BuildEdgeMap()
|
||||||
|
{
|
||||||
|
// Create edges for each monitor using monitor index (not HMONITOR)
|
||||||
|
// This is important because HMONITOR handles can change when monitors are
|
||||||
|
// added/removed dynamically, but indices remain stable within a single
|
||||||
|
// topology configuration
|
||||||
|
for (size_t idx = 0; idx < m_monitors.size(); ++idx)
|
||||||
|
{
|
||||||
|
const auto& monitor = m_monitors[idx];
|
||||||
|
int monitorIndex = static_cast<int>(idx);
|
||||||
|
|
||||||
|
// Left edge
|
||||||
|
MonitorEdge leftEdge;
|
||||||
|
leftEdge.monitorIndex = monitorIndex;
|
||||||
|
leftEdge.type = EdgeType::Left;
|
||||||
|
leftEdge.position = monitor.rect.left;
|
||||||
|
leftEdge.start = monitor.rect.top;
|
||||||
|
leftEdge.end = monitor.rect.bottom;
|
||||||
|
leftEdge.isOuter = true; // Will be updated in IdentifyOuterEdges
|
||||||
|
m_edgeMap[{monitorIndex, EdgeType::Left}] = leftEdge;
|
||||||
|
|
||||||
|
// Right edge
|
||||||
|
MonitorEdge rightEdge;
|
||||||
|
rightEdge.monitorIndex = monitorIndex;
|
||||||
|
rightEdge.type = EdgeType::Right;
|
||||||
|
rightEdge.position = monitor.rect.right - 1;
|
||||||
|
rightEdge.start = monitor.rect.top;
|
||||||
|
rightEdge.end = monitor.rect.bottom;
|
||||||
|
rightEdge.isOuter = true;
|
||||||
|
m_edgeMap[{monitorIndex, EdgeType::Right}] = rightEdge;
|
||||||
|
|
||||||
|
// Top edge
|
||||||
|
MonitorEdge topEdge;
|
||||||
|
topEdge.monitorIndex = monitorIndex;
|
||||||
|
topEdge.type = EdgeType::Top;
|
||||||
|
topEdge.position = monitor.rect.top;
|
||||||
|
topEdge.start = monitor.rect.left;
|
||||||
|
topEdge.end = monitor.rect.right;
|
||||||
|
topEdge.isOuter = true;
|
||||||
|
m_edgeMap[{monitorIndex, EdgeType::Top}] = topEdge;
|
||||||
|
|
||||||
|
// Bottom edge
|
||||||
|
MonitorEdge bottomEdge;
|
||||||
|
bottomEdge.monitorIndex = monitorIndex;
|
||||||
|
bottomEdge.type = EdgeType::Bottom;
|
||||||
|
bottomEdge.position = monitor.rect.bottom - 1;
|
||||||
|
bottomEdge.start = monitor.rect.left;
|
||||||
|
bottomEdge.end = monitor.rect.right;
|
||||||
|
bottomEdge.isOuter = true;
|
||||||
|
m_edgeMap[{monitorIndex, EdgeType::Bottom}] = bottomEdge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void MonitorTopology::IdentifyOuterEdges()
|
||||||
|
{
|
||||||
|
const int tolerance = 50;
|
||||||
|
|
||||||
|
// Check each edge against all other edges to find adjacent ones
|
||||||
|
for (auto& [key1, edge1] : m_edgeMap)
|
||||||
|
{
|
||||||
|
for (const auto& [key2, edge2] : m_edgeMap)
|
||||||
|
{
|
||||||
|
if (edge1.monitorIndex == edge2.monitorIndex)
|
||||||
|
{
|
||||||
|
continue; // Same monitor
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if edges are adjacent
|
||||||
|
if (EdgesAreAdjacent(edge1, edge2, tolerance))
|
||||||
|
{
|
||||||
|
edge1.isOuter = false;
|
||||||
|
break; // This edge has an adjacent monitor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (edge1.isOuter)
|
||||||
|
{
|
||||||
|
m_outerEdges.push_back(edge1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MonitorTopology::EdgesAreAdjacent(const MonitorEdge& edge1, const MonitorEdge& edge2, int tolerance) const
|
||||||
|
{
|
||||||
|
// Edges must be opposite types to be adjacent
|
||||||
|
bool oppositeTypes = false;
|
||||||
|
|
||||||
|
if ((edge1.type == EdgeType::Left && edge2.type == EdgeType::Right) ||
|
||||||
|
(edge1.type == EdgeType::Right && edge2.type == EdgeType::Left) ||
|
||||||
|
(edge1.type == EdgeType::Top && edge2.type == EdgeType::Bottom) ||
|
||||||
|
(edge1.type == EdgeType::Bottom && edge2.type == EdgeType::Top))
|
||||||
|
{
|
||||||
|
oppositeTypes = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!oppositeTypes)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if positions are within tolerance
|
||||||
|
if (abs(edge1.position - edge2.position) > tolerance)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if perpendicular ranges overlap
|
||||||
|
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
|
||||||
|
{
|
||||||
|
RECT monitorRect;
|
||||||
|
if (!GetMonitorRect(monitor, monitorRect))
|
||||||
|
{
|
||||||
|
Logger::warn(L"IsOnOuterEdge: GetMonitorRect failed for monitor handle {}", reinterpret_cast<uintptr_t>(monitor));
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get monitor index for edge map lookup
|
||||||
|
int monitorIndex = GetMonitorIndex(monitor);
|
||||||
|
if (monitorIndex < 0)
|
||||||
|
{
|
||||||
|
Logger::warn(L"IsOnOuterEdge: Monitor index not found for handle {} at cursor ({}, {})",
|
||||||
|
reinterpret_cast<uintptr_t>(monitor), cursorPos.x, cursorPos.y);
|
||||||
|
return false; // Monitor not found in our list
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check each edge type
|
||||||
|
const int edgeThreshold = 1;
|
||||||
|
|
||||||
|
// At corners, multiple edges may match - collect all candidates and try each
|
||||||
|
// to find one with a valid wrap destination
|
||||||
|
std::vector<EdgeType> candidateEdges;
|
||||||
|
|
||||||
|
// Left edge - only if mode allows horizontal wrapping
|
||||||
|
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) &&
|
||||||
|
cursorPos.x <= monitorRect.left + edgeThreshold)
|
||||||
|
{
|
||||||
|
auto it = m_edgeMap.find({monitorIndex, EdgeType::Left});
|
||||||
|
if (it != m_edgeMap.end() && it->second.isOuter)
|
||||||
|
{
|
||||||
|
candidateEdges.push_back(EdgeType::Left);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Right edge - only if mode allows horizontal wrapping
|
||||||
|
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::HorizontalOnly) &&
|
||||||
|
cursorPos.x >= monitorRect.right - 1 - edgeThreshold)
|
||||||
|
{
|
||||||
|
auto it = m_edgeMap.find({monitorIndex, EdgeType::Right});
|
||||||
|
if (it != m_edgeMap.end())
|
||||||
|
{
|
||||||
|
if (it->second.isOuter)
|
||||||
|
{
|
||||||
|
candidateEdges.push_back(EdgeType::Right);
|
||||||
|
}
|
||||||
|
// Debug: Log why right edge isn't outer
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::trace(L"IsOnOuterEdge: Monitor {} right edge is NOT outer (inner edge)", monitorIndex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Top edge - only if mode allows vertical wrapping
|
||||||
|
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) &&
|
||||||
|
cursorPos.y <= monitorRect.top + edgeThreshold)
|
||||||
|
{
|
||||||
|
auto it = m_edgeMap.find({monitorIndex, EdgeType::Top});
|
||||||
|
if (it != m_edgeMap.end() && it->second.isOuter)
|
||||||
|
{
|
||||||
|
candidateEdges.push_back(EdgeType::Top);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bottom edge - only if mode allows vertical wrapping
|
||||||
|
if ((wrapMode == WrapMode::Both || wrapMode == WrapMode::VerticalOnly) &&
|
||||||
|
cursorPos.y >= monitorRect.bottom - 1 - edgeThreshold)
|
||||||
|
{
|
||||||
|
auto it = m_edgeMap.find({monitorIndex, EdgeType::Bottom});
|
||||||
|
if (it != m_edgeMap.end() && it->second.isOuter)
|
||||||
|
{
|
||||||
|
candidateEdges.push_back(EdgeType::Bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (candidateEdges.empty())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Try each candidate edge and return first with valid wrap destination
|
||||||
|
for (EdgeType candidate : candidateEdges)
|
||||||
|
{
|
||||||
|
MonitorEdge oppositeEdge = FindOppositeOuterEdge(candidate,
|
||||||
|
(candidate == EdgeType::Left || candidate == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||||
|
|
||||||
|
if (oppositeEdge.monitorIndex >= 0)
|
||||||
|
{
|
||||||
|
outEdgeType = candidate;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
POINT MonitorTopology::GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const
|
||||||
|
{
|
||||||
|
// Get monitor index for edge map lookup
|
||||||
|
int monitorIndex = GetMonitorIndex(fromMonitor);
|
||||||
|
if (monitorIndex < 0)
|
||||||
|
{
|
||||||
|
return cursorPos; // Monitor not found
|
||||||
|
}
|
||||||
|
|
||||||
|
auto it = m_edgeMap.find({monitorIndex, edgeType});
|
||||||
|
if (it == m_edgeMap.end())
|
||||||
|
{
|
||||||
|
return cursorPos; // Edge not found
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Find opposite outer edge
|
||||||
|
MonitorEdge oppositeEdge = FindOppositeOuterEdge(edgeType,
|
||||||
|
(edgeType == EdgeType::Left || edgeType == EdgeType::Right) ? cursorPos.y : cursorPos.x);
|
||||||
|
|
||||||
|
if (oppositeEdge.monitorIndex < 0)
|
||||||
|
{
|
||||||
|
// No opposite edge found, wrap within same monitor
|
||||||
|
RECT monitorRect;
|
||||||
|
if (GetMonitorRect(fromMonitor, monitorRect))
|
||||||
|
{
|
||||||
|
POINT result = cursorPos;
|
||||||
|
switch (edgeType)
|
||||||
|
{
|
||||||
|
case EdgeType::Left:
|
||||||
|
result.x = monitorRect.right - 2;
|
||||||
|
break;
|
||||||
|
case EdgeType::Right:
|
||||||
|
result.x = monitorRect.left + 1;
|
||||||
|
break;
|
||||||
|
case EdgeType::Top:
|
||||||
|
result.y = monitorRect.bottom - 2;
|
||||||
|
break;
|
||||||
|
case EdgeType::Bottom:
|
||||||
|
result.y = monitorRect.top + 1;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return cursorPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate target position on opposite edge
|
||||||
|
POINT result;
|
||||||
|
|
||||||
|
if (edgeType == EdgeType::Left || edgeType == EdgeType::Right)
|
||||||
|
{
|
||||||
|
// Horizontal edge -> vertical movement
|
||||||
|
result.x = oppositeEdge.position;
|
||||||
|
result.y = GetAbsolutePosition(oppositeEdge, relativePos);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Vertical edge -> horizontal movement
|
||||||
|
result.y = oppositeEdge.position;
|
||||||
|
result.x = GetAbsolutePosition(oppositeEdge, relativePos);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorEdge MonitorTopology::FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) const
|
||||||
|
{
|
||||||
|
EdgeType targetType;
|
||||||
|
bool findMax; // true = find max position, false = find min position
|
||||||
|
|
||||||
|
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 { .monitorIndex = -1 }; // Invalid edge type
|
||||||
|
}
|
||||||
|
|
||||||
|
MonitorEdge result = { .monitorIndex = -1 }; // -1 indicates not found
|
||||||
|
int extremePosition = findMax ? INT_MIN : INT_MAX;
|
||||||
|
|
||||||
|
for (const auto& edge : m_outerEdges)
|
||||||
|
{
|
||||||
|
if (edge.type != targetType)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if this edge overlaps with the relative position
|
||||||
|
if (relativePosition >= edge.start && relativePosition <= edge.end)
|
||||||
|
{
|
||||||
|
if ((findMax && edge.position > extremePosition) ||
|
||||||
|
(!findMax && edge.position < extremePosition))
|
||||||
|
{
|
||||||
|
extremePosition = edge.position;
|
||||||
|
result = edge;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
double MonitorTopology::GetRelativePosition(const MonitorEdge& edge, int coordinate) const
|
||||||
|
{
|
||||||
|
if (edge.end == edge.start)
|
||||||
|
{
|
||||||
|
return 0.5; // Avoid division by zero
|
||||||
|
}
|
||||||
|
|
||||||
|
int clamped = max(edge.start, min(coordinate, edge.end));
|
||||||
|
// Use int64_t to avoid overflow warning C26451
|
||||||
|
int64_t numerator = static_cast<int64_t>(clamped) - static_cast<int64_t>(edge.start);
|
||||||
|
int64_t denominator = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
|
||||||
|
return static_cast<double>(numerator) / static_cast<double>(denominator);
|
||||||
|
}
|
||||||
|
|
||||||
|
int MonitorTopology::GetAbsolutePosition(const MonitorEdge& edge, double relativePosition) const
|
||||||
|
{
|
||||||
|
// Use int64_t to prevent arithmetic overflow during subtraction and multiplication
|
||||||
|
int64_t range = static_cast<int64_t>(edge.end) - static_cast<int64_t>(edge.start);
|
||||||
|
int64_t offset = static_cast<int64_t>(relativePosition * static_cast<double>(range));
|
||||||
|
// Clamp result to int range before returning
|
||||||
|
int64_t result = static_cast<int64_t>(edge.start) + offset;
|
||||||
|
return static_cast<int>(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<MonitorTopology::GapInfo> MonitorTopology::DetectMonitorGaps() const
|
||||||
|
{
|
||||||
|
std::vector<GapInfo> gaps;
|
||||||
|
const int gapThreshold = 50; // Same as ADJACENCY_TOLERANCE
|
||||||
|
|
||||||
|
// Check each pair of monitors
|
||||||
|
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||||
|
{
|
||||||
|
for (size_t j = i + 1; j < m_monitors.size(); ++j)
|
||||||
|
{
|
||||||
|
const auto& m1 = m_monitors[i];
|
||||||
|
const auto& m2 = m_monitors[j];
|
||||||
|
|
||||||
|
// Check vertical overlap
|
||||||
|
int vOverlapStart = max(m1.rect.top, m2.rect.top);
|
||||||
|
int vOverlapEnd = min(m1.rect.bottom, m2.rect.bottom);
|
||||||
|
int vOverlap = vOverlapEnd - vOverlapStart;
|
||||||
|
|
||||||
|
if (vOverlap <= 0)
|
||||||
|
{
|
||||||
|
continue; // No vertical overlap, skip
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check horizontal gap
|
||||||
|
int hGap = min(abs(m1.rect.right - m2.rect.left), abs(m2.rect.right - m1.rect.left));
|
||||||
|
|
||||||
|
if (hGap > gapThreshold)
|
||||||
|
{
|
||||||
|
GapInfo gap;
|
||||||
|
gap.monitor1Index = static_cast<int>(i);
|
||||||
|
gap.monitor2Index = static_cast<int>(j);
|
||||||
|
gap.horizontalGap = hGap;
|
||||||
|
gap.verticalOverlap = vOverlap;
|
||||||
|
gaps.push_back(gap);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return gaps;
|
||||||
|
}
|
||||||
|
|
||||||
|
HMONITOR MonitorTopology::GetMonitorFromPoint(const POINT& pt) const
|
||||||
|
{
|
||||||
|
return MonitorFromPoint(pt, MONITOR_DEFAULTTONEAREST);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool MonitorTopology::GetMonitorRect(HMONITOR monitor, RECT& rect) const
|
||||||
|
{
|
||||||
|
// First try direct HMONITOR comparison
|
||||||
|
for (const auto& monitorInfo : m_monitors)
|
||||||
|
{
|
||||||
|
if (monitorInfo.hMonitor == monitor)
|
||||||
|
{
|
||||||
|
rect = monitorInfo.rect;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: If direct comparison fails, try matching by current monitor info
|
||||||
|
MONITORINFO mi{};
|
||||||
|
mi.cbSize = sizeof(MONITORINFO);
|
||||||
|
if (GetMonitorInfo(monitor, &mi))
|
||||||
|
{
|
||||||
|
for (const auto& monitorInfo : m_monitors)
|
||||||
|
{
|
||||||
|
if (monitorInfo.rect.left == mi.rcMonitor.left &&
|
||||||
|
monitorInfo.rect.top == mi.rcMonitor.top &&
|
||||||
|
monitorInfo.rect.right == mi.rcMonitor.right &&
|
||||||
|
monitorInfo.rect.bottom == mi.rcMonitor.bottom)
|
||||||
|
{
|
||||||
|
rect = monitorInfo.rect;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
HMONITOR MonitorTopology::GetMonitorFromRect(const RECT& rect) const
|
||||||
|
{
|
||||||
|
return MonitorFromRect(&rect, MONITOR_DEFAULTTONEAREST);
|
||||||
|
}
|
||||||
|
|
||||||
|
int MonitorTopology::GetMonitorIndex(HMONITOR monitor) const
|
||||||
|
{
|
||||||
|
// First try direct HMONITOR comparison (fast and accurate)
|
||||||
|
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||||
|
{
|
||||||
|
if (m_monitors[i].hMonitor == monitor)
|
||||||
|
{
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: If direct comparison fails (e.g., handle changed after display reconfiguration),
|
||||||
|
// try matching by position. Get the monitor's current rect and find matching stored rect.
|
||||||
|
MONITORINFO mi{};
|
||||||
|
mi.cbSize = sizeof(MONITORINFO);
|
||||||
|
if (GetMonitorInfo(monitor, &mi))
|
||||||
|
{
|
||||||
|
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||||
|
{
|
||||||
|
// Match by rect bounds
|
||||||
|
if (m_monitors[i].rect.left == mi.rcMonitor.left &&
|
||||||
|
m_monitors[i].rect.top == mi.rcMonitor.top &&
|
||||||
|
m_monitors[i].rect.right == mi.rcMonitor.right &&
|
||||||
|
m_monitors[i].rect.bottom == mi.rcMonitor.bottom)
|
||||||
|
{
|
||||||
|
Logger::trace(L"GetMonitorIndex: Found monitor {} via rect fallback (handle changed from {} to {})",
|
||||||
|
i, reinterpret_cast<uintptr_t>(m_monitors[i].hMonitor), reinterpret_cast<uintptr_t>(monitor));
|
||||||
|
return static_cast<int>(i);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log all stored monitors vs the requested one for debugging
|
||||||
|
Logger::warn(L"GetMonitorIndex: No match found. Requested monitor rect=({},{},{},{})",
|
||||||
|
mi.rcMonitor.left, mi.rcMonitor.top, mi.rcMonitor.right, mi.rcMonitor.bottom);
|
||||||
|
for (size_t i = 0; i < m_monitors.size(); ++i)
|
||||||
|
{
|
||||||
|
Logger::warn(L" Stored monitor {}: rect=({},{},{},{})",
|
||||||
|
i, m_monitors[i].rect.left, m_monitors[i].rect.top,
|
||||||
|
m_monitors[i].rect.right, m_monitors[i].rect.bottom);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Logger::warn(L"GetMonitorIndex: GetMonitorInfo failed for handle {}", reinterpret_cast<uintptr_t>(monitor));
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1; // Not found
|
||||||
|
}
|
||||||
|
|
||||||
106
src/modules/MouseUtils/CursorWrap/MonitorTopology.h
Normal file
106
src/modules/MouseUtils/CursorWrap/MonitorTopology.h
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// Copyright (c) Microsoft Corporation
|
||||||
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
#include <windows.h>
|
||||||
|
#include <vector>
|
||||||
|
#include <map>
|
||||||
|
|
||||||
|
// Monitor information structure
|
||||||
|
struct MonitorInfo
|
||||||
|
{
|
||||||
|
HMONITOR hMonitor; // Direct handle for accurate lookup after display changes
|
||||||
|
RECT rect;
|
||||||
|
bool isPrimary;
|
||||||
|
int monitorId;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Edge type enumeration
|
||||||
|
enum class EdgeType
|
||||||
|
{
|
||||||
|
Left = 0,
|
||||||
|
Right = 1,
|
||||||
|
Top = 2,
|
||||||
|
Bottom = 3
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wrap mode enumeration (matches Settings UI dropdown)
|
||||||
|
enum class WrapMode
|
||||||
|
{
|
||||||
|
Both = 0, // Wrap in both directions
|
||||||
|
VerticalOnly = 1, // Only wrap top/bottom
|
||||||
|
HorizontalOnly = 2 // Only wrap left/right
|
||||||
|
};
|
||||||
|
|
||||||
|
// Represents a single edge of a monitor
|
||||||
|
struct MonitorEdge
|
||||||
|
{
|
||||||
|
int monitorIndex; // Index into m_monitors (stable across display changes)
|
||||||
|
EdgeType type;
|
||||||
|
int start; // For vertical edges: Y start; horizontal: X start
|
||||||
|
int end; // For vertical edges: Y end; horizontal: X end
|
||||||
|
int position; // For vertical edges: X coord; horizontal: Y coord
|
||||||
|
bool isOuter; // True if no adjacent monitor touches this edge
|
||||||
|
};
|
||||||
|
|
||||||
|
// Monitor topology helper - manages edge-based monitor layout
|
||||||
|
struct MonitorTopology
|
||||||
|
{
|
||||||
|
void Initialize(const std::vector<MonitorInfo>& monitors);
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
// Get the wrap destination point for a cursor on an outer edge
|
||||||
|
POINT GetWrapDestination(HMONITOR fromMonitor, const POINT& cursorPos, EdgeType edgeType) const;
|
||||||
|
|
||||||
|
// Get monitor at point (helper)
|
||||||
|
HMONITOR GetMonitorFromPoint(const POINT& pt) const;
|
||||||
|
|
||||||
|
// Get monitor rectangle (helper)
|
||||||
|
bool GetMonitorRect(HMONITOR monitor, RECT& rect) const;
|
||||||
|
|
||||||
|
// Get outer edges collection (for debugging)
|
||||||
|
const std::vector<MonitorEdge>& GetOuterEdges() const { return m_outerEdges; }
|
||||||
|
|
||||||
|
// Detect gaps between monitors that should be snapped together
|
||||||
|
struct GapInfo {
|
||||||
|
int monitor1Index;
|
||||||
|
int monitor2Index;
|
||||||
|
int horizontalGap;
|
||||||
|
int verticalOverlap;
|
||||||
|
};
|
||||||
|
std::vector<GapInfo> DetectMonitorGaps() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<MonitorInfo> m_monitors;
|
||||||
|
std::vector<MonitorEdge> m_outerEdges;
|
||||||
|
|
||||||
|
// Map from (monitor index, edge type) to edge info
|
||||||
|
// Using monitor index instead of HMONITOR because HMONITOR handles can change
|
||||||
|
// when monitors are added/removed dynamically
|
||||||
|
std::map<std::pair<int, EdgeType>, MonitorEdge> m_edgeMap;
|
||||||
|
|
||||||
|
// Helper to resolve HMONITOR to monitor index at runtime
|
||||||
|
int GetMonitorIndex(HMONITOR monitor) const;
|
||||||
|
|
||||||
|
// Helper to get consistent HMONITOR from RECT
|
||||||
|
HMONITOR GetMonitorFromRect(const RECT& rect) const;
|
||||||
|
|
||||||
|
void BuildEdgeMap();
|
||||||
|
void IdentifyOuterEdges();
|
||||||
|
|
||||||
|
// 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
|
||||||
|
MonitorEdge FindOppositeOuterEdge(EdgeType fromEdge, int relativePosition) 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;
|
||||||
|
};
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -22,11 +22,15 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
[JsonPropertyName("disable_wrap_during_drag")]
|
[JsonPropertyName("disable_wrap_during_drag")]
|
||||||
public BoolProperty DisableWrapDuringDrag { get; set; }
|
public BoolProperty DisableWrapDuringDrag { get; set; }
|
||||||
|
|
||||||
|
[JsonPropertyName("wrap_mode")]
|
||||||
|
public IntProperty WrapMode { get; set; }
|
||||||
|
|
||||||
public CursorWrapProperties()
|
public CursorWrapProperties()
|
||||||
{
|
{
|
||||||
ActivationShortcut = DefaultActivationShortcut;
|
ActivationShortcut = DefaultActivationShortcut;
|
||||||
AutoActivate = new BoolProperty(false);
|
AutoActivate = new BoolProperty(false);
|
||||||
DisableWrapDuringDrag = new BoolProperty(true);
|
DisableWrapDuringDrag = new BoolProperty(true);
|
||||||
|
WrapMode = new IntProperty(0); // 0=Both (default), 1=VerticalOnly, 2=HorizontalOnly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,16 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
// This can be utilized in the future if the settings.json file is to be modified/deleted.
|
// This can be utilized in the future if the settings.json file is to be modified/deleted.
|
||||||
public bool UpgradeSettingsConfiguration()
|
public bool UpgradeSettingsConfiguration()
|
||||||
{
|
{
|
||||||
return false;
|
bool settingsUpgraded = false;
|
||||||
|
|
||||||
|
// Add WrapMode property if it doesn't exist (for users upgrading from older versions)
|
||||||
|
if (Properties.WrapMode == null)
|
||||||
|
{
|
||||||
|
Properties.WrapMode = new IntProperty(0); // Default to Both
|
||||||
|
settingsUpgraded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsUpgraded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,13 @@
|
|||||||
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
|
<tkcontrols:SettingsCard ContentAlignment="Left" IsEnabled="{x:Bind ViewModel.IsCursorWrapEnabled, Mode=OneWay}">
|
||||||
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableWrapDuringDrag" IsChecked="{x:Bind ViewModel.CursorWrapDisableWrapDuringDrag, Mode=TwoWay}" />
|
<CheckBox x:Uid="MouseUtils_CursorWrap_DisableWrapDuringDrag" IsChecked="{x:Bind ViewModel.CursorWrapDisableWrapDuringDrag, Mode=TwoWay}" />
|
||||||
</tkcontrols:SettingsCard>
|
</tkcontrols:SettingsCard>
|
||||||
|
<tkcontrols:SettingsCard Name="MouseUtilsCursorWrapWrapMode" x:Uid="MouseUtils_CursorWrap_WrapMode">
|
||||||
|
<ComboBox MinWidth="{StaticResource SettingActionControlMinWidth}" SelectedIndex="{x:Bind Path=ViewModel.CursorWrapWrapMode, Mode=TwoWay}">
|
||||||
|
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_Both" />
|
||||||
|
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_VerticalOnly" />
|
||||||
|
<ComboBoxItem x:Uid="MouseUtils_CursorWrap_WrapMode_HorizontalOnly" />
|
||||||
|
</ComboBox>
|
||||||
|
</tkcontrols:SettingsCard>
|
||||||
</tkcontrols:SettingsExpander.Items>
|
</tkcontrols:SettingsExpander.Items>
|
||||||
</tkcontrols:SettingsExpander>
|
</tkcontrols:SettingsExpander>
|
||||||
</controls:SettingsGroup>
|
</controls:SettingsGroup>
|
||||||
|
|||||||
@@ -2728,6 +2728,18 @@ From there, simply click on one of the supported files in the File Explorer and
|
|||||||
<data name="MouseUtils_CursorWrap_AutoActivate.Content" xml:space="preserve">
|
<data name="MouseUtils_CursorWrap_AutoActivate.Content" xml:space="preserve">
|
||||||
<value>Automatically activate on utility startup</value>
|
<value>Automatically activate on utility startup</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="MouseUtils_CursorWrap_WrapMode.Header" xml:space="preserve">
|
||||||
|
<value>Wrap mode</value>
|
||||||
|
</data>
|
||||||
|
<data name="MouseUtils_CursorWrap_WrapMode_VerticalOnly.Content" xml:space="preserve">
|
||||||
|
<value>Vertical only</value>
|
||||||
|
</data>
|
||||||
|
<data name="MouseUtils_CursorWrap_WrapMode_HorizontalOnly.Content" xml:space="preserve">
|
||||||
|
<value>Horizontal only</value>
|
||||||
|
</data>
|
||||||
|
<data name="MouseUtils_CursorWrap_WrapMode_Both.Content" xml:space="preserve">
|
||||||
|
<value>Vertical and horizontal</value>
|
||||||
|
</data>
|
||||||
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Text" xml:space="preserve">
|
<data name="Oobe_MouseUtils_MousePointerCrosshairs.Text" xml:space="preserve">
|
||||||
<value>Mouse Pointer Crosshairs</value>
|
<value>Mouse Pointer Crosshairs</value>
|
||||||
<comment>Mouse as in the hardware peripheral.</comment>
|
<comment>Mouse as in the hardware peripheral.</comment>
|
||||||
|
|||||||
@@ -113,6 +113,9 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
// Null-safe access in case property wasn't upgraded yet - default to TRUE
|
// Null-safe access in case property wasn't upgraded yet - default to TRUE
|
||||||
_cursorWrapDisableWrapDuringDrag = CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag?.Value ?? true;
|
_cursorWrapDisableWrapDuringDrag = CursorWrapSettingsConfig.Properties.DisableWrapDuringDrag?.Value ?? true;
|
||||||
|
|
||||||
|
// Null-safe access in case property wasn't upgraded yet - default to 0 (Both)
|
||||||
|
_cursorWrapWrapMode = CursorWrapSettingsConfig.Properties.WrapMode?.Value ?? 0;
|
||||||
|
|
||||||
int isEnabled = 0;
|
int isEnabled = 0;
|
||||||
|
|
||||||
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
|
Utilities.NativeMethods.SystemParametersInfo(Utilities.NativeMethods.SPI_GETCLIENTAREAANIMATION, 0, ref isEnabled, 0);
|
||||||
@@ -1083,6 +1086,34 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public int CursorWrapWrapMode
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _cursorWrapWrapMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != _cursorWrapWrapMode)
|
||||||
|
{
|
||||||
|
_cursorWrapWrapMode = value;
|
||||||
|
|
||||||
|
// Ensure the property exists before setting value
|
||||||
|
if (CursorWrapSettingsConfig.Properties.WrapMode == null)
|
||||||
|
{
|
||||||
|
CursorWrapSettingsConfig.Properties.WrapMode = new IntProperty(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CursorWrapSettingsConfig.Properties.WrapMode.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifyCursorWrapPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
|
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(propertyName);
|
OnPropertyChanged(propertyName);
|
||||||
@@ -1154,5 +1185,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
private bool _isCursorWrapEnabled;
|
private bool _isCursorWrapEnabled;
|
||||||
private bool _cursorWrapAutoActivate;
|
private bool _cursorWrapAutoActivate;
|
||||||
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
|
private bool _cursorWrapDisableWrapDuringDrag; // Will be initialized in constructor from settings
|
||||||
|
private int _cursorWrapWrapMode; // 0=Both, 1=VerticalOnly, 2=HorizontalOnly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user