mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-02-19 01:30:03 +01:00
Compare commits
24 Commits
dev/snickl
...
stable
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
034759f949 | ||
|
|
934c3bbce9 | ||
|
|
ac548297c9 | ||
|
|
17a215d321 | ||
|
|
1b6a8c54ff | ||
|
|
67518dd754 | ||
|
|
18d1fd568c | ||
|
|
3eae35f356 | ||
|
|
e349779766 | ||
|
|
5466ab6cf8 | ||
|
|
bf19bdc1ee | ||
|
|
2441621b80 | ||
|
|
1ca9d10ff5 | ||
|
|
b438f15f6e | ||
|
|
48de981f50 | ||
|
|
9ab6559fac | ||
|
|
8cc32d3098 | ||
|
|
3b7eedfb67 | ||
|
|
d7e1b18ba4 | ||
|
|
0206fdbec1 | ||
|
|
bdaf644f02 | ||
|
|
329c8c2616 | ||
|
|
b081e413b1 | ||
|
|
290fa01adf |
62
.github/actions/spell-check/expect.txt
vendored
62
.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
|
||||||
@@ -99,7 +98,6 @@ ASYNCWINDOWPLACEMENT
|
|||||||
ASYNCWINDOWPOS
|
ASYNCWINDOWPOS
|
||||||
atl
|
atl
|
||||||
ATRIOX
|
ATRIOX
|
||||||
ATX
|
|
||||||
aumid
|
aumid
|
||||||
authenticode
|
authenticode
|
||||||
AUTOBUDDY
|
AUTOBUDDY
|
||||||
@@ -216,8 +214,10 @@ cim
|
|||||||
CImage
|
CImage
|
||||||
cla
|
cla
|
||||||
CLASSDC
|
CLASSDC
|
||||||
|
classguid
|
||||||
classmethod
|
classmethod
|
||||||
CLASSNOTAVAILABLE
|
CLASSNOTAVAILABLE
|
||||||
|
claude
|
||||||
CLEARTYPE
|
CLEARTYPE
|
||||||
clickable
|
clickable
|
||||||
clickonce
|
clickonce
|
||||||
@@ -256,7 +256,6 @@ colorhistory
|
|||||||
colorhistorylimit
|
colorhistorylimit
|
||||||
COLORKEY
|
COLORKEY
|
||||||
colorref
|
colorref
|
||||||
Convs
|
|
||||||
comctl
|
comctl
|
||||||
comdlg
|
comdlg
|
||||||
comexp
|
comexp
|
||||||
@@ -277,6 +276,7 @@ CONTEXTHELP
|
|||||||
CONTEXTMENUHANDLER
|
CONTEXTMENUHANDLER
|
||||||
contractversion
|
contractversion
|
||||||
CONTROLPARENT
|
CONTROLPARENT
|
||||||
|
Convs
|
||||||
copiedcolorrepresentation
|
copiedcolorrepresentation
|
||||||
coppied
|
coppied
|
||||||
copyable
|
copyable
|
||||||
@@ -292,7 +292,6 @@ cpcontrols
|
|||||||
cph
|
cph
|
||||||
cplusplus
|
cplusplus
|
||||||
CPower
|
CPower
|
||||||
cppcoreguidelines
|
|
||||||
cpptools
|
cpptools
|
||||||
cppvsdbg
|
cppvsdbg
|
||||||
cppwinrt
|
cppwinrt
|
||||||
@@ -321,7 +320,7 @@ CURRENTDIR
|
|||||||
CURSORINFO
|
CURSORINFO
|
||||||
cursorpos
|
cursorpos
|
||||||
CURSORSHOWING
|
CURSORSHOWING
|
||||||
CURSORWRAP
|
cursorwrap
|
||||||
customaction
|
customaction
|
||||||
CUSTOMACTIONTEST
|
CUSTOMACTIONTEST
|
||||||
CUSTOMFORMATPLACEHOLDER
|
CUSTOMFORMATPLACEHOLDER
|
||||||
@@ -344,12 +343,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
|
||||||
@@ -367,8 +368,7 @@ DEFAULTICON
|
|||||||
defaultlib
|
defaultlib
|
||||||
DEFAULTONLY
|
DEFAULTONLY
|
||||||
DEFAULTSIZE
|
DEFAULTSIZE
|
||||||
DEFAULTTONEAREST
|
defaulttonearest
|
||||||
Defaulttonearest
|
|
||||||
DEFAULTTONULL
|
DEFAULTTONULL
|
||||||
DEFAULTTOPRIMARY
|
DEFAULTTOPRIMARY
|
||||||
DEFERERASE
|
DEFERERASE
|
||||||
@@ -390,14 +390,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
|
||||||
@@ -540,7 +545,6 @@ fdx
|
|||||||
FErase
|
FErase
|
||||||
fesf
|
fesf
|
||||||
FFFF
|
FFFF
|
||||||
FInc
|
|
||||||
Figma
|
Figma
|
||||||
FILEEXPLORER
|
FILEEXPLORER
|
||||||
fileexploreraddons
|
fileexploreraddons
|
||||||
@@ -561,6 +565,7 @@ FILESYSPATH
|
|||||||
Filetime
|
Filetime
|
||||||
FILEVERSION
|
FILEVERSION
|
||||||
FILTERMODE
|
FILTERMODE
|
||||||
|
FInc
|
||||||
findfast
|
findfast
|
||||||
findmymouse
|
findmymouse
|
||||||
FIXEDFILEINFO
|
FIXEDFILEINFO
|
||||||
@@ -662,13 +667,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
|
||||||
@@ -744,9 +750,9 @@ HWNDPARENT
|
|||||||
HWNDPREV
|
HWNDPREV
|
||||||
hyjiacan
|
hyjiacan
|
||||||
IAI
|
IAI
|
||||||
|
icf
|
||||||
ICONERROR
|
ICONERROR
|
||||||
ICONLOCATION
|
ICONLOCATION
|
||||||
icf
|
|
||||||
IDCANCEL
|
IDCANCEL
|
||||||
IDD
|
IDD
|
||||||
idk
|
idk
|
||||||
@@ -837,8 +843,8 @@ jeli
|
|||||||
jfif
|
jfif
|
||||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||||
jjw
|
jjw
|
||||||
JOBOBJECT
|
|
||||||
jobject
|
jobject
|
||||||
|
JOBOBJECT
|
||||||
jpe
|
jpe
|
||||||
jpnime
|
jpnime
|
||||||
Jsons
|
Jsons
|
||||||
@@ -882,6 +888,7 @@ Ldr
|
|||||||
LEFTALIGN
|
LEFTALIGN
|
||||||
LEFTSCROLLBAR
|
LEFTSCROLLBAR
|
||||||
LEFTTEXT
|
LEFTTEXT
|
||||||
|
leftclick
|
||||||
LError
|
LError
|
||||||
LEVELID
|
LEVELID
|
||||||
LExit
|
LExit
|
||||||
@@ -924,9 +931,9 @@ LOWORD
|
|||||||
lparam
|
lparam
|
||||||
LPBITMAPINFOHEADER
|
LPBITMAPINFOHEADER
|
||||||
LPCFHOOKPROC
|
LPCFHOOKPROC
|
||||||
|
lpch
|
||||||
LPCITEMIDLIST
|
LPCITEMIDLIST
|
||||||
LPCLSID
|
LPCLSID
|
||||||
lpch
|
|
||||||
lpcmi
|
lpcmi
|
||||||
LPCMINVOKECOMMANDINFO
|
LPCMINVOKECOMMANDINFO
|
||||||
LPCREATESTRUCT
|
LPCREATESTRUCT
|
||||||
@@ -942,6 +949,7 @@ LPMONITORINFO
|
|||||||
LPOSVERSIONINFOEXW
|
LPOSVERSIONINFOEXW
|
||||||
LPQUERY
|
LPQUERY
|
||||||
lprc
|
lprc
|
||||||
|
LPrivate
|
||||||
LPSAFEARRAY
|
LPSAFEARRAY
|
||||||
lpstr
|
lpstr
|
||||||
lpsz
|
lpsz
|
||||||
@@ -951,7 +959,6 @@ lptpm
|
|||||||
LPTR
|
LPTR
|
||||||
LPTSTR
|
LPTSTR
|
||||||
lpv
|
lpv
|
||||||
LPrivate
|
|
||||||
LPW
|
LPW
|
||||||
lpwcx
|
lpwcx
|
||||||
lpwndpl
|
lpwndpl
|
||||||
@@ -995,20 +1002,22 @@ 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
|
||||||
MERGECOPY
|
MERGECOPY
|
||||||
MERGEPAINT
|
MERGEPAINT
|
||||||
|
Metacharacter
|
||||||
metadatamatters
|
metadatamatters
|
||||||
Metadatas
|
Metadatas
|
||||||
|
Metacharacter
|
||||||
metafile
|
metafile
|
||||||
mfc
|
mfc
|
||||||
Mgmt
|
Mgmt
|
||||||
@@ -1037,6 +1046,7 @@ mmsys
|
|||||||
mobileredirect
|
mobileredirect
|
||||||
mockapi
|
mockapi
|
||||||
MODALFRAME
|
MODALFRAME
|
||||||
|
modelcontextprotocol
|
||||||
MODESPRUNED
|
MODESPRUNED
|
||||||
MONITORENUMPROC
|
MONITORENUMPROC
|
||||||
MONITORINFO
|
MONITORINFO
|
||||||
@@ -1080,9 +1090,9 @@ MSLLHOOKSTRUCT
|
|||||||
Mso
|
Mso
|
||||||
msrc
|
msrc
|
||||||
msstore
|
msstore
|
||||||
|
mstsc
|
||||||
msvcp
|
msvcp
|
||||||
MT
|
MT
|
||||||
mstsc
|
|
||||||
MTND
|
MTND
|
||||||
MULTIPLEUSE
|
MULTIPLEUSE
|
||||||
multizone
|
multizone
|
||||||
@@ -1092,11 +1102,11 @@ muxxc
|
|||||||
muxxh
|
muxxh
|
||||||
MVPs
|
MVPs
|
||||||
mvvm
|
mvvm
|
||||||
myorg
|
|
||||||
myrepo
|
|
||||||
MVVMTK
|
MVVMTK
|
||||||
MWBEx
|
MWBEx
|
||||||
MYICON
|
MYICON
|
||||||
|
myorg
|
||||||
|
myrepo
|
||||||
NAMECHANGE
|
NAMECHANGE
|
||||||
namespaceanddescendants
|
namespaceanddescendants
|
||||||
nao
|
nao
|
||||||
@@ -1237,10 +1247,8 @@ opencode
|
|||||||
OPENFILENAME
|
OPENFILENAME
|
||||||
openrdp
|
openrdp
|
||||||
opensource
|
opensource
|
||||||
openxmlformats
|
|
||||||
ollama
|
|
||||||
onnx
|
|
||||||
openurl
|
openurl
|
||||||
|
openxmlformats
|
||||||
OPTIMIZEFORINVOKE
|
OPTIMIZEFORINVOKE
|
||||||
ORPHANEDDIALOGTITLE
|
ORPHANEDDIALOGTITLE
|
||||||
ORSCANS
|
ORSCANS
|
||||||
@@ -1456,7 +1464,6 @@ rbhid
|
|||||||
Rbuttondown
|
Rbuttondown
|
||||||
rclsid
|
rclsid
|
||||||
RCZOOMIT
|
RCZOOMIT
|
||||||
remotedesktop
|
|
||||||
rdp
|
rdp
|
||||||
RDW
|
RDW
|
||||||
READMODE
|
READMODE
|
||||||
@@ -1485,6 +1492,7 @@ remappings
|
|||||||
REMAPSUCCESSFUL
|
REMAPSUCCESSFUL
|
||||||
REMAPUNSUCCESSFUL
|
REMAPUNSUCCESSFUL
|
||||||
Remotable
|
Remotable
|
||||||
|
remotedesktop
|
||||||
remoteip
|
remoteip
|
||||||
Removelnk
|
Removelnk
|
||||||
renamable
|
renamable
|
||||||
@@ -1518,8 +1526,8 @@ RIGHTSCROLLBAR
|
|||||||
riid
|
riid
|
||||||
RKey
|
RKey
|
||||||
RNumber
|
RNumber
|
||||||
rop
|
|
||||||
rollups
|
rollups
|
||||||
|
rop
|
||||||
ROUNDSMALL
|
ROUNDSMALL
|
||||||
ROWSETEXT
|
ROWSETEXT
|
||||||
rpcrt
|
rpcrt
|
||||||
@@ -1757,8 +1765,7 @@ SVGIO
|
|||||||
svgz
|
svgz
|
||||||
SVSI
|
SVSI
|
||||||
SWFO
|
SWFO
|
||||||
SWP
|
swp
|
||||||
Swp
|
|
||||||
SWPNOSIZE
|
SWPNOSIZE
|
||||||
SWPNOZORDER
|
SWPNOZORDER
|
||||||
SWRESTORE
|
SWRESTORE
|
||||||
@@ -1777,8 +1784,7 @@ SYSKEY
|
|||||||
syskeydown
|
syskeydown
|
||||||
SYSKEYUP
|
SYSKEYUP
|
||||||
SYSLIB
|
SYSLIB
|
||||||
SYSMENU
|
sysmenu
|
||||||
Sysmenu
|
|
||||||
systemai
|
systemai
|
||||||
SYSTEMAPPS
|
SYSTEMAPPS
|
||||||
SYSTEMMODAL
|
SYSTEMMODAL
|
||||||
@@ -1882,9 +1888,9 @@ uitests
|
|||||||
UITo
|
UITo
|
||||||
ULONGLONG
|
ULONGLONG
|
||||||
Ultrawide
|
Ultrawide
|
||||||
ums
|
|
||||||
UMax
|
UMax
|
||||||
UMin
|
UMin
|
||||||
|
ums
|
||||||
uncompilable
|
uncompilable
|
||||||
UNCPRIORITY
|
UNCPRIORITY
|
||||||
UNDNAME
|
UNDNAME
|
||||||
|
|||||||
@@ -504,6 +504,14 @@ jobs:
|
|||||||
Remove-Item -Force -Recurse "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore
|
Remove-Item -Force -Recurse "$(JobOutputDirectory)/_appx" -ErrorAction:Ignore
|
||||||
displayName: Re-pack the new CmdPal package after signing
|
displayName: Re-pack the new CmdPal package after signing
|
||||||
|
|
||||||
|
- pwsh: |
|
||||||
|
$testsPath = "$(Build.SourcesDirectory)/$(BuildPlatform)/$(BuildConfiguration)/tests"
|
||||||
|
if (Test-Path $testsPath) {
|
||||||
|
Remove-Item -Path $testsPath -Recurse -Force
|
||||||
|
Write-Host "Removed tests folder to reduce signing workload: $testsPath"
|
||||||
|
}
|
||||||
|
displayName: Remove tests folder before signing
|
||||||
|
|
||||||
- template: steps-esrp-signing.yml
|
- template: steps-esrp-signing.yml
|
||||||
parameters:
|
parameters:
|
||||||
displayName: Sign Core PowerToys
|
displayName: Sign Core PowerToys
|
||||||
|
|||||||
@@ -26,7 +26,7 @@
|
|||||||
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Converters" Version="8.2.250402" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
<PackageVersion Include="CommunityToolkit.WinUI.Extensions" Version="8.2.250402" />
|
||||||
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
<PackageVersion Include="CommunityToolkit.WinUI.UI.Controls.DataGrid" Version="7.1.2" />
|
||||||
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260107-build.2454" />
|
<PackageVersion Include="CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock" Version="0.1.260116-build.2514" />
|
||||||
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
<PackageVersion Include="ControlzEx" Version="6.0.0" />
|
||||||
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
<PackageVersion Include="HelixToolkit" Version="2.24.0" />
|
||||||
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
<PackageVersion Include="HelixToolkit.Core.Wpf" Version="2.24.0" />
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||||
<RegistryValue Type="string" Name="Module_CmdPal" Value="" KeyPath="yes" />
|
<RegistryValue Type="string" Name="Module_CmdPal" Value="" KeyPath="yes" />
|
||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
|
<RemoveFile Id="RemoveOldCmdPalMsix" Name="Microsoft.CmdPal.UI_*.msix" On="install" />
|
||||||
<?if $(sys.BUILDARCH) = x64 ?>
|
<?if $(sys.BUILDARCH) = x64 ?>
|
||||||
<File Id="Microsoft.CmdPal.UI___var.CmdPalVersion_._x64.msix" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_x64.msix" />
|
<File Id="Microsoft.CmdPal.UI___var.CmdPalVersion_._x64.msix" Source="$(var.CmdPalBuildDir)AppPackages\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_Test\Microsoft.CmdPal.UI_$(var.CmdPalVersion)_x64.msix" />
|
||||||
<?else?>
|
<?else?>
|
||||||
|
|||||||
@@ -146,7 +146,7 @@
|
|||||||
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||||
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
<Custom Action="UninstallCommandNotFound" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||||
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
|
<Custom Action="UpgradeCommandNotFound" After="InstallFiles" Condition="WIX_UPGRADE_DETECTED" />
|
||||||
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
<Custom Action="UninstallPackageIdentityMSIX" Before="RemoveFiles" Condition="Installed AND (REMOVE="ALL")" />
|
||||||
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
<Custom Action="UninstallServicesTask" After="InstallFinalize" Condition="Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")" />
|
||||||
<!-- TODO: Use to activate embedded MSIX -->
|
<!-- TODO: Use to activate embedded MSIX -->
|
||||||
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
|
<!--<Custom Action="UninstallEmbeddedMSIXTask" After="InstallFinalize">
|
||||||
|
|||||||
@@ -367,6 +367,12 @@
|
|||||||
</RegistryKey>
|
</RegistryKey>
|
||||||
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
|
<File Id="BgcodePreviewHandler_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\PowerToys.BgcodePreviewHandler.resources.dll" />
|
||||||
</Component>
|
</Component>
|
||||||
|
<Component Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Directory="Resource$(var.IdSafeLanguage)INSTALLFOLDER" Guid="$(var.CompGUIDPrefix)23">
|
||||||
|
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\powertoys\components">
|
||||||
|
<RegistryValue Type="string" Name="CmdPalExtPowerToys_$(var.IdSafeLanguage)_Component" Value="" KeyPath="yes" />
|
||||||
|
</RegistryKey>
|
||||||
|
<File Id="CmdPalExtPowerToys_$(var.IdSafeLanguage)_File" Source="$(var.BinDir)\$(var.Language)\Microsoft.CmdPal.Ext.PowerToys.resources.dll" />
|
||||||
|
</Component>
|
||||||
<?undef IdSafeLanguage?>
|
<?undef IdSafeLanguage?>
|
||||||
<?undef CompGUIDPrefix?>
|
<?undef CompGUIDPrefix?>
|
||||||
<?endforeach?>
|
<?endforeach?>
|
||||||
|
|||||||
@@ -16,13 +16,50 @@ DWORD WINAPI _checkTheme(LPVOID lpParam)
|
|||||||
|
|
||||||
void ThemeListener::AddChangedHandler(THEME_HANDLE handle)
|
void ThemeListener::AddChangedHandler(THEME_HANDLE handle)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||||
handles.push_back(handle);
|
handles.push_back(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeListener::DelChangedHandler(THEME_HANDLE handle)
|
void ThemeListener::DelChangedHandler(THEME_HANDLE handle)
|
||||||
{
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||||
auto it = std::find(handles.begin(), handles.end(), handle);
|
auto it = std::find(handles.begin(), handles.end(), handle);
|
||||||
handles.erase(it);
|
if (it != handles.end())
|
||||||
|
{
|
||||||
|
handles.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeListener::AddAppThemeChangedHandler(THEME_HANDLE handle)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||||
|
appThemeHandles.push_back(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeListener::DelAppThemeChangedHandler(THEME_HANDLE handle)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||||
|
auto it = std::find(appThemeHandles.begin(), appThemeHandles.end(), handle);
|
||||||
|
if (it != appThemeHandles.end())
|
||||||
|
{
|
||||||
|
appThemeHandles.erase(it);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeListener::AddSystemThemeChangedHandler(THEME_HANDLE handle)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||||
|
systemThemeHandles.push_back(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
void ThemeListener::DelSystemThemeChangedHandler(THEME_HANDLE handle)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||||
|
auto it = std::find(systemThemeHandles.begin(), systemThemeHandles.end(), handle);
|
||||||
|
if (it != systemThemeHandles.end())
|
||||||
|
{
|
||||||
|
systemThemeHandles.erase(it);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ThemeListener::CheckTheme()
|
void ThemeListener::CheckTheme()
|
||||||
@@ -48,13 +85,51 @@ void ThemeListener::CheckTheme()
|
|||||||
|
|
||||||
WaitForSingleObject(hEvent, INFINITE);
|
WaitForSingleObject(hEvent, INFINITE);
|
||||||
|
|
||||||
auto _theme = ThemeHelpers::GetAppTheme();
|
auto _appTheme = ThemeHelpers::GetAppTheme();
|
||||||
if (AppTheme != _theme)
|
auto _systemTheme = ThemeHelpers::GetSystemTheme();
|
||||||
|
|
||||||
|
bool appThemeChanged = (AppTheme != _appTheme);
|
||||||
|
bool systemThemeChanged = (SystemTheme != _systemTheme);
|
||||||
|
|
||||||
|
if (appThemeChanged || systemThemeChanged)
|
||||||
{
|
{
|
||||||
AppTheme = _theme;
|
AppTheme = _appTheme;
|
||||||
for (int i = 0; i < handles.size(); i++)
|
SystemTheme = _systemTheme;
|
||||||
|
|
||||||
|
// Copy handlers under lock, then invoke outside lock to avoid deadlock
|
||||||
|
std::vector<THEME_HANDLE> handlesCopy;
|
||||||
|
std::vector<THEME_HANDLE> appThemeHandlesCopy;
|
||||||
|
std::vector<THEME_HANDLE> systemThemeHandlesCopy;
|
||||||
|
|
||||||
{
|
{
|
||||||
handles[i]();
|
std::lock_guard<std::mutex> lock(handlesMutex);
|
||||||
|
handlesCopy = handles;
|
||||||
|
if (appThemeChanged)
|
||||||
|
{
|
||||||
|
appThemeHandlesCopy = appThemeHandles;
|
||||||
|
}
|
||||||
|
if (systemThemeChanged)
|
||||||
|
{
|
||||||
|
systemThemeHandlesCopy = systemThemeHandles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call generic handlers (backward compatible)
|
||||||
|
for (const auto& handler : handlesCopy)
|
||||||
|
{
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call app theme specific handlers
|
||||||
|
for (const auto& handler : appThemeHandlesCopy)
|
||||||
|
{
|
||||||
|
handler();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call system theme specific handlers
|
||||||
|
for (const auto& handler : systemThemeHandlesCopy)
|
||||||
|
{
|
||||||
|
handler();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
#include <windows.h>
|
#include <windows.h>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
typedef void (*THEME_HANDLE)();
|
typedef void (*THEME_HANDLE)();
|
||||||
DWORD WINAPI _checkTheme(LPVOID lpParam);
|
DWORD WINAPI _checkTheme(LPVOID lpParam);
|
||||||
@@ -14,6 +15,7 @@ public:
|
|||||||
ThemeListener()
|
ThemeListener()
|
||||||
{
|
{
|
||||||
AppTheme = ThemeHelpers::GetAppTheme();
|
AppTheme = ThemeHelpers::GetAppTheme();
|
||||||
|
SystemTheme = ThemeHelpers::GetSystemTheme();
|
||||||
dwThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_checkTheme, this, 0, &dwThreadId);
|
dwThreadHandle = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)_checkTheme, this, 0, &dwThreadId);
|
||||||
}
|
}
|
||||||
~ThemeListener()
|
~ThemeListener()
|
||||||
@@ -23,12 +25,20 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
Theme AppTheme;
|
Theme AppTheme;
|
||||||
|
Theme SystemTheme;
|
||||||
void ThemeListener::AddChangedHandler(THEME_HANDLE handle);
|
void ThemeListener::AddChangedHandler(THEME_HANDLE handle);
|
||||||
void ThemeListener::DelChangedHandler(THEME_HANDLE handle);
|
void ThemeListener::DelChangedHandler(THEME_HANDLE handle);
|
||||||
|
void ThemeListener::AddAppThemeChangedHandler(THEME_HANDLE handle);
|
||||||
|
void ThemeListener::DelAppThemeChangedHandler(THEME_HANDLE handle);
|
||||||
|
void ThemeListener::AddSystemThemeChangedHandler(THEME_HANDLE handle);
|
||||||
|
void ThemeListener::DelSystemThemeChangedHandler(THEME_HANDLE handle);
|
||||||
void CheckTheme();
|
void CheckTheme();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
HANDLE dwThreadHandle;
|
HANDLE dwThreadHandle;
|
||||||
DWORD dwThreadId;
|
DWORD dwThreadId;
|
||||||
std::vector<THEME_HANDLE> handles;
|
std::vector<THEME_HANDLE> handles;
|
||||||
|
std::vector<THEME_HANDLE> appThemeHandles;
|
||||||
|
std::vector<THEME_HANDLE> systemThemeHandles;
|
||||||
|
mutable std::mutex handlesMutex;
|
||||||
};
|
};
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
#include <appxpackaging.h>
|
#include <appxpackaging.h>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -337,6 +338,30 @@ namespace package
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Sort by package version in descending order (newest first)
|
||||||
|
std::sort(matchedFiles.begin(), matchedFiles.end(), [](const std::wstring& a, const std::wstring& b) {
|
||||||
|
std::wstring nameA, nameB;
|
||||||
|
PACKAGE_VERSION versionA{}, versionB{};
|
||||||
|
|
||||||
|
bool gotA = GetPackageNameAndVersionFromAppx(a, nameA, versionA);
|
||||||
|
bool gotB = GetPackageNameAndVersionFromAppx(b, nameB, versionB);
|
||||||
|
|
||||||
|
// Files that failed to parse go to the end
|
||||||
|
if (!gotA)
|
||||||
|
return false;
|
||||||
|
if (!gotB)
|
||||||
|
return true;
|
||||||
|
|
||||||
|
// Compare versions: Major, Minor, Build, Revision (descending)
|
||||||
|
if (versionA.Major != versionB.Major)
|
||||||
|
return versionA.Major > versionB.Major;
|
||||||
|
if (versionA.Minor != versionB.Minor)
|
||||||
|
return versionA.Minor > versionB.Minor;
|
||||||
|
if (versionA.Build != versionB.Build)
|
||||||
|
return versionA.Build > versionB.Build;
|
||||||
|
return versionA.Revision > versionB.Revision;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
catch (const std::exception& ex)
|
catch (const std::exception& ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (c) Microsoft Corporation.
|
<!-- Copyright (c) Microsoft Corporation.
|
||||||
Licensed under the MIT License. -->
|
Licensed under the MIT License. -->
|
||||||
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.18" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
<policyDefinitions xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||||
<policyNamespaces>
|
<policyNamespaces>
|
||||||
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
|
<target prefix="powertoys" namespace="Microsoft.Policies.PowerToys" />
|
||||||
</policyNamespaces>
|
</policyNamespaces>
|
||||||
<resources minRequiredRevision="1.18"/><!-- Last changed with PowerToys v0.96.0 -->
|
<resources minRequiredRevision="1.19"/><!-- Last changed with PowerToys v0.97.0 -->
|
||||||
<supportedOn>
|
<supportedOn>
|
||||||
<definitions>
|
<definitions>
|
||||||
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
|
<definition name="SUPPORTED_POWERTOYS_0_64_0" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0)"/>
|
||||||
@@ -27,6 +27,7 @@
|
|||||||
<definition name="SUPPORTED_POWERTOYS_0_89_0" displayName="$(string.SUPPORTED_POWERTOYS_0_89_0)"/>
|
<definition name="SUPPORTED_POWERTOYS_0_89_0" displayName="$(string.SUPPORTED_POWERTOYS_0_89_0)"/>
|
||||||
<definition name="SUPPORTED_POWERTOYS_0_90_0" displayName="$(string.SUPPORTED_POWERTOYS_0_90_0)"/>
|
<definition name="SUPPORTED_POWERTOYS_0_90_0" displayName="$(string.SUPPORTED_POWERTOYS_0_90_0)"/>
|
||||||
<definition name="SUPPORTED_POWERTOYS_0_96_0" displayName="$(string.SUPPORTED_POWERTOYS_0_96_0)"/>
|
<definition name="SUPPORTED_POWERTOYS_0_96_0" displayName="$(string.SUPPORTED_POWERTOYS_0_96_0)"/>
|
||||||
|
<definition name="SUPPORTED_POWERTOYS_0_97_0" displayName="$(string.SUPPORTED_POWERTOYS_0_97_0)"/>
|
||||||
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
|
<definition name="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1" displayName="$(string.SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1)"/>
|
||||||
</definitions>
|
</definitions>
|
||||||
</supportedOn>
|
</supportedOn>
|
||||||
@@ -338,6 +339,16 @@
|
|||||||
<decimal value="0" />
|
<decimal value="0" />
|
||||||
</disabledValue>
|
</disabledValue>
|
||||||
</policy>
|
</policy>
|
||||||
|
<policy name="ConfigureEnabledUtilityCursorWrap" class="Both" displayName="$(string.ConfigureEnabledUtilityCursorWrap)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityCursorWrap">
|
||||||
|
<parentCategory ref="PowerToys" />
|
||||||
|
<supportedOn ref="SUPPORTED_POWERTOYS_0_97_0" />
|
||||||
|
<enabledValue>
|
||||||
|
<decimal value="1" />
|
||||||
|
</enabledValue>
|
||||||
|
<disabledValue>
|
||||||
|
<decimal value="0" />
|
||||||
|
</disabledValue>
|
||||||
|
</policy>
|
||||||
<policy name="ConfigureEnabledUtilityFindMyMouse" class="Both" displayName="$(string.ConfigureEnabledUtilityFindMyMouse)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityFindMyMouse">
|
<policy name="ConfigureEnabledUtilityFindMyMouse" class="Both" displayName="$(string.ConfigureEnabledUtilityFindMyMouse)" explainText="$(string.ConfigureEnabledUtilityDescription)" key="Software\Policies\PowerToys" valueName="ConfigureEnabledUtilityFindMyMouse">
|
||||||
<parentCategory ref="PowerToys" />
|
<parentCategory ref="PowerToys" />
|
||||||
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />
|
<supportedOn ref="SUPPORTED_POWERTOYS_0_64_0" />
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
<!-- Copyright (c) Microsoft Corporation.
|
<!-- Copyright (c) Microsoft Corporation.
|
||||||
Licensed under the MIT License. -->
|
Licensed under the MIT License. -->
|
||||||
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.18" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
<policyDefinitionResources xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" revision="1.19" schemaVersion="1.0" xmlns="http://schemas.microsoft.com/GroupPolicy/2006/07/PolicyDefinitions">
|
||||||
<displayName>PowerToys</displayName>
|
<displayName>PowerToys</displayName>
|
||||||
<description>PowerToys</description>
|
<description>PowerToys</description>
|
||||||
<resources>
|
<resources>
|
||||||
@@ -34,6 +34,7 @@
|
|||||||
<string id="SUPPORTED_POWERTOYS_0_89_0">PowerToys version 0.89.0 or later</string>
|
<string id="SUPPORTED_POWERTOYS_0_89_0">PowerToys version 0.89.0 or later</string>
|
||||||
<string id="SUPPORTED_POWERTOYS_0_90_0">PowerToys version 0.90.0 or later</string>
|
<string id="SUPPORTED_POWERTOYS_0_90_0">PowerToys version 0.90.0 or later</string>
|
||||||
<string id="SUPPORTED_POWERTOYS_0_96_0">PowerToys version 0.96.0 or later</string>
|
<string id="SUPPORTED_POWERTOYS_0_96_0">PowerToys version 0.96.0 or later</string>
|
||||||
|
<string id="SUPPORTED_POWERTOYS_0_97_0">PowerToys version 0.97.0 or later</string>
|
||||||
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
|
<string id="SUPPORTED_POWERTOYS_0_64_0_TO_0_87_1">From PowerToys version 0.64.0 until PowerToys version 0.87.1</string>
|
||||||
|
|
||||||
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
|
<string id="ConfigureAllUtilityGlobalEnabledStateDescription">This policy configures the enabled state for all PowerToys utilities.
|
||||||
@@ -266,6 +267,7 @@ If you don't configure this policy, the user will be able to control the setting
|
|||||||
<string id="ConfigureEnabledUtilityKeyboardManager">Keyboard Manager: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityKeyboardManager">Keyboard Manager: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityFindMyMouse">Find My Mouse: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityFindMyMouse">Find My Mouse: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityMouseHighlighter">Mouse Highlighter: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityMouseHighlighter">Mouse Highlighter: Configure enabled state</string>
|
||||||
|
<string id="ConfigureEnabledUtilityCursorWrap">CursorWrap: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityMouseJump">Mouse Jump: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityMouseJump">Mouse Jump: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityMousePointerCrosshairs">Mouse Pointer Crosshairs: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityMousePointerCrosshairs">Mouse Pointer Crosshairs: Configure enabled state</string>
|
||||||
<string id="ConfigureEnabledUtilityMouseWithoutBorders">Mouse Without Borders: Configure enabled state</string>
|
<string id="ConfigureEnabledUtilityMouseWithoutBorders">Mouse Without Borders: Configure enabled state</string>
|
||||||
|
|||||||
@@ -250,7 +250,6 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
|||||||
|
|
||||||
Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute);
|
Logger::info(L"[LightSwitchService] Initialized at {:02d}:{:02d}.", st.wHour, st.wMinute);
|
||||||
stateManager.SyncInitialThemeState();
|
stateManager.SyncInitialThemeState();
|
||||||
stateManager.OnTick(nowMinutes);
|
|
||||||
|
|
||||||
// ────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────
|
||||||
// Worker Loop
|
// Worker Loop
|
||||||
@@ -281,7 +280,7 @@ DWORD WINAPI ServiceWorkerThread(LPVOID lpParam)
|
|||||||
GetLocalTime(&st);
|
GetLocalTime(&st);
|
||||||
nowMinutes = st.wHour * 60 + st.wMinute;
|
nowMinutes = st.wHour * 60 + st.wMinute;
|
||||||
DetectAndHandleExternalThemeChange(stateManager);
|
DetectAndHandleExternalThemeChange(stateManager);
|
||||||
stateManager.OnTick(nowMinutes);
|
stateManager.OnTick();
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ void LightSwitchStateManager::OnSettingsChanged()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called once per minute
|
// Called once per minute
|
||||||
void LightSwitchStateManager::OnTick(int currentMinutes)
|
void LightSwitchStateManager::OnTick()
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||||
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
|
if (_state.lastAppliedMode != ScheduleMode::FollowNightLight)
|
||||||
@@ -109,10 +109,14 @@ void LightSwitchStateManager::SyncInitialThemeState()
|
|||||||
std::lock_guard<std::mutex> lock(_stateMutex);
|
std::lock_guard<std::mutex> lock(_stateMutex);
|
||||||
_state.isSystemLightActive = GetCurrentSystemTheme();
|
_state.isSystemLightActive = GetCurrentSystemTheme();
|
||||||
_state.isAppsLightActive = GetCurrentAppsTheme();
|
_state.isAppsLightActive = GetCurrentAppsTheme();
|
||||||
|
_state.isNightLightActive = IsNightLightEnabled();
|
||||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current system theme ({})",
|
||||||
_state.isSystemLightActive ? L"light" : L"dark");
|
_state.isSystemLightActive ? L"light" : L"dark");
|
||||||
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
Logger::debug(L"[LightSwitchStateManager] Synced initial state to current apps theme ({})",
|
||||||
_state.isAppsLightActive ? L"light" : L"dark");
|
_state.isAppsLightActive ? L"light" : L"dark");
|
||||||
|
|
||||||
|
// This will ensure that the theme is applied according to current settings at startup
|
||||||
|
EvaluateAndApplyIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
static std::pair<int, int> update_sun_times(auto& settings)
|
static std::pair<int, int> update_sun_times(auto& settings)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ public:
|
|||||||
void OnSettingsChanged();
|
void OnSettingsChanged();
|
||||||
|
|
||||||
// Called every minute (from service worker tick).
|
// Called every minute (from service worker tick).
|
||||||
void OnTick(int currentMinutes);
|
void OnTick();
|
||||||
|
|
||||||
// Called when manual override is toggled (via shortcut or system change).
|
// Called when manual override is toggled (via shortcut or system change).
|
||||||
void OnManualOverride();
|
void OnManualOverride();
|
||||||
|
|||||||
@@ -10,7 +10,6 @@
|
|||||||
IsMaximizable="False"
|
IsMaximizable="False"
|
||||||
IsMinimizable="False"
|
IsMinimizable="False"
|
||||||
IsResizable="False"
|
IsResizable="False"
|
||||||
IsShownInSwitchers="False"
|
|
||||||
IsTitleBarVisible="False"
|
IsTitleBarVisible="False"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<winuiex:WindowEx.Backdrop>
|
<winuiex:WindowEx.Backdrop>
|
||||||
|
|||||||
@@ -52,12 +52,23 @@ namespace MeasureToolUI
|
|||||||
var presenter = _appWindow.Presenter as OverlappedPresenter;
|
var presenter = _appWindow.Presenter as OverlappedPresenter;
|
||||||
presenter.IsAlwaysOnTop = true;
|
presenter.IsAlwaysOnTop = true;
|
||||||
this.SetIsAlwaysOnTop(true);
|
this.SetIsAlwaysOnTop(true);
|
||||||
this.SetIsShownInSwitchers(false);
|
|
||||||
this.SetIsResizable(false);
|
this.SetIsResizable(false);
|
||||||
this.SetIsMinimizable(false);
|
this.SetIsMinimizable(false);
|
||||||
this.SetIsMaximizable(false);
|
this.SetIsMaximizable(false);
|
||||||
IsTitleBarVisible = false;
|
IsTitleBarVisible = false;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
this.SetIsShownInSwitchers(false);
|
||||||
|
}
|
||||||
|
catch (NotImplementedException)
|
||||||
|
{
|
||||||
|
// WinUI will throw if explorer is not running, safely ignore
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
// Remove the caption style from the window style. Windows App SDK 1.6 added it, which made the title bar and borders appear for Measure Tool. This code removes it.
|
// Remove the caption style from the window style. Windows App SDK 1.6 added it, which made the title bar and borders appear for Measure Tool. This code removes it.
|
||||||
var windowStyle = GetWindowLong(hwnd, GWL_STYLE);
|
var windowStyle = GetWindowLong(hwnd, GWL_STYLE);
|
||||||
windowStyle = windowStyle & (~WS_CAPTION);
|
windowStyle = windowStyle & (~WS_CAPTION);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
282
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
282
src/modules/MouseUtils/CursorWrap/CursorWrapCore.cpp
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
// 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, bool disableOnSingleMonitor)
|
||||||
|
{
|
||||||
|
// Check if wrapping should be disabled on single monitor
|
||||||
|
if (disableOnSingleMonitor && m_monitors.size() <= 1)
|
||||||
|
{
|
||||||
|
#ifdef _DEBUG
|
||||||
|
static bool loggedOnce = false;
|
||||||
|
if (!loggedOnce)
|
||||||
|
{
|
||||||
|
OutputDebugStringW(L"[CursorWrap] Single monitor detected - cursor wrapping disabled\n");
|
||||||
|
loggedOnce = true;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
return currentPos;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
35
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
35
src/modules/MouseUtils/CursorWrap/CursorWrapCore.h
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// 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
|
||||||
|
// disableOnSingleMonitor: if true, cursor wrapping is disabled when only one monitor is connected
|
||||||
|
POINT HandleMouseMove(const POINT& currentPos, bool disableWrapDuringDrag, int wrapMode, bool disableOnSingleMonitor);
|
||||||
|
|
||||||
|
const std::vector<MonitorInfo>& GetMonitors() const { return m_monitors; }
|
||||||
|
size_t GetMonitorCount() const { return m_monitors.size(); }
|
||||||
|
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
@@ -2,8 +2,11 @@
|
|||||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||||
// See the LICENSE file in the project root for more information.
|
// See the LICENSE file in the project root for more information.
|
||||||
|
|
||||||
|
using System;
|
||||||
using System.Windows;
|
using System.Windows;
|
||||||
|
|
||||||
|
using WorkspacesEditor.Utils;
|
||||||
|
|
||||||
namespace WorkspacesEditor
|
namespace WorkspacesEditor
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -11,9 +14,40 @@ namespace WorkspacesEditor
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public partial class OverlayWindow : Window
|
public partial class OverlayWindow : Window
|
||||||
{
|
{
|
||||||
|
private int _targetX;
|
||||||
|
private int _targetY;
|
||||||
|
private int _targetWidth;
|
||||||
|
private int _targetHeight;
|
||||||
|
|
||||||
public OverlayWindow()
|
public OverlayWindow()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
|
SourceInitialized += OnWindowSourceInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sets the target bounds for the overlay window.
|
||||||
|
/// The window will be positioned using DPI-unaware context after initialization.
|
||||||
|
/// </summary>
|
||||||
|
public void SetTargetBounds(int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
_targetX = x;
|
||||||
|
_targetY = y;
|
||||||
|
_targetWidth = width;
|
||||||
|
_targetHeight = height;
|
||||||
|
|
||||||
|
// Set initial WPF properties (will be corrected after HWND creation)
|
||||||
|
Left = x;
|
||||||
|
Top = y;
|
||||||
|
Width = width;
|
||||||
|
Height = height;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWindowSourceInitialized(object sender, EventArgs e)
|
||||||
|
{
|
||||||
|
// Reposition window using DPI-unaware context to match the virtual coordinates.
|
||||||
|
// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||||
|
NativeMethods.SetWindowPositionDpiUnaware(this, _targetX, _targetY, _targetWidth, _targetHeight);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
using System;
|
using System;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Windows;
|
||||||
|
using System.Windows.Interop;
|
||||||
|
|
||||||
namespace WorkspacesEditor.Utils
|
namespace WorkspacesEditor.Utils
|
||||||
{
|
{
|
||||||
@@ -17,6 +19,39 @@ namespace WorkspacesEditor.Utils
|
|||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
public static extern bool MoveWindow(IntPtr hWnd, int X, int Y, int nWidth, int nHeight, bool bRepaint);
|
||||||
|
|
||||||
|
[DllImport("user32.dll", SetLastError = true)]
|
||||||
|
private static extern bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
|
||||||
|
|
||||||
|
[DllImport("user32.dll")]
|
||||||
|
private static extern IntPtr SetThreadDpiAwarenessContext(IntPtr dpiContext);
|
||||||
|
|
||||||
|
private const uint SWP_NOZORDER = 0x0004;
|
||||||
|
private const uint SWP_NOACTIVATE = 0x0010;
|
||||||
|
|
||||||
|
private static readonly IntPtr DPI_AWARENESS_CONTEXT_UNAWARE = new IntPtr(-1);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Positions a WPF window using DPI-unaware context to match the virtual coordinates.
|
||||||
|
/// This fixes overlay positioning on mixed-DPI multi-monitor setups.
|
||||||
|
/// </summary>
|
||||||
|
public static void SetWindowPositionDpiUnaware(Window window, int x, int y, int width, int height)
|
||||||
|
{
|
||||||
|
var helper = new WindowInteropHelper(window).Handle;
|
||||||
|
if (helper != IntPtr.Zero)
|
||||||
|
{
|
||||||
|
// Temporarily switch to DPI-unaware context to position window.
|
||||||
|
IntPtr oldContext = SetThreadDpiAwarenessContext(DPI_AWARENESS_CONTEXT_UNAWARE);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
SetWindowPos(helper, IntPtr.Zero, x, y, width, height, SWP_NOZORDER | SWP_NOACTIVATE);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SetThreadDpiAwarenessContext(oldContext);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[DllImport("USER32.DLL")]
|
[DllImport("USER32.DLL")]
|
||||||
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
public static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||||
|
|
||||||
|
|||||||
@@ -495,10 +495,10 @@ namespace WorkspacesEditor.ViewModels
|
|||||||
{
|
{
|
||||||
var bounds = screen.Bounds;
|
var bounds = screen.Bounds;
|
||||||
OverlayWindow overlayWindow = new OverlayWindow();
|
OverlayWindow overlayWindow = new OverlayWindow();
|
||||||
overlayWindow.Top = bounds.Top;
|
|
||||||
overlayWindow.Left = bounds.Left;
|
// Use DPI-unaware positioning to fix overlay on mixed-DPI multi-monitor setups
|
||||||
overlayWindow.Width = bounds.Width;
|
overlayWindow.SetTargetBounds(bounds.Left, bounds.Top, bounds.Width, bounds.Height);
|
||||||
overlayWindow.Height = bounds.Height;
|
|
||||||
overlayWindow.ShowActivated = true;
|
overlayWindow.ShowActivated = true;
|
||||||
overlayWindow.Topmost = true;
|
overlayWindow.Topmost = true;
|
||||||
overlayWindow.Show();
|
overlayWindow.Show();
|
||||||
|
|||||||
@@ -17,12 +17,25 @@ public sealed partial class AppListItem : ListItem
|
|||||||
{
|
{
|
||||||
private readonly AppCommand _appCommand;
|
private readonly AppCommand _appCommand;
|
||||||
private readonly AppItem _app;
|
private readonly AppItem _app;
|
||||||
private readonly Lazy<Details> _details;
|
|
||||||
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
|
private readonly Lazy<Task<IconInfo?>> _iconLoadTask;
|
||||||
|
private readonly Lazy<Task<Details>> _detailsLoadTask;
|
||||||
|
|
||||||
private InterlockedBoolean _isLoadingIcon;
|
private InterlockedBoolean _isLoadingIcon;
|
||||||
|
private InterlockedBoolean _isLoadingDetails;
|
||||||
|
|
||||||
public override IDetails? Details { get => _details.Value; set => base.Details = value; }
|
public override IDetails? Details
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
if (_isLoadingDetails.Set())
|
||||||
|
{
|
||||||
|
_ = LoadDetailsAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
return base.Details;
|
||||||
|
}
|
||||||
|
set => base.Details = value;
|
||||||
|
}
|
||||||
|
|
||||||
public override IIconInfo? Icon
|
public override IIconInfo? Icon
|
||||||
{
|
{
|
||||||
@@ -52,16 +65,22 @@ public sealed partial class AppListItem : ListItem
|
|||||||
|
|
||||||
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
MoreCommands = AddPinCommands(_app.Commands!, isPinned);
|
||||||
|
|
||||||
_details = new Lazy<Details>(() =>
|
_detailsLoadTask = new Lazy<Task<Details>>(BuildDetails);
|
||||||
{
|
|
||||||
var t = BuildDetails();
|
|
||||||
t.Wait();
|
|
||||||
return t.Result;
|
|
||||||
});
|
|
||||||
|
|
||||||
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
|
_iconLoadTask = new Lazy<Task<IconInfo?>>(async () => await FetchIcon(useThumbnails));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task LoadDetailsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Details = await _detailsLoadTask.Value;
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogWarning($"Failed to load details for {AppIdentifier}\n{ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private async Task LoadIconAsync()
|
private async Task LoadIconAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -98,18 +117,16 @@ public sealed partial class AppListItem : ListItem
|
|||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
try
|
// do nothing if we fail to load an icon.
|
||||||
|
// Logging it would be too NOISY, there's really no need.
|
||||||
|
if (!string.IsNullOrEmpty(_app.IcoPath))
|
||||||
{
|
{
|
||||||
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath, true);
|
heroImage = await TryLoadThumbnail(_app.IcoPath, jumbo: true, logOnFailure: false);
|
||||||
if (stream is not null)
|
|
||||||
{
|
|
||||||
heroImage = IconInfo.FromStream(stream);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
catch (Exception)
|
|
||||||
|
if (heroImage == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||||
{
|
{
|
||||||
// do nothing if we fail to load an icon.
|
heroImage = await TryLoadThumbnail(_app.ExePath, jumbo: true, logOnFailure: false);
|
||||||
// Logging it would be too NOISY, there's really no need.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -132,26 +149,19 @@ public sealed partial class AppListItem : ListItem
|
|||||||
|
|
||||||
if (useThumbnails)
|
if (useThumbnails)
|
||||||
{
|
{
|
||||||
try
|
if (!string.IsNullOrEmpty(_app.IcoPath))
|
||||||
{
|
{
|
||||||
var stream = await ThumbnailHelper.GetThumbnail(_app.ExePath);
|
icon = await TryLoadThumbnail(_app.IcoPath, jumbo: false, logOnFailure: true);
|
||||||
if (stream is not null)
|
|
||||||
{
|
|
||||||
icon = IconInfo.FromStream(stream);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogDebug($"Failed to load icon for {AppIdentifier}:\n{ex}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
icon = icon ?? new IconInfo(_app.IcoPath);
|
if (icon == null && !string.IsNullOrEmpty(_app.ExePath))
|
||||||
}
|
{
|
||||||
else
|
icon = await TryLoadThumbnail(_app.ExePath, jumbo: false, logOnFailure: true);
|
||||||
{
|
}
|
||||||
icon = new IconInfo(_app.IcoPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
icon ??= new IconInfo(_app.IcoPath);
|
||||||
|
|
||||||
return icon;
|
return icon;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -183,4 +193,25 @@ public sealed partial class AppListItem : ListItem
|
|||||||
|
|
||||||
return newCommands.ToArray();
|
return newCommands.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task<IconInfo?> TryLoadThumbnail(string path, bool jumbo, bool logOnFailure)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var stream = await ThumbnailHelper.GetThumbnail(path, jumbo);
|
||||||
|
if (stream is not null)
|
||||||
|
{
|
||||||
|
return IconInfo.FromStream(stream);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
if (logOnFailure)
|
||||||
|
{
|
||||||
|
Logger.LogDebug($"Failed to load icon {path} for {AppIdentifier}:\n{ex}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1052,9 +1052,6 @@ public class Win32Program : IProgram
|
|||||||
app.FullPath) :
|
app.FullPath) :
|
||||||
app.IcoPath;
|
app.IcoPath;
|
||||||
|
|
||||||
icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ?
|
|
||||||
app.FullPath :
|
|
||||||
icoPath;
|
|
||||||
return new AppItem()
|
return new AppItem()
|
||||||
{
|
{
|
||||||
Name = app.Name,
|
Name = app.Name,
|
||||||
|
|||||||
@@ -69,14 +69,14 @@ internal sealed partial class SampleMarkdownImagesPage : ContentPage
|
|||||||
|
|
||||||
### Web URL
|
### Web URL
|
||||||
```xml
|
```xml
|
||||||

|

|
||||||
```
|
```
|
||||||

|

|
||||||
|
|
||||||
```xml
|
```xml
|
||||||

|

|
||||||
```
|
```
|
||||||

|

|
||||||
|
|
||||||
### File URL (PNG):
|
### File URL (PNG):
|
||||||
```xml
|
```xml
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ Result:
|
|||||||
|
|
||||||
Result:
|
Result:
|
||||||
|
|
||||||

|

|
||||||
|
|
||||||
### Links
|
### Links
|
||||||
|
|
||||||
|
|||||||
@@ -140,7 +140,7 @@
|
|||||||
<TextBlock
|
<TextBlock
|
||||||
x:Name="FormatNameTextBlock"
|
x:Name="FormatNameTextBlock"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Foreground="{DynamicResource TextFillColorSecondaryBrush}"
|
Foreground="{DynamicResource TextFillColorPrimaryBrush}"
|
||||||
Style="{StaticResource CaptionTextBlockStyle}"
|
Style="{StaticResource CaptionTextBlockStyle}"
|
||||||
TextTrimming="CharacterEllipsis" />
|
TextTrimming="CharacterEllipsis" />
|
||||||
|
|
||||||
|
|||||||
@@ -67,12 +67,27 @@ namespace ImageResizer
|
|||||||
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
|
// Fix for .net 3.1.19 making Image Resizer not adapt to DPI changes.
|
||||||
NativeMethods.SetProcessDPIAware();
|
NativeMethods.SetProcessDPIAware();
|
||||||
|
|
||||||
|
// TODO: Re-enable AI Super Resolution in next release by removing this #if block
|
||||||
|
// Temporarily disable AI Super Resolution feature (hide from UI but keep code)
|
||||||
|
#if true // Set to false to re-enable AI Super Resolution
|
||||||
|
AiAvailabilityState = AiAvailabilityState.NotSupported;
|
||||||
|
ResizeBatch.SetAiSuperResolutionService(Services.NoOpAiSuperResolutionService.Instance);
|
||||||
|
|
||||||
|
// Skip AI detection mode as well
|
||||||
|
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||||
|
{
|
||||||
|
Services.AiAvailabilityCacheService.SaveCache(AiAvailabilityState.NotSupported);
|
||||||
|
Environment.Exit(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#else
|
||||||
// Check for AI detection mode (called by Runner in background)
|
// Check for AI detection mode (called by Runner in background)
|
||||||
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
if (e?.Args?.Length > 0 && e.Args[0] == "--detect-ai")
|
||||||
{
|
{
|
||||||
RunAiDetectionMode();
|
RunAiDetectionMode();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
if (PowerToys.GPOWrapperProjection.GPOWrapper.GetConfiguredImageResizerEnabledValue() == PowerToys.GPOWrapperProjection.GpoRuleConfigured.Disabled)
|
||||||
{
|
{
|
||||||
@@ -173,31 +188,8 @@ namespace ImageResizer
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
private static AiAvailabilityState CheckAiAvailability()
|
private static AiAvailabilityState CheckAiAvailability()
|
||||||
{
|
{
|
||||||
try
|
// AI feature disabled - always return NotSupported
|
||||||
{
|
return AiAvailabilityState.NotSupported;
|
||||||
// Check Windows AI service model ready state
|
|
||||||
// it's so slow, why?
|
|
||||||
var readyState = Services.WinAiSuperResolutionService.GetModelReadyState();
|
|
||||||
|
|
||||||
// Map AI service state to our availability state
|
|
||||||
switch (readyState)
|
|
||||||
{
|
|
||||||
case Microsoft.Windows.AI.AIFeatureReadyState.Ready:
|
|
||||||
return AiAvailabilityState.Ready;
|
|
||||||
|
|
||||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotReady:
|
|
||||||
return AiAvailabilityState.ModelNotReady;
|
|
||||||
|
|
||||||
case Microsoft.Windows.AI.AIFeatureReadyState.DisabledByUser:
|
|
||||||
case Microsoft.Windows.AI.AIFeatureReadyState.NotSupportedOnCurrentSystem:
|
|
||||||
default:
|
|
||||||
return AiAvailabilityState.NotSupported;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
return AiAvailabilityState.NotSupported;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -27,13 +27,13 @@
|
|||||||
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
<ApplicationIcon>Resources\ImageResizer.ico</ApplicationIcon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<!-- <PropertyGroup>
|
||||||
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
<ApplicationManifest>ImageResizerUI.dev.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
<PropertyGroup Condition="'$(CIBuild)'=='true'">
|
||||||
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
<ApplicationManifest>ImageResizerUI.prod.manifest</ApplicationManifest>
|
||||||
</PropertyGroup>
|
</PropertyGroup> -->
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Update="Properties\Resources.resx">
|
<EmbeddedResource Update="Properties\Resources.resx">
|
||||||
|
|||||||
@@ -39,29 +39,8 @@ namespace ImageResizer.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static AiAvailabilityState? LoadCache()
|
public static AiAvailabilityState? LoadCache()
|
||||||
{
|
{
|
||||||
try
|
// Cache disabled - always return null to use default value
|
||||||
{
|
return null;
|
||||||
if (!File.Exists(CachePath))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = File.ReadAllText(CachePath);
|
|
||||||
var cache = JsonSerializer.Deserialize<AiCapabilityCache>(json);
|
|
||||||
|
|
||||||
if (!IsCacheValid(cache))
|
|
||||||
{
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (AiAvailabilityState)cache.State;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
// Read failure (file locked, corrupted JSON, etc.) - return null and use fallback
|
|
||||||
Logger.LogError($"Failed to load AI cache: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -70,32 +49,8 @@ namespace ImageResizer.Services
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public static void SaveCache(AiAvailabilityState state)
|
public static void SaveCache(AiAvailabilityState state)
|
||||||
{
|
{
|
||||||
try
|
// Cache disabled - do not save anything
|
||||||
{
|
return;
|
||||||
var cache = new AiCapabilityCache
|
|
||||||
{
|
|
||||||
Version = CacheVersion,
|
|
||||||
State = (int)state,
|
|
||||||
WindowsBuild = Environment.OSVersion.Version.ToString(),
|
|
||||||
Architecture = RuntimeInformation.ProcessArchitecture.ToString(),
|
|
||||||
Timestamp = DateTime.UtcNow.ToString("o"),
|
|
||||||
};
|
|
||||||
|
|
||||||
var dir = Path.GetDirectoryName(CachePath);
|
|
||||||
if (!Directory.Exists(dir))
|
|
||||||
{
|
|
||||||
Directory.CreateDirectory(dir);
|
|
||||||
}
|
|
||||||
|
|
||||||
var json = JsonSerializer.Serialize(cache, SerializerOptions);
|
|
||||||
File.WriteAllText(CachePath, json);
|
|
||||||
|
|
||||||
Logger.LogInfo($"AI cache saved: {state}");
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logger.LogError($"Failed to save AI cache: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
|
|||||||
@@ -197,16 +197,10 @@ namespace Peek.UI
|
|||||||
|
|
||||||
ViewModel.Initialize(selectedItem);
|
ViewModel.Initialize(selectedItem);
|
||||||
|
|
||||||
// If no files were found (e.g., in virtual folders like Home/Recent), show an error
|
// If no files were found (e.g., user is typing in rename/search box, or in virtual folders),
|
||||||
|
// don't show anything - just return silently to avoid stealing focus
|
||||||
if (ViewModel.CurrentItem == null)
|
if (ViewModel.CurrentItem == null)
|
||||||
{
|
{
|
||||||
Logger.LogInfo("Peek: No files found to preview, showing error.");
|
|
||||||
var errorMessage = ResourceLoaderInstance.ResourceLoader.GetString("NoFilesSelected");
|
|
||||||
ViewModel.ShowError(errorMessage);
|
|
||||||
|
|
||||||
// Still show the window so user can see the warning
|
|
||||||
this.Show();
|
|
||||||
WindowHelpers.BringToForeground(this.GetWindowHandle());
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -33,23 +33,27 @@ static std::wstring SanitizeAndNormalize(const std::wstring& input)
|
|||||||
|
|
||||||
// Normalize to NFC (Precomposed).
|
// Normalize to NFC (Precomposed).
|
||||||
// Get the size needed for the normalized string, including null terminator.
|
// Get the size needed for the normalized string, including null terminator.
|
||||||
int size = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
|
int sizeEstimate = NormalizeString(NormalizationC, sanitized.c_str(), -1, nullptr, 0);
|
||||||
if (size <= 0)
|
if (sizeEstimate <= 0)
|
||||||
{
|
{
|
||||||
return sanitized; // Return unaltered if normalization fails.
|
return sanitized; // Return unaltered if normalization fails.
|
||||||
}
|
}
|
||||||
|
|
||||||
// Perform the normalization.
|
// Perform the normalization.
|
||||||
std::wstring normalized;
|
std::wstring normalized;
|
||||||
normalized.resize(size);
|
normalized.resize(sizeEstimate);
|
||||||
NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], size);
|
int actualSize = NormalizeString(NormalizationC, sanitized.c_str(), -1, &normalized[0], sizeEstimate);
|
||||||
|
|
||||||
// Remove the explicit null terminator added by NormalizeString.
|
if (actualSize <= 0)
|
||||||
if (!normalized.empty() && normalized.back() == L'\0')
|
|
||||||
{
|
{
|
||||||
normalized.pop_back();
|
// Normalization failed, return sanitized string.
|
||||||
|
return sanitized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Resize to actual size minus the null terminator.
|
||||||
|
// actualSize includes the null terminator when input length is -1.
|
||||||
|
normalized.resize(static_cast<size_t>(actualSize) - 1);
|
||||||
|
|
||||||
return normalized;
|
return normalized;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -695,6 +695,38 @@ TEST_METHOD(VerifyUnicodeAndWhitespaceNormalizationRegex)
|
|||||||
VerifyNormalizationHelper(UseRegularExpressions);
|
VerifyNormalizationHelper(UseRegularExpressions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(VerifyRegexMetacharacterDollarSign)
|
||||||
|
{
|
||||||
|
CComPtr<IPowerRenameRegEx> renameRegEx;
|
||||||
|
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
|
||||||
|
DWORD flags = UseRegularExpressions;
|
||||||
|
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
|
||||||
|
|
||||||
|
PWSTR result = nullptr;
|
||||||
|
Assert::IsTrue(renameRegEx->PutSearchTerm(L"$") == S_OK);
|
||||||
|
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"_end") == S_OK);
|
||||||
|
unsigned long index = {};
|
||||||
|
Assert::IsTrue(renameRegEx->Replace(L"test.txt", &result, index) == S_OK);
|
||||||
|
Assert::AreEqual(L"test.txt_end", result);
|
||||||
|
CoTaskMemFree(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST_METHOD(VerifyRegexMetacharacterCaret)
|
||||||
|
{
|
||||||
|
CComPtr<IPowerRenameRegEx> renameRegEx;
|
||||||
|
Assert::IsTrue(CPowerRenameRegEx::s_CreateInstance(&renameRegEx) == S_OK);
|
||||||
|
DWORD flags = UseRegularExpressions;
|
||||||
|
Assert::IsTrue(renameRegEx->PutFlags(flags) == S_OK);
|
||||||
|
|
||||||
|
PWSTR result = nullptr;
|
||||||
|
Assert::IsTrue(renameRegEx->PutSearchTerm(L"^") == S_OK);
|
||||||
|
Assert::IsTrue(renameRegEx->PutReplaceTerm(L"start_") == S_OK);
|
||||||
|
unsigned long index = {};
|
||||||
|
Assert::IsTrue(renameRegEx->Replace(L"test.txt", &result, index) == S_OK);
|
||||||
|
Assert::AreEqual(L"start_test.txt", result);
|
||||||
|
CoTaskMemFree(result);
|
||||||
|
}
|
||||||
|
|
||||||
#ifndef TESTS_PARTIAL
|
#ifndef TESTS_PARTIAL
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -173,6 +173,10 @@
|
|||||||
<value>Settings\tDouble-click</value>
|
<value>Settings\tDouble-click</value>
|
||||||
<comment>Don't localize "\t" as that is what separates the click portion to be right aligned in the menu.</comment>
|
<comment>Don't localize "\t" as that is what separates the click portion to be right aligned in the menu.</comment>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="SETTINGS_MENU_TEXT_LEFTCLICK" xml:space="preserve">
|
||||||
|
<value>Settings\tLeft-click</value>
|
||||||
|
<comment>Don't localize "\t" as that is what separates the click portion to be right aligned in the menu. This is shown when Quick Access is disabled.</comment>
|
||||||
|
</data>
|
||||||
<data name="DOCUMENTATION_MENU_TEXT" xml:space="preserve">
|
<data name="DOCUMENTATION_MENU_TEXT" xml:space="preserve">
|
||||||
<value>Documentation</value>
|
<value>Documentation</value>
|
||||||
</data>
|
</data>
|
||||||
|
|||||||
@@ -81,3 +81,36 @@ void Trace::UpdateDownloadCompleted(bool success, const std::wstring& version)
|
|||||||
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||||
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void Trace::TrayIconLeftClick(bool quickAccessEnabled)
|
||||||
|
{
|
||||||
|
TraceLoggingWriteWrapper(
|
||||||
|
g_hProvider,
|
||||||
|
"TrayIcon_LeftClick",
|
||||||
|
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
|
||||||
|
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||||
|
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||||
|
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Trace::TrayIconDoubleClick(bool quickAccessEnabled)
|
||||||
|
{
|
||||||
|
TraceLoggingWriteWrapper(
|
||||||
|
g_hProvider,
|
||||||
|
"TrayIcon_DoubleClick",
|
||||||
|
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
|
||||||
|
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||||
|
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||||
|
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||||
|
}
|
||||||
|
|
||||||
|
void Trace::TrayIconRightClick(bool quickAccessEnabled)
|
||||||
|
{
|
||||||
|
TraceLoggingWriteWrapper(
|
||||||
|
g_hProvider,
|
||||||
|
"TrayIcon_RightClick",
|
||||||
|
TraceLoggingBoolean(quickAccessEnabled, "QuickAccessEnabled"),
|
||||||
|
ProjectTelemetryPrivacyDataTag(ProjectTelemetryTag_ProductAndServicePerformance),
|
||||||
|
TraceLoggingBoolean(TRUE, "UTCReplace_AppSessionGuid"),
|
||||||
|
TraceLoggingKeyword(PROJECT_KEYWORD_MEASURE));
|
||||||
|
}
|
||||||
|
|||||||
@@ -13,4 +13,9 @@ public:
|
|||||||
// Auto-update telemetry
|
// Auto-update telemetry
|
||||||
static void UpdateCheckCompleted(bool success, bool updateAvailable, const std::wstring& fromVersion, const std::wstring& toVersion);
|
static void UpdateCheckCompleted(bool success, bool updateAvailable, const std::wstring& fromVersion, const std::wstring& toVersion);
|
||||||
static void UpdateDownloadCompleted(bool success, const std::wstring& version);
|
static void UpdateDownloadCompleted(bool success, const std::wstring& version);
|
||||||
|
|
||||||
|
// Tray icon interaction telemetry
|
||||||
|
static void TrayIconLeftClick(bool quickAccessEnabled);
|
||||||
|
static void TrayIconDoubleClick(bool quickAccessEnabled);
|
||||||
|
static void TrayIconRightClick(bool quickAccessEnabled);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -7,6 +7,7 @@
|
|||||||
#include "centralized_kb_hook.h"
|
#include "centralized_kb_hook.h"
|
||||||
#include "quick_access_host.h"
|
#include "quick_access_host.h"
|
||||||
#include "hotkey_conflict_detector.h"
|
#include "hotkey_conflict_detector.h"
|
||||||
|
#include "trace.h"
|
||||||
#include <Windows.h>
|
#include <Windows.h>
|
||||||
|
|
||||||
#include <common/utils/resources.h>
|
#include <common/utils/resources.h>
|
||||||
@@ -14,6 +15,7 @@
|
|||||||
#include <common/logger/logger.h>
|
#include <common/logger/logger.h>
|
||||||
#include <common/utils/elevation.h>
|
#include <common/utils/elevation.h>
|
||||||
#include <common/Themes/theme_listener.h>
|
#include <common/Themes/theme_listener.h>
|
||||||
|
#include <common/Themes/theme_helpers.h>
|
||||||
#include "bug_report.h"
|
#include "bug_report.h"
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
@@ -39,6 +41,7 @@ namespace
|
|||||||
bool double_click_timer_running = false;
|
bool double_click_timer_running = false;
|
||||||
bool double_clicked = false;
|
bool double_clicked = false;
|
||||||
POINT tray_icon_click_point;
|
POINT tray_icon_click_point;
|
||||||
|
std::optional<bool> last_quick_access_state; // Track the last known Quick Access state
|
||||||
|
|
||||||
static ThemeListener theme_listener;
|
static ThemeListener theme_listener;
|
||||||
static bool theme_adaptive_enabled = false;
|
static bool theme_adaptive_enabled = false;
|
||||||
@@ -129,6 +132,9 @@ void click_timer_elapsed()
|
|||||||
double_click_timer_running = false;
|
double_click_timer_running = false;
|
||||||
if (!double_clicked)
|
if (!double_clicked)
|
||||||
{
|
{
|
||||||
|
// Log telemetry for single click (confirmed it's not a double click)
|
||||||
|
Trace::TrayIconLeftClick(get_general_settings().enableQuickAccess);
|
||||||
|
|
||||||
if (get_general_settings().enableQuickAccess)
|
if (get_general_settings().enableQuickAccess)
|
||||||
{
|
{
|
||||||
open_quick_access_flyout_window();
|
open_quick_access_flyout_window();
|
||||||
@@ -194,6 +200,21 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
|
|||||||
case WM_RBUTTONUP:
|
case WM_RBUTTONUP:
|
||||||
case WM_CONTEXTMENU:
|
case WM_CONTEXTMENU:
|
||||||
{
|
{
|
||||||
|
bool quick_access_enabled = get_general_settings().enableQuickAccess;
|
||||||
|
|
||||||
|
// Log telemetry
|
||||||
|
Trace::TrayIconRightClick(quick_access_enabled);
|
||||||
|
|
||||||
|
// Reload menu if Quick Access state has changed or is first time
|
||||||
|
if (h_menu && (!last_quick_access_state.has_value() || quick_access_enabled != last_quick_access_state.value()))
|
||||||
|
{
|
||||||
|
DestroyMenu(h_menu);
|
||||||
|
h_menu = nullptr;
|
||||||
|
h_sub_menu = nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
last_quick_access_state = quick_access_enabled;
|
||||||
|
|
||||||
if (!h_menu)
|
if (!h_menu)
|
||||||
{
|
{
|
||||||
h_menu = LoadMenu(reinterpret_cast<HINSTANCE>(&__ImageBase), MAKEINTRESOURCE(ID_TRAY_MENU));
|
h_menu = LoadMenu(reinterpret_cast<HINSTANCE>(&__ImageBase), MAKEINTRESOURCE(ID_TRAY_MENU));
|
||||||
@@ -201,17 +222,39 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
|
|||||||
if (h_menu)
|
if (h_menu)
|
||||||
{
|
{
|
||||||
static std::wstring settings_menuitem_label = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT);
|
static std::wstring settings_menuitem_label = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT);
|
||||||
|
static std::wstring settings_menuitem_label_leftclick = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT_LEFTCLICK);
|
||||||
static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_MENU_TEXT);
|
static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_MENU_TEXT);
|
||||||
static std::wstring submit_bug_menuitem_label = GET_RESOURCE_STRING(IDS_SUBMIT_BUG_TEXT);
|
static std::wstring submit_bug_menuitem_label = GET_RESOURCE_STRING(IDS_SUBMIT_BUG_TEXT);
|
||||||
static std::wstring documentation_menuitem_label = GET_RESOURCE_STRING(IDS_DOCUMENTATION_MENU_TEXT);
|
static std::wstring documentation_menuitem_label = GET_RESOURCE_STRING(IDS_DOCUMENTATION_MENU_TEXT);
|
||||||
static std::wstring quick_access_menuitem_label = GET_RESOURCE_STRING(IDS_QUICK_ACCESS_MENU_TEXT);
|
static std::wstring quick_access_menuitem_label = GET_RESOURCE_STRING(IDS_QUICK_ACCESS_MENU_TEXT);
|
||||||
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
|
|
||||||
|
// Update Settings menu text based on Quick Access state
|
||||||
|
if (quick_access_enabled)
|
||||||
|
{
|
||||||
|
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label_leftclick.data());
|
||||||
|
}
|
||||||
|
|
||||||
change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_menuitem_label.data());
|
change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_menuitem_label.data());
|
||||||
change_menu_item_text(ID_REPORT_BUG_COMMAND, submit_bug_menuitem_label.data());
|
change_menu_item_text(ID_REPORT_BUG_COMMAND, submit_bug_menuitem_label.data());
|
||||||
bool bug_report_disabled = is_bug_report_running();
|
bool bug_report_disabled = is_bug_report_running();
|
||||||
EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (bug_report_disabled ? MF_GRAYED : MF_ENABLED));
|
EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (bug_report_disabled ? MF_GRAYED : MF_ENABLED));
|
||||||
change_menu_item_text(ID_DOCUMENTATION_MENU_COMMAND, documentation_menuitem_label.data());
|
change_menu_item_text(ID_DOCUMENTATION_MENU_COMMAND, documentation_menuitem_label.data());
|
||||||
change_menu_item_text(ID_QUICK_ACCESS_MENU_COMMAND, quick_access_menuitem_label.data());
|
change_menu_item_text(ID_QUICK_ACCESS_MENU_COMMAND, quick_access_menuitem_label.data());
|
||||||
|
|
||||||
|
// Hide or show Quick Access menu item based on setting
|
||||||
|
if (!h_sub_menu)
|
||||||
|
{
|
||||||
|
h_sub_menu = GetSubMenu(h_menu, 0);
|
||||||
|
}
|
||||||
|
if (!quick_access_enabled)
|
||||||
|
{
|
||||||
|
// Remove Quick Access menu item when disabled
|
||||||
|
DeleteMenu(h_sub_menu, ID_QUICK_ACCESS_MENU_COMMAND, MF_BYCOMMAND);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!h_sub_menu)
|
if (!h_sub_menu)
|
||||||
{
|
{
|
||||||
@@ -242,6 +285,9 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
|
|||||||
}
|
}
|
||||||
case WM_LBUTTONDBLCLK:
|
case WM_LBUTTONDBLCLK:
|
||||||
{
|
{
|
||||||
|
// Log telemetry
|
||||||
|
Trace::TrayIconDoubleClick(get_general_settings().enableQuickAccess);
|
||||||
|
|
||||||
double_clicked = true;
|
double_clicked = true;
|
||||||
open_settings_window(std::nullopt);
|
open_settings_window(std::nullopt);
|
||||||
break;
|
break;
|
||||||
@@ -293,7 +339,7 @@ static void handle_theme_change()
|
|||||||
{
|
{
|
||||||
if (theme_adaptive_enabled)
|
if (theme_adaptive_enabled)
|
||||||
{
|
{
|
||||||
tray_icon_data.hIcon = get_icon(theme_listener.AppTheme);
|
tray_icon_data.hIcon = get_icon(ThemeHelpers::GetSystemTheme());
|
||||||
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
Shell_NotifyIcon(NIM_MODIFY, &tray_icon_data);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -310,7 +356,7 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive)
|
|||||||
{
|
{
|
||||||
theme_adaptive_enabled = theme_adaptive;
|
theme_adaptive_enabled = theme_adaptive;
|
||||||
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
auto h_instance = reinterpret_cast<HINSTANCE>(&__ImageBase);
|
||||||
HICON const icon = theme_adaptive ? get_icon(theme_listener.AppTheme) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
HICON const icon = theme_adaptive ? get_icon(ThemeHelpers::GetSystemTheme()) : LoadIcon(h_instance, MAKEINTRESOURCE(APPICON));
|
||||||
if (icon)
|
if (icon)
|
||||||
{
|
{
|
||||||
UINT id_tray_icon = 1;
|
UINT id_tray_icon = 1;
|
||||||
@@ -357,7 +403,7 @@ void start_tray_icon(bool isProcessElevated, bool theme_adaptive)
|
|||||||
ChangeWindowMessageFilterEx(hwnd, WM_COMMAND, MSGFLT_ALLOW, nullptr);
|
ChangeWindowMessageFilterEx(hwnd, WM_COMMAND, MSGFLT_ALLOW, nullptr);
|
||||||
|
|
||||||
tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE;
|
tray_icon_created = Shell_NotifyIcon(NIM_ADD, &tray_icon_data) == TRUE;
|
||||||
theme_listener.AddChangedHandler(&handle_theme_change);
|
theme_listener.AddSystemThemeChangedHandler(&handle_theme_change);
|
||||||
|
|
||||||
// Register callback to update bug report menu item status
|
// Register callback to update bug report menu item status
|
||||||
BugReportManager::instance().register_callback([](bool isRunning) {
|
BugReportManager::instance().register_callback([](bool isRunning) {
|
||||||
@@ -389,7 +435,7 @@ void set_tray_icon_theme_adaptive(bool theme_adaptive)
|
|||||||
|
|
||||||
if (theme_adaptive)
|
if (theme_adaptive)
|
||||||
{
|
{
|
||||||
icon = get_icon(theme_listener.AppTheme);
|
icon = get_icon(ThemeHelpers::GetSystemTheme());
|
||||||
if (!icon)
|
if (!icon)
|
||||||
{
|
{
|
||||||
Logger::warn(L"set_tray_icon_theme_adaptive: Failed to load theme adaptive icon, falling back to default");
|
Logger::warn(L"set_tray_icon_theme_adaptive: Failed to load theme adaptive icon, falling back to default");
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
IsMaximizable="False"
|
IsMaximizable="False"
|
||||||
IsMinimizable="False"
|
IsMinimizable="False"
|
||||||
IsResizable="False"
|
IsResizable="False"
|
||||||
IsShownInSwitchers="False"
|
|
||||||
IsTitleBarVisible="False"
|
IsTitleBarVisible="False"
|
||||||
mc:Ignorable="d">
|
mc:Ignorable="d">
|
||||||
<winuiEx:WindowEx.Backdrop>
|
<winuiEx:WindowEx.Backdrop>
|
||||||
|
|||||||
@@ -305,7 +305,17 @@ public sealed partial class MainWindow : WindowEx, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_appWindow.IsShownInSwitchers = false;
|
try
|
||||||
|
{
|
||||||
|
_appWindow.IsShownInSwitchers = false;
|
||||||
|
}
|
||||||
|
catch (NotImplementedException)
|
||||||
|
{
|
||||||
|
// WinUI Will throw if explorer is not running, safely ignore
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CloakWindow()
|
private bool CloakWindow()
|
||||||
|
|||||||
@@ -10,6 +10,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library;
|
|||||||
|
|
||||||
public sealed class AdvancedPasteAdditionalActions
|
public sealed class AdvancedPasteAdditionalActions
|
||||||
{
|
{
|
||||||
|
private AdvancedPasteAdditionalAction _imageToText = new();
|
||||||
|
private AdvancedPastePasteAsFileAction _pasteAsFile = new();
|
||||||
|
private AdvancedPasteTranscodeAction _transcode = new();
|
||||||
|
|
||||||
public static class PropertyNames
|
public static class PropertyNames
|
||||||
{
|
{
|
||||||
public const string ImageToText = "image-to-text";
|
public const string ImageToText = "image-to-text";
|
||||||
@@ -18,13 +22,25 @@ public sealed class AdvancedPasteAdditionalActions
|
|||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName(PropertyNames.ImageToText)]
|
[JsonPropertyName(PropertyNames.ImageToText)]
|
||||||
public AdvancedPasteAdditionalAction ImageToText { get; init; } = new();
|
public AdvancedPasteAdditionalAction ImageToText
|
||||||
|
{
|
||||||
|
get => _imageToText;
|
||||||
|
init => _imageToText = value ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
[JsonPropertyName(PropertyNames.PasteAsFile)]
|
[JsonPropertyName(PropertyNames.PasteAsFile)]
|
||||||
public AdvancedPastePasteAsFileAction PasteAsFile { get; init; } = new();
|
public AdvancedPastePasteAsFileAction PasteAsFile
|
||||||
|
{
|
||||||
|
get => _pasteAsFile;
|
||||||
|
init => _pasteAsFile = value ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
[JsonPropertyName(PropertyNames.Transcode)]
|
[JsonPropertyName(PropertyNames.Transcode)]
|
||||||
public AdvancedPasteTranscodeAction Transcode { get; init; } = new();
|
public AdvancedPasteTranscodeAction Transcode
|
||||||
|
{
|
||||||
|
get => _transcode;
|
||||||
|
init => _transcode = value ?? new();
|
||||||
|
}
|
||||||
|
|
||||||
public IEnumerable<IAdvancedPasteAction> GetAllActions()
|
public IEnumerable<IAdvancedPasteAction> GetAllActions()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -34,21 +34,21 @@ public sealed class AdvancedPastePasteAsFileAction : Observable, IAdvancedPasteA
|
|||||||
public AdvancedPasteAdditionalAction PasteAsTxtFile
|
public AdvancedPasteAdditionalAction PasteAsTxtFile
|
||||||
{
|
{
|
||||||
get => _pasteAsTxtFile;
|
get => _pasteAsTxtFile;
|
||||||
init => Set(ref _pasteAsTxtFile, value);
|
init => Set(ref _pasteAsTxtFile, value ?? new());
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName(PropertyNames.PasteAsPngFile)]
|
[JsonPropertyName(PropertyNames.PasteAsPngFile)]
|
||||||
public AdvancedPasteAdditionalAction PasteAsPngFile
|
public AdvancedPasteAdditionalAction PasteAsPngFile
|
||||||
{
|
{
|
||||||
get => _pasteAsPngFile;
|
get => _pasteAsPngFile;
|
||||||
init => Set(ref _pasteAsPngFile, value);
|
init => Set(ref _pasteAsPngFile, value ?? new());
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName(PropertyNames.PasteAsHtmlFile)]
|
[JsonPropertyName(PropertyNames.PasteAsHtmlFile)]
|
||||||
public AdvancedPasteAdditionalAction PasteAsHtmlFile
|
public AdvancedPasteAdditionalAction PasteAsHtmlFile
|
||||||
{
|
{
|
||||||
get => _pasteAsHtmlFile;
|
get => _pasteAsHtmlFile;
|
||||||
init => Set(ref _pasteAsHtmlFile, value);
|
init => Set(ref _pasteAsHtmlFile, value ?? new());
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
|
|||||||
@@ -93,11 +93,11 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
|||||||
|
|
||||||
[JsonPropertyName("custom-actions")]
|
[JsonPropertyName("custom-actions")]
|
||||||
[CmdConfigureIgnoreAttribute]
|
[CmdConfigureIgnoreAttribute]
|
||||||
public AdvancedPasteCustomActions CustomActions { get; init; }
|
public AdvancedPasteCustomActions CustomActions { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("additional-actions")]
|
[JsonPropertyName("additional-actions")]
|
||||||
[CmdConfigureIgnoreAttribute]
|
[CmdConfigureIgnoreAttribute]
|
||||||
public AdvancedPasteAdditionalActions AdditionalActions { get; init; }
|
public AdvancedPasteAdditionalActions AdditionalActions { get; set; }
|
||||||
|
|
||||||
[JsonPropertyName("paste-ai-configuration")]
|
[JsonPropertyName("paste-ai-configuration")]
|
||||||
[CmdConfigureIgnoreAttribute]
|
[CmdConfigureIgnoreAttribute]
|
||||||
|
|||||||
@@ -32,14 +32,14 @@ public sealed class AdvancedPasteTranscodeAction : Observable, IAdvancedPasteAct
|
|||||||
public AdvancedPasteAdditionalAction TranscodeToMp3
|
public AdvancedPasteAdditionalAction TranscodeToMp3
|
||||||
{
|
{
|
||||||
get => _transcodeToMp3;
|
get => _transcodeToMp3;
|
||||||
init => Set(ref _transcodeToMp3, value);
|
init => Set(ref _transcodeToMp3, value ?? new());
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
|
[JsonPropertyName(PropertyNames.TranscodeToMp4)]
|
||||||
public AdvancedPasteAdditionalAction TranscodeToMp4
|
public AdvancedPasteAdditionalAction TranscodeToMp4
|
||||||
{
|
{
|
||||||
get => _transcodeToMp4;
|
get => _transcodeToMp4;
|
||||||
init => Set(ref _transcodeToMp4, value);
|
init => Set(ref _transcodeToMp4, value ?? new());
|
||||||
}
|
}
|
||||||
|
|
||||||
[JsonIgnore]
|
[JsonIgnore]
|
||||||
|
|||||||
@@ -22,11 +22,19 @@ 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; }
|
||||||
|
|
||||||
|
[JsonPropertyName("disable_cursor_wrap_on_single_monitor")]
|
||||||
|
public BoolProperty DisableCursorWrapOnSingleMonitor { 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
|
||||||
|
DisableCursorWrapOnSingleMonitor = new BoolProperty(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,7 +47,23 @@ 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add DisableCursorWrapOnSingleMonitor property if it doesn't exist (for users upgrading from older versions)
|
||||||
|
if (Properties.DisableCursorWrapOnSingleMonitor == null)
|
||||||
|
{
|
||||||
|
Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(false); // Default to false
|
||||||
|
settingsUpgraded = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return settingsUpgraded;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -47,6 +47,16 @@
|
|||||||
<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: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>
|
||||||
</tkcontrols:SettingsExpander.Items>
|
</tkcontrols:SettingsExpander.Items>
|
||||||
</tkcontrols:SettingsExpander>
|
</tkcontrols:SettingsExpander>
|
||||||
</controls:SettingsGroup>
|
</controls:SettingsGroup>
|
||||||
|
|||||||
@@ -2722,12 +2722,27 @@ From there, simply click on one of the supported files in the File Explorer and
|
|||||||
<data name="MouseUtils_CursorWrap_DisableWrapDuringDrag.Content" xml:space="preserve">
|
<data name="MouseUtils_CursorWrap_DisableWrapDuringDrag.Content" xml:space="preserve">
|
||||||
<value>Disable wrapping while dragging</value>
|
<value>Disable wrapping while dragging</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="MouseUtils_CursorWrap_DisableOnSingleMonitor.Content" xml:space="preserve">
|
||||||
|
<value>Disable wrapping when using a single monitor</value>
|
||||||
|
</data>
|
||||||
<data name="MouseUtils_CursorWrap_AutoActivate.Header" xml:space="preserve">
|
<data name="MouseUtils_CursorWrap_AutoActivate.Header" xml:space="preserve">
|
||||||
<value>Auto-activate on startup</value>
|
<value>Auto-activate on startup</value>
|
||||||
</data>
|
</data>
|
||||||
<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>
|
||||||
|
|||||||
@@ -76,16 +76,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
|
|
||||||
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
GeneralSettingsConfig = settingsRepository.SettingsConfig;
|
||||||
|
|
||||||
// To obtain the settings configurations of Fancy zones.
|
// To obtain the settings configurations of Advanced Paste.
|
||||||
ArgumentNullException.ThrowIfNull(settingsRepository);
|
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
||||||
|
|
||||||
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
|
||||||
|
|
||||||
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
_settingsUtils = settingsUtils ?? throw new ArgumentNullException(nameof(settingsUtils));
|
||||||
|
|
||||||
ArgumentNullException.ThrowIfNull(advancedPasteSettingsRepository);
|
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig ?? throw new ArgumentException("SettingsConfig cannot be null", nameof(advancedPasteSettingsRepository));
|
||||||
|
|
||||||
_advancedPasteSettings = advancedPasteSettingsRepository.SettingsConfig;
|
if (_advancedPasteSettings.Properties is null)
|
||||||
|
{
|
||||||
|
throw new ArgumentException("AdvancedPasteSettings.Properties cannot be null", nameof(advancedPasteSettingsRepository));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure AdditionalActions and CustomActions are initialized to prevent null reference exceptions
|
||||||
|
// This handles legacy settings files that may be missing these properties
|
||||||
|
_advancedPasteSettings.Properties.AdditionalActions ??= new AdvancedPasteAdditionalActions();
|
||||||
|
_advancedPasteSettings.Properties.CustomActions ??= new AdvancedPasteCustomActions();
|
||||||
|
|
||||||
AttachConfigurationHandlers();
|
AttachConfigurationHandlers();
|
||||||
|
|
||||||
@@ -93,7 +101,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
SendConfigMSG = ipcMSGCallBackFunc;
|
SendConfigMSG = ipcMSGCallBackFunc;
|
||||||
|
|
||||||
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
_additionalActions = _advancedPasteSettings.Properties.AdditionalActions;
|
||||||
_customActions = _advancedPasteSettings.Properties.CustomActions.Value;
|
_customActions = _advancedPasteSettings.Properties.CustomActions.Value ?? new ObservableCollection<AdvancedPasteCustomAction>();
|
||||||
|
|
||||||
SetupSettingsFileWatcher();
|
SetupSettingsFileWatcher();
|
||||||
|
|
||||||
@@ -469,7 +477,13 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
|
|
||||||
public PasteAIConfiguration PasteAIConfiguration
|
public PasteAIConfiguration PasteAIConfiguration
|
||||||
{
|
{
|
||||||
get => _advancedPasteSettings.Properties.PasteAIConfiguration;
|
get
|
||||||
|
{
|
||||||
|
// Ensure PasteAIConfiguration is never null for XAML binding
|
||||||
|
_advancedPasteSettings.Properties.PasteAIConfiguration ??= new PasteAIConfiguration();
|
||||||
|
return _advancedPasteSettings.Properties.PasteAIConfiguration;
|
||||||
|
}
|
||||||
|
|
||||||
set
|
set
|
||||||
{
|
{
|
||||||
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))
|
if (!ReferenceEquals(value, _advancedPasteSettings.Properties.PasteAIConfiguration))
|
||||||
|
|||||||
@@ -113,6 +113,12 @@ 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;
|
||||||
|
|
||||||
|
// Null-safe access in case property wasn't upgraded yet - default to false
|
||||||
|
_cursorWrapDisableOnSingleMonitor = CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor?.Value ?? false;
|
||||||
|
|
||||||
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);
|
||||||
@@ -1000,13 +1006,6 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
|
|||||||
GeneralSettingsConfig.Enabled.CursorWrap = value;
|
GeneralSettingsConfig.Enabled.CursorWrap = value;
|
||||||
OnPropertyChanged(nameof(IsCursorWrapEnabled));
|
OnPropertyChanged(nameof(IsCursorWrapEnabled));
|
||||||
|
|
||||||
// Auto-enable the AutoActivate setting when CursorWrap is enabled
|
|
||||||
// This ensures cursor wrapping is active immediately after enabling
|
|
||||||
if (value && !_cursorWrapAutoActivate)
|
|
||||||
{
|
|
||||||
CursorWrapAutoActivate = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
OutGoingGeneralSettings outgoing = new OutGoingGeneralSettings(GeneralSettingsConfig);
|
||||||
SendConfigMSG(outgoing.ToString());
|
SendConfigMSG(outgoing.ToString());
|
||||||
|
|
||||||
@@ -1083,6 +1082,62 @@ 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 bool CursorWrapDisableOnSingleMonitor
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return _cursorWrapDisableOnSingleMonitor;
|
||||||
|
}
|
||||||
|
|
||||||
|
set
|
||||||
|
{
|
||||||
|
if (value != _cursorWrapDisableOnSingleMonitor)
|
||||||
|
{
|
||||||
|
_cursorWrapDisableOnSingleMonitor = value;
|
||||||
|
|
||||||
|
// Ensure the property exists before setting value
|
||||||
|
if (CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor == null)
|
||||||
|
{
|
||||||
|
CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor = new BoolProperty(value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CursorWrapSettingsConfig.Properties.DisableCursorWrapOnSingleMonitor.Value = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
NotifyCursorWrapPropertyChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
|
public void NotifyCursorWrapPropertyChanged([CallerMemberName] string propertyName = null)
|
||||||
{
|
{
|
||||||
OnPropertyChanged(propertyName);
|
OnPropertyChanged(propertyName);
|
||||||
@@ -1154,5 +1209,7 @@ 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
|
||||||
|
private bool _cursorWrapDisableOnSingleMonitor; // Disable cursor wrap when only one monitor is connected
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user