Merge remote-tracking branch 'origin/main' into dev/snickler/net10-upgrade

This commit is contained in:
Jeremy Sinclair
2025-08-21 13:50:00 -04:00
247 changed files with 9511 additions and 909 deletions

View File

@@ -25,11 +25,14 @@ ADMINS
adml adml
admx admx
advancedpaste advancedpaste
advancedpasteui
advancedpasteuishortcut
advfirewall advfirewall
AFeature AFeature
affordances affordances
AFX AFX
AGGREGATABLE AGGREGATABLE
AHK
AHybrid AHybrid
akv akv
ALarger ALarger
@@ -40,6 +43,7 @@ ALLINPUT
Allman Allman
Allmodule Allmodule
ALLOWUNDO ALLOWUNDO
allpc
ALLVIEW ALLVIEW
ALPHATYPE ALPHATYPE
AModifier AModifier
@@ -629,6 +633,7 @@ HKCU
hkey hkey
HKLM HKLM
HKM HKM
hkmng
HKPD HKPD
HKU HKU
HMD HMD
@@ -646,7 +651,11 @@ Hostx
hotfixes hotfixes
hotkeycontrol hotkeycontrol
HOTKEYF HOTKEYF
hotkeylockmachine
hotkeyreconnect
hotkeys hotkeys
hotkeyswitch
hotkeytoggleeasymouse
hotlight hotlight
hotspot hotspot
HPAINTBUFFER HPAINTBUFFER
@@ -659,6 +668,7 @@ HROW
hsb hsb
HSCROLL HSCROLL
hsi hsi
HSpeed
HTCLIENT HTCLIENT
hthumbnail hthumbnail
HTOUCHINPUT HTOUCHINPUT
@@ -704,9 +714,12 @@ IMAGERESIZERCONTEXTMENU
IMAGERESIZEREXT IMAGERESIZEREXT
imageresizerinput imageresizerinput
imageresizersettings imageresizersettings
imagetotext
imagetotextshortcut
imagingdevices imagingdevices
ime ime
imgflip imgflip
inapp
inbox inbox
INCONTACT INCONTACT
Indo Indo
@@ -760,6 +773,7 @@ istep
ith ith
ITHUMBNAIL ITHUMBNAIL
IUI IUI
IUWP
IWIC IWIC
jfif jfif
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
@@ -789,6 +803,7 @@ keyvault
KILLFOCUS KILLFOCUS
killrunner killrunner
kmph kmph
kvp
Kybd Kybd
lastcodeanalysissucceeded lastcodeanalysissucceeded
LASTEXITCODE LASTEXITCODE
@@ -827,6 +842,7 @@ localappdata
localpackage localpackage
LOCALSYSTEM LOCALSYSTEM
LOCATIONCHANGE LOCATIONCHANGE
LOCKMACHINE
LOCKTYPE LOCKTYPE
LOGFONT LOGFONT
LOGFONTW LOGFONTW
@@ -912,6 +928,7 @@ MDL
mdtext mdtext
mdtxt mdtxt
mdwn mdwn
measuretool
meme meme
memicmp memicmp
MENUITEMINFO MENUITEMINFO
@@ -961,6 +978,7 @@ MOUSEHWHEEL
MOUSEINPUT MOUSEINPUT
mousejump mousejump
mousepointer mousepointer
mousepointercrosshairs
mouseutils mouseutils
MOVESIZEEND MOVESIZEEND
MOVESIZESTART MOVESIZESTART
@@ -1100,6 +1118,7 @@ NOTSRCCOPY
NOTSRCERASE NOTSRCERASE
notwindows notwindows
NOTXORPEN NOTXORPEN
nowarn
NOZORDER NOZORDER
NPH NPH
npmjs npmjs
@@ -1161,6 +1180,18 @@ PARENTRELATIVEFORADDRESSBAR
PARENTRELATIVEPARSING PARENTRELATIVEPARSING
parray parray
PARTIALCONFIRMATIONDIALOGTITLE PARTIALCONFIRMATIONDIALOGTITLE
pasteashtmlfile
pasteashtmlfileshortcut
pasteasjson
pasteasjsonshortcut
pasteasmarkdown
pasteasmarkdownshortcut
pasteasplaintext
pasteasplaintextshortcut
pasteaspngfile
pasteaspngfileshortcut
pasteastxtfile
pasteastxtfileshortcut
PATCOPY PATCOPY
PATHMUSTEXIST PATHMUSTEXIST
PATINVERT PATINVERT
@@ -1228,6 +1259,7 @@ Pomodoro
Popups Popups
POPUPWINDOW POPUPWINDOW
POSITIONITEM POSITIONITEM
powerocr
POWERRENAMECONTEXTMENU POWERRENAMECONTEXTMENU
powerrenameinput powerrenameinput
POWERRENAMETEST POWERRENAMETEST
@@ -1368,6 +1400,7 @@ Removelnk
renamable renamable
RENAMEONCOLLISION RENAMEONCOLLISION
reparented reparented
reparenthotkey
reparenting reparenting
reportfileaccesses reportfileaccesses
requery requery
@@ -1617,6 +1650,7 @@ STYLECHANGED
STYLECHANGING STYLECHANGING
subkeys subkeys
sublang sublang
Subdomain
SUBMODULEUPDATE SUBMODULEUPDATE
subresource subresource
Superbar Superbar
@@ -1687,6 +1721,7 @@ THH
THICKFRAME THICKFRAME
THISCOMPONENT THISCOMPONENT
throughs throughs
thumbnailhotkey
TILEDWINDOW TILEDWINDOW
TILLSON TILLSON
timedate timedate
@@ -1701,6 +1736,7 @@ tlb
tlbimp tlbimp
tlc tlc
TNP TNP
TOGGLEEASYMOUSE
Toolhelp Toolhelp
toolkitconverters toolkitconverters
toolwindow toolwindow
@@ -1714,6 +1750,7 @@ tracelogging
tracerpt tracerpt
trackbar trackbar
trafficmanager trafficmanager
transcodetomp
transicc transicc
TRAYMOUSEMESSAGE TRAYMOUSEMESSAGE
triaging triaging
@@ -1828,6 +1865,7 @@ VSINSTALLDIR
VSM VSM
vso vso
vsonline vsonline
VSpeed
vstemplate vstemplate
vstest vstest
VSTHRD VSTHRD
@@ -1964,10 +2002,13 @@ XNamespace
Xoshiro Xoshiro
XPels XPels
XPixel XPixel
XPos
XResource XResource
xsi xsi
XSpeed
XStr XStr
xstyler xstyler
XTimer
XUP XUP
XVIRTUALSCREEN XVIRTUALSCREEN
xxxxxx xxxxxx
@@ -1977,7 +2018,10 @@ YIncrement
yinle yinle
yinyue yinyue
YPels YPels
YPos
YResolution YResolution
YSpeed
YTimer
YStr YStr
YVIRTUALSCREEN YVIRTUALSCREEN
ZEROINIT ZEROINIT

View File

@@ -57,12 +57,19 @@ $totalList = $projFiles | ForEach-Object -Parallel {
$p = -split $p $p = -split $p
$p = $p[1, 2] $p = $p[1, 2]
$tempString = $p[0] + " " + $p[1] $tempString = $p[0]
if(![string]::IsNullOrWhiteSpace($tempString)) if([string]::IsNullOrWhiteSpace($tempString))
{ {
echo "- $tempString"; Continue
} }
if($tempString.StartsWith("Microsoft.") -Or $tempString.StartsWith("System."))
{
Continue
}
echo "- $tempString"
} }
$csproj = $null; $csproj = $null;
} }

View File

@@ -21,4 +21,13 @@ if (-not $?)
exit 1 exit 1
} }
# Ignore NU1503 on vcxproj files
dotnet restore $solution /nowarn:NU1503
if ($lastExitCode -ne 0)
{
$result = $lastExitCode
Write-Error "Error running dotnet restore, with the exit code $lastExitCode. Please verify logs on the nuget package versions."
exit $result
}
exit 0 exit 0

View File

@@ -65,7 +65,8 @@
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed --> <!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
<PackageVersion Include="Moq" Version="4.18.4" /> <PackageVersion Include="Moq" Version="4.18.4" />
<PackageVersion Include="MSTest" Version="3.8.3" /> <PackageVersion Include="MSTest" Version="3.8.3" />
<PackageVersion Include="NLog" Version="5.0.4" /> <PackageVersion Include="Newtonsoft.Json" Version="13.0.3" />
<PackageVersion Include="NLog" Version="5.2.8" />
<PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" /> <PackageVersion Include="NLog.Extensions.Logging" Version="5.3.8" />
<PackageVersion Include="NLog.Schema" Version="5.2.8" /> <PackageVersion Include="NLog.Schema" Version="5.2.8" />
<PackageVersion Include="OpenAI" Version="2.0.0" /> <PackageVersion Include="OpenAI" Version="2.0.0" />
@@ -103,7 +104,7 @@
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" /> <PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
<PackageVersion Include="UnicodeInformation" Version="2.6.0" /> <PackageVersion Include="UnicodeInformation" Version="2.6.0" />
<PackageVersion Include="UnitsNet" Version="5.56.0" /> <PackageVersion Include="UnitsNet" Version="5.56.0" />
<PackageVersion Include="UTF.Unknown" Version="2.5.1" /> <PackageVersion Include="UTF.Unknown" Version="2.6.0" />
<PackageVersion Include="WinUIEx" Version="2.2.0" /> <PackageVersion Include="WinUIEx" Version="2.2.0" />
<PackageVersion Include="WPF-UI" Version="3.0.5" /> <PackageVersion Include="WPF-UI" Version="3.0.5" />
<PackageVersion Include="WyHash" Version="1.0.5" /> <PackageVersion Include="WyHash" Version="1.0.5" />
@@ -113,4 +114,4 @@
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" /> <PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" /> <PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
</ItemGroup> </ItemGroup>
</Project> </Project>

137
NOTICE.md
View File

@@ -1491,93 +1491,50 @@ SOFTWARE.
## NuGet Packages used by PowerToys ## NuGet Packages used by PowerToys
- AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta - AdaptiveCards.ObjectModel.WinUI3
- AdaptiveCards.Rendering.WinUI3 2.1.0-beta - AdaptiveCards.Rendering.WinUI3
- AdaptiveCards.Templating 2.0.5 - AdaptiveCards.Templating
- Appium.WebDriver 4.4.5 - Appium.WebDriver
- Azure.AI.OpenAI 1.0.0-beta.17 - Azure.AI.OpenAI
- CoenM.ImageSharp.ImageHash 1.3.6 - CoenM.ImageSharp.ImageHash
- CommunityToolkit.Common 8.4.0 - CommunityToolkit.Common
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock 0.1.250703-build.2173 - CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
- CommunityToolkit.Mvvm 8.4.0 - CommunityToolkit.Mvvm
- CommunityToolkit.WinUI.Animations 8.2.250402 - CommunityToolkit.WinUI.Animations
- CommunityToolkit.WinUI.Collections 8.2.250402 - CommunityToolkit.WinUI.Collections
- CommunityToolkit.WinUI.Controls.Primitives 8.2.250402 - CommunityToolkit.WinUI.Controls.Primitives
- CommunityToolkit.WinUI.Controls.Segmented 8.2.250402 - CommunityToolkit.WinUI.Controls.Segmented
- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250402 - CommunityToolkit.WinUI.Controls.SettingsControls
- CommunityToolkit.WinUI.Controls.Sizers 8.2.250402 - CommunityToolkit.WinUI.Controls.Sizers
- CommunityToolkit.WinUI.Converters 8.2.250402 - CommunityToolkit.WinUI.Converters
- CommunityToolkit.WinUI.Extensions 8.2.250402 - CommunityToolkit.WinUI.Extensions
- CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2 - CommunityToolkit.WinUI.UI.Controls.DataGrid
- CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2 - CommunityToolkit.WinUI.UI.Controls.Markdown
- ControlzEx 6.0.0 - ControlzEx
- HelixToolkit 2.24.0 - HelixToolkit
- HelixToolkit.Core.Wpf 2.24.0 - HelixToolkit.Core.Wpf
- hyjiacan.pinyin4net 4.1.1 - hyjiacan.pinyin4net
- Interop.Microsoft.Office.Interop.OneNote 1.1.0.2 - Interop.Microsoft.Office.Interop.OneNote
- LazyCache 2.4.0 - LazyCache
- Mages 3.0.0 - Mages
- Markdig.Signed 0.34.0 - Markdig.Signed
- MessagePack 3.1.3 - MessagePack
- Microsoft.Bcl.AsyncInterfaces 10.0.0-preview.7.25380.108 - ModernWpfUI
- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0 - Moq
- Microsoft.CodeAnalysis.NetAnalyzers 10.0.0-preview.25380.108 - MSTest
- Microsoft.Data.Sqlite 10.0.0-preview.7.25380.108 - NLog
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16 - NLog.Extensions.Logging
- Microsoft.DotNet.ILCompiler (A) - NLog.Schema
- Microsoft.Extensions.DependencyInjection 10.0.0-preview.7.25380.108 - OpenAI
- Microsoft.Extensions.Hosting 10.0.0-preview.7.25380.108 - ReverseMarkdown
- Microsoft.Extensions.Hosting.WindowsServices 10.0.0-preview.7.25380.108 - ScipBe.Common.Office.OneNote
- Microsoft.Extensions.Logging 10.0.0-preview.7.25380.108 - SharpCompress
- Microsoft.Extensions.Logging.Abstractions 10.0.0-preview.7.25380.108 - SkiaSharp.Views.WinUI
- Microsoft.NET.ILLink.Tasks (A) - StreamJsonRpc
- Microsoft.SemanticKernel 1.15.0 - StyleCop.Analyzers
- Microsoft.Toolkit.Uwp.Notifications 7.1.2 - UnicodeInformation
- Microsoft.Web.WebView2 1.0.2903.40 - UnitsNet
- Microsoft.Win32.SystemEvents 10.0.0-preview.7.25380.108 - UTF.Unknown
- Microsoft.Windows.Compatibility 10.0.0-preview.7.25380.108 - WinUIEx
- Microsoft.Windows.CsWin32 0.3.183 - WPF-UI
- Microsoft.Windows.CsWinRT 2.2.0 - WyHash
- Microsoft.Windows.SDK.BuildTools 10.0.26100.4188
- Microsoft.WindowsAppSDK 1.7.250513003
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4
- Moq 4.18.4
- MSTest 3.8.3
- NLog.Extensions.Logging 5.3.8
- NLog.Schema 5.2.8
- OpenAI 2.0.0
- ReverseMarkdown 4.1.0
- ScipBe.Common.Office.OneNote 3.0.1
- SharpCompress 0.37.2
- SkiaSharp.Views.WinUI 2.88.9
- StreamJsonRpc 2.21.69
- StyleCop.Analyzers 1.2.0-beta.556
- System.CodeDom 10.0.0-preview.7.25380.108
- System.CommandLine 2.0.0-beta4.22272.1
- System.ComponentModel.Composition 10.0.0-preview.7.25380.108
- System.Configuration.ConfigurationManager 10.0.0-preview.7.25380.108
- System.Data.OleDb 10.0.0-preview.7.25380.108
- System.Data.SqlClient 4.9.0
- System.Diagnostics.EventLog 10.0.0-preview.7.25380.108
- System.Diagnostics.PerformanceCounter 10.0.0-preview.7.25380.108
- System.Drawing.Common 10.0.0-preview.7.25380.108
- System.IO.Abstractions 22.0.13
- System.IO.Abstractions.TestingHelpers 22.0.13
- System.Management 10.0.0-preview.7.25380.108
- System.Net.Http 4.3.4
- System.Private.Uri 4.3.2
- System.Reactive 6.0.1
- System.Runtime.Caching 10.0.0-preview.7.25380.108
- System.ServiceProcess.ServiceController 10.0.0-preview.7.25380.108
- System.Text.Encoding.CodePages 10.0.0-preview.7.25380.108
- System.Text.Json 10.0.0-preview.7.25380.108
- System.Text.RegularExpressions 4.3.1
- UnicodeInformation 2.6.0
- UnitsNet 5.56.0
- UTF.Unknown 2.5.1
- WinUIEx 2.2.0
- WPF-UI 3.0.5
- WyHash 1.0.5

View File

@@ -262,6 +262,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\EventLocker.h = src\common\utils\EventLocker.h src\common\utils\EventLocker.h = src\common\utils\EventLocker.h
src\common\utils\EventWaiter.h = src\common\utils\EventWaiter.h src\common\utils\EventWaiter.h = src\common\utils\EventWaiter.h
src\common\utils\excluded_apps.h = src\common\utils\excluded_apps.h src\common\utils\excluded_apps.h = src\common\utils\excluded_apps.h
src\common\utils\shell_ext_registration.h = src\common\utils\shell_ext_registration.h
src\common\utils\exec.h = src\common\utils\exec.h src\common\utils\exec.h = src\common\utils\exec.h
src\common\utils\game_mode.h = src\common\utils\game_mode.h src\common\utils\game_mode.h = src\common\utils\game_mode.h
src\common\utils\gpo.h = src\common\utils\gpo.h src\common\utils\gpo.h = src\common\utils\gpo.h
@@ -788,6 +789,14 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Window
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}" Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.UnitTestBase", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj", "{00D8659C-2068-40B6-8B86-759CD6284BBB}"
EndProject EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Apps.UnitTests\Microsoft.CmdPal.Ext.Apps.UnitTests.csproj", "{E816D7B1-4688-4ECB-97CC-3D8E798F3830}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Bookmarks.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests\Microsoft.CmdPal.Ext.Bookmarks.UnitTests.csproj", "{E816D7B3-4688-4ECB-97CC-3D8E798F3832}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.WebSearch.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.WebSearch.UnitTests\Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj", "{E816D7B2-4688-4ECB-97CC-3D8E798F3831}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Shell.UnitTests", "src\modules\cmdpal\Tests\Microsoft.CmdPal.Ext.Shell.UnitTests\Microsoft.CmdPal.Ext.Shell.UnitTests.csproj", "{E816D7B4-4688-4ECB-97CC-3D8E798F3833}"
EndProject
Global Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64 Debug|ARM64 = Debug|ARM64
@@ -2850,6 +2859,38 @@ Global
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|ARM64.Build.0 = Release|ARM64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.ActiveCfg = Release|x64
{00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64 {00D8659C-2068-40B6-8B86-759CD6284BBB}.Release|x64.Build.0 = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Debug|x64.Build.0 = Debug|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.ActiveCfg = Release|x64
{E816D7B1-4688-4ECB-97CC-3D8E798F3830}.Release|x64.Build.0 = Release|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Debug|x64.Build.0 = Debug|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.ActiveCfg = Release|x64
{E816D7B3-4688-4ECB-97CC-3D8E798F3832}.Release|x64.Build.0 = Release|x64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Debug|x64.Build.0 = Debug|x64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.ActiveCfg = Release|x64
{E816D7B2-4688-4ECB-97CC-3D8E798F3831}.Release|x64.Build.0 = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.ActiveCfg = Debug|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|ARM64.Build.0 = Debug|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.ActiveCfg = Debug|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Debug|x64.Build.0 = Debug|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.ActiveCfg = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|ARM64.Build.0 = Release|ARM64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.ActiveCfg = Release|x64
{E816D7B4-4688-4ECB-97CC-3D8E798F3833}.Release|x64.Build.0 = Release|x64
EndGlobalSection EndGlobalSection
GlobalSection(SolutionProperties) = preSolution GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE HideSolutionNode = FALSE
@@ -3161,6 +3202,10 @@ Global
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {E816D7B0-4688-4ECB-97CC-3D8E798F3829} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8} {00D8659C-2068-40B6-8B86-759CD6284BBB} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B1-4688-4ECB-97CC-3D8E798F3830} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B3-4688-4ECB-97CC-3D8E798F3832} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B2-4688-4ECB-97CC-3D8E798F3831} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
{E816D7B4-4688-4ECB-97CC-3D8E798F3833} = {8EF25507-2575-4ADE-BF7E-D23376903AB8}
EndGlobalSection EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0} SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}

View File

@@ -71,6 +71,41 @@ When the user changes settings in the UI:
3. The runner calls the `set_config` function on the appropriate module 3. The runner calls the `set_config` function on the appropriate module
4. The module parses the JSON and applies the new settings 4. The module parses the JSON and applies the new settings
# Shortcut Conflict Detection
Steps to enable conflict detection for a hotkey:
### 1. Implement module interface for hotkeys
Ensure the module interface provides either `size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size)` or `std::optional<HotkeyEx> GetHotkeyEx()`.
- If not yet implemented, you need to add it so that it returns all hotkeys used by the module.
- **Important**: The order of the returned hotkeys matters. This order is used as an index to uniquely identify each hotkey for conflict detection and lookup.
- For reference, see: `src/modules/AdvancedPaste/AdvancedPasteModuleInterface/dllmain.cpp`
### 2. Implement IHotkeyConfig in the module settings (UI side)
Make sure the modules settings file inherits from `IHotkeyConfig` and implements `HotkeyAccessor[] GetAllHotkeyAccessors()`.
- This method should return all hotkeys used in the module.
- **Important**: The order of the returned hotkeys must be consistent with step 1 (`get_hotkeys()` or `GetHotkeyEx()`).
- For reference, see: `src/settings-ui/Settings.UI.Library/AdvancedPasteSettings.cs`
- **_Note:_** `HotkeyAccessor` is a wrapper around HotkeySettings.
It provides both `getter` and `setter` methods to read and update the corresponding hotkey.
Additionally, each `HotkeyAccessor` requires a resource string that describes the purpose of the hotkey.
This string is typically defined in: `src/settings-ui/Settings.UI/Strings/en-us/Resources.resw`
### 3. Update the modules ViewModel
The corresponding ViewModel should inherit from `PageViewModelBase` and implement `Dictionary<string, HotkeySettings[]> GetAllHotkeySettings()`.
- This method should return all hotkeys, maintaining the same order as in steps 1 and 2.
- For reference, see: `src/settings-ui/Settings.UI/ViewModels/AdvancedPasteViewModel.cs`
### 4. Ensure the modules Views call `OnPageLoaded()`
Once the modules view is loaded, make sure to invoke the ViewModels `OnPageLoaded()` method:
```cs
Loaded += (s, e) => ViewModel.OnPageLoaded();
```
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
## Debugging Settings ## Debugging Settings
To debug settings issues: To debug settings issues:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.4 KiB

After

Width:  |  Height:  |  Size: 18 KiB

View File

@@ -14,21 +14,6 @@
<DirectoryRef Id="FileLocksmithAssetsInstallFolder" FileSource="$(var.FileLocksmithAssetsFilesPath)"> <DirectoryRef Id="FileLocksmithAssetsInstallFolder" FileSource="$(var.FileLocksmithAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 --> <!-- Generated by generateFileComponents.ps1 -->
<!--FileLocksmithAssetsFiles_Component_Def--> <!--FileLocksmithAssetsFiles_Component_Def-->
<!-- !Warning! Make sure to change Component Guid if you update something here -->
<Component Id="Module_FileLocksmith" Guid="108D3EC1-E6E0-4E81-88EF-25966133CB41" Win64="yes">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{84D68575-E186-46AD-B0CB-BAEB45EE29C0}">
<RegistryValue Type="string" Value="File Locksmith Shell Extension" />
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.FileLocksmithExt.dll" />
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
</RegistryKey>
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\AllFileSystemObjects\ShellEx\ContextMenuHandlers\FileLocksmithExt">
<RegistryValue Type="string" Value="{84D68575-E186-46AD-B0CB-BAEB45EE29C0}"/>
</RegistryKey>
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\Drive\ShellEx\ContextMenuHandlers\FileLocksmithExt">
<RegistryValue Type="string" Value="{84D68575-E186-46AD-B0CB-BAEB45EE29C0}"/>
</RegistryKey>
</Component>
</DirectoryRef> </DirectoryRef>
<ComponentGroup Id="FileLocksmithComponentGroup"> <ComponentGroup Id="FileLocksmithComponentGroup">
@@ -38,7 +23,6 @@
</RegistryKey> </RegistryKey>
<RemoveFolder Id="RemoveFolderFileLocksmithAssetsFolder" Directory="FileLocksmithAssetsInstallFolder" On="uninstall"/> <RemoveFolder Id="RemoveFolderFileLocksmithAssetsFolder" Directory="FileLocksmithAssetsInstallFolder" On="uninstall"/>
</Component> </Component>
<ComponentRef Id="Module_FileLocksmith" />
</ComponentGroup> </ComponentGroup>
</Fragment> </Fragment>

View File

@@ -16,71 +16,6 @@
<!-- Generated by generateFileComponents.ps1 --> <!-- Generated by generateFileComponents.ps1 -->
<!--ImageResizerAssetsFiles_Component_Def--> <!--ImageResizerAssetsFiles_Component_Def-->
<Component Id="Module_ImageResizer_Registry" Win64="yes">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{51B4D7E5-7568-4234-B4BB-47FB3C016A69}\InprocServer32">
<RegistryValue Value="[WinUI3AppsInstallFolder]PowerToys.ImageResizerExt.dll" Type="string" />
<RegistryValue Name="ThreadingModel" Value="Apartment" Type="string" />
</RegistryKey>
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\Directory\ShellEx\DragDropHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<!-- Registry Keys for the context menu handler for each of the following image formats: bmp, dib, gif, jfif, jpe, jpeg, jpg, jxr, png, rle, tif, tiff, wdp -->
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.bmp\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.dib\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.gif\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.jfif\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.jpe\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.jpeg\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.jpg\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.jxr\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.png\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.rle\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.tif\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.tiff\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
<RegistryValue Root="$(var.RegistryScope)"
Key="SOFTWARE\Classes\SystemFileAssociations\.wdp\ShellEx\ContextMenuHandlers\ImageResizer"
Value="{51B4D7E5-7568-4234-B4BB-47FB3C016A69}"
Type="string" />
</Component>
</DirectoryRef> </DirectoryRef>
<ComponentGroup Id="ImageResizerComponentGroup"> <ComponentGroup Id="ImageResizerComponentGroup">
@@ -90,7 +25,6 @@
</RegistryKey> </RegistryKey>
<RemoveFolder Id="RemoveFolderImageResizerAssetsFolder" Directory="ImageResizerAssetsFolder" On="uninstall"/> <RemoveFolder Id="RemoveFolderImageResizerAssetsFolder" Directory="ImageResizerAssetsFolder" On="uninstall"/>
</Component> </Component>
<ComponentRef Id="Module_ImageResizer_Registry" />
</ComponentGroup> </ComponentGroup>
</Fragment> </Fragment>
</Wix> </Wix>

View File

@@ -18,19 +18,6 @@
<DirectoryRef Id="NewPlusAssetsInstallFolder" FileSource="$(var.NewPlusAssetsFilesPath)"> <DirectoryRef Id="NewPlusAssetsInstallFolder" FileSource="$(var.NewPlusAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 --> <!-- Generated by generateFileComponents.ps1 -->
<!--NewPlusAssetsFiles_Component_Def--> <!--NewPlusAssetsFiles_Component_Def-->
<!-- NewPlus Shell Extension for Win10 registration -->
<Component Id="NewPlus_ShellExtension_win10" Guid="D5456D4A-6EEC-4B85-944D-6A6A4A74FFA6" Win64="yes">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{FF90D477-E32A-4BE8-8CC5-A502A97F5401}">
<RegistryValue Type="string" Value="NewPlus Shell Extension Win10" />
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.NewPlus.ShellExtension.win10.dll" />
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
</RegistryKey>
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\Directory\background\ShellEx\ContextMenuHandlers\NewPlusShellExtensionWin10">
<RegistryValue Type="string" Value="{FF90D477-E32A-4BE8-8CC5-A502A97F5401}"/>
</RegistryKey>
</Component>
</DirectoryRef> </DirectoryRef>
<ComponentGroup Id="NewPlusComponentGroup"> <ComponentGroup Id="NewPlusComponentGroup">
@@ -40,8 +27,7 @@
</RegistryKey> </RegistryKey>
<RemoveFolder Id="RemoveFolderNewPlusAssetsFolder" Directory="NewPlusAssetsInstallFolder" On="uninstall"/> <RemoveFolder Id="RemoveFolderNewPlusAssetsFolder" Directory="NewPlusAssetsInstallFolder" On="uninstall"/>
</Component> </Component>
<ComponentRef Id="NewPlus_ShellExtension_win10" /> </ComponentGroup>
</ComponentGroup>
<!-- Example templates --> <!-- Example templates -->
@@ -81,7 +67,7 @@
</Component> </Component>
<ComponentRef Id="NewPlusTemplateFiles_Component" /> <ComponentRef Id="NewPlusTemplateFiles_Component" />
<ComponentRef Id="NewPlusTemplateSubFiles_Component" /> <ComponentRef Id="NewPlusTemplateSubFiles_Component" />
</ComponentGroup> </ComponentGroup>
</Fragment> </Fragment>
</Wix> </Wix>

View File

@@ -14,22 +14,6 @@
<DirectoryRef Id="PowerRenameAssetsFolder" FileSource="$(var.PowerRenameAssetsFilesPath)"> <DirectoryRef Id="PowerRenameAssetsFolder" FileSource="$(var.PowerRenameAssetsFilesPath)">
<!-- Generated by generateFileComponents.ps1 --> <!-- Generated by generateFileComponents.ps1 -->
<!--PowerRenameAssetsFiles_Component_Def--> <!--PowerRenameAssetsFiles_Component_Def-->
<!-- !Warning! Make sure to change Component Guid if you update something here -->
<Component Id="Module_PowerRename" Guid="40D43079-240E-402D-8CE8-571BFFA71175" Win64="yes">
<RegistryKey Root="$(var.RegistryScope)" Key="Software\Classes\CLSID\{0440049F-D1DC-4E46-B27B-98393D79486B}">
<RegistryValue Type="string" Value="PowerRename Shell Extension" />
<RegistryValue Type="string" Name="ContextMenuOptIn" Value="" />
<RegistryValue Type="string" Key="InprocServer32" Value="[WinUI3AppsInstallFolder]PowerToys.PowerRenameExt.dll" />
<RegistryValue Type="string" Key="InprocServer32" Name="ThreadingModel" Value="Apartment" />
</RegistryKey>
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\AllFileSystemObjects\ShellEx\ContextMenuHandlers\PowerRenameExt">
<RegistryValue Type="string" Value="{0440049F-D1DC-4E46-B27B-98393D79486B}"/>
</RegistryKey>
<RegistryKey Root="$(var.RegistryScope)" Key="SOFTWARE\Classes\Directory\background\ShellEx\ContextMenuHandlers\PowerRenameExt">
<RegistryValue Type="string" Value="{0440049F-D1DC-4E46-B27B-98393D79486B}"/>
</RegistryKey>
</Component>
</DirectoryRef> </DirectoryRef>
<ComponentGroup Id="PowerRenameComponentGroup"> <ComponentGroup Id="PowerRenameComponentGroup">
@@ -39,7 +23,6 @@
</RegistryKey> </RegistryKey>
<RemoveFolder Id="RemoveFolderPowerRenameAssetsFolder" Directory="PowerRenameAssetsFolder" On="uninstall"/> <RemoveFolder Id="RemoveFolderPowerRenameAssetsFolder" Directory="PowerRenameAssetsFolder" On="uninstall"/>
</Component> </Component>
<ComponentRef Id="Module_PowerRename" />
</ComponentGroup> </ComponentGroup>
</Fragment> </Fragment>

View File

@@ -176,6 +176,18 @@
<Custom Action="UnRegisterContextMenuPackages" Before="RemoveFiles"> <Custom Action="UnRegisterContextMenuPackages" Before="RemoveFiles">
Installed AND (REMOVE="ALL") Installed AND (REMOVE="ALL")
</Custom> </Custom>
<Custom Action="CleanImageResizerRuntimeRegistry" Before="RemoveFiles">
Installed AND (REMOVE="ALL")
</Custom>
<Custom Action="CleanFileLocksmithRuntimeRegistry" Before="RemoveFiles">
Installed AND (REMOVE="ALL")
</Custom>
<Custom Action="CleanPowerRenameRuntimeRegistry" Before="RemoveFiles">
Installed AND (REMOVE="ALL")
</Custom>
<Custom Action="CleanNewPlusRuntimeRegistry" Before="RemoveFiles">
Installed AND (REMOVE="ALL")
</Custom>
<Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles"> <Custom Action="UnRegisterCmdPalPackage" Before="RemoveFiles">
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL") Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
</Custom> </Custom>
@@ -437,6 +449,35 @@
Execute="deferred" Execute="deferred"
BinaryKey="PTCustomActions" BinaryKey="PTCustomActions"
DllEntry="UnRegisterContextMenuPackagesCA" DllEntry="UnRegisterContextMenuPackagesCA"
/>
<CustomAction Id="CleanImageResizerRuntimeRegistry"
Return="ignore"
Impersonate="yes"
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="CleanImageResizerRuntimeRegistryCA"
/>
<CustomAction Id="CleanFileLocksmithRuntimeRegistry"
Return="ignore"
Impersonate="yes"
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="CleanFileLocksmithRuntimeRegistryCA"
/>
<CustomAction Id="CleanPowerRenameRuntimeRegistry"
Return="ignore"
Impersonate="yes"
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="CleanPowerRenameRuntimeRegistryCA"
/>
<CustomAction Id="CleanNewPlusRuntimeRegistry"
Return="ignore"
Impersonate="yes"
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="CleanNewPlusRuntimeRegistryCA"
/> />
<CustomAction Id="UnRegisterCmdPalPackage" <CustomAction Id="UnRegisterCmdPalPackage"

View File

@@ -1153,6 +1153,113 @@ UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall)
return WcaFinalize(er); return WcaFinalize(er);
} }
UINT __stdcall CleanImageResizerRuntimeRegistryCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
hr = WcaInitialize(hInstall, "CleanImageResizerRuntimeRegistryCA");
try
{
const wchar_t* CLSID_STR = L"{51B4D7E5-7568-4234-B4BB-47FB3C016A69}";
const wchar_t* exts[] = { L".bmp", L".dib", L".gif", L".jfif", L".jpe", L".jpeg", L".jpg", L".jxr", L".png", L".rle", L".tif", L".tiff", L".wdp" };
auto deleteKeyRecursive = [](HKEY root, const std::wstring &path) {
RegDeleteTreeW(root, path.c_str());
};
// InprocServer32 chain root CLSID
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
// DragDrop handler
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\ShellEx\\DragDropHandlers\\ImageResizer");
// Extensions
for (auto ext : exts)
{
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\SystemFileAssociations\\" + std::wstring(ext) + L"\\ShellEx\\ContextMenuHandlers\\ImageResizer");
}
// Sentinel
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\ImageResizer");
}
catch (...)
{
er = ERROR_INSTALL_FAILURE;
}
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
return WcaFinalize(er);
}
UINT __stdcall CleanFileLocksmithRuntimeRegistryCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
hr = WcaInitialize(hInstall, "CleanFileLocksmithRuntimeRegistryCA");
try
{
const wchar_t* CLSID_STR = L"{84D68575-E186-46AD-B0CB-BAEB45EE29C0}";
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
RegDeleteTreeW(root, path.c_str());
};
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt");
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Drive\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt");
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\FileLocksmith");
}
catch (...)
{
er = ERROR_INSTALL_FAILURE;
}
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
return WcaFinalize(er);
}
UINT __stdcall CleanPowerRenameRuntimeRegistryCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
hr = WcaInitialize(hInstall, "CleanPowerRenameRuntimeRegistryCA");
try
{
const wchar_t* CLSID_STR = L"{0440049F-D1DC-4E46-B27B-98393D79486B}";
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
RegDeleteTreeW(root, path.c_str());
};
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\PowerRenameExt");
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\PowerRenameExt");
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\PowerRename");
}
catch (...)
{
er = ERROR_INSTALL_FAILURE;
}
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
return WcaFinalize(er);
}
UINT __stdcall CleanNewPlusRuntimeRegistryCA(MSIHANDLE hInstall)
{
HRESULT hr = S_OK;
UINT er = ERROR_SUCCESS;
hr = WcaInitialize(hInstall, "CleanNewPlusRuntimeRegistryCA");
try
{
const wchar_t* CLSID_STR = L"{FF90D477-E32A-4BE8-8CC5-A502A97F5401}";
auto deleteKeyRecursive = [](HKEY root, const std::wstring& path) {
RegDeleteTreeW(root, path.c_str());
};
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\CLSID\\" + std::wstring(CLSID_STR));
deleteKeyRecursive(HKEY_CURRENT_USER, L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\NewPlusShellExtensionWin10");
RegDeleteTreeW(HKEY_CURRENT_USER, L"Software\\Microsoft\\PowerToys\\NewPlus");
}
catch (...)
{
er = ERROR_INSTALL_FAILURE;
}
er = er == ERROR_SUCCESS ? (SUCCEEDED(hr) ? ERROR_SUCCESS : ERROR_INSTALL_FAILURE) : er;
return WcaFinalize(er);
}
UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall) UINT __stdcall TerminateProcessesCA(MSIHANDLE hInstall)
{ {
HRESULT hr = S_OK; HRESULT hr = S_OK;

View File

@@ -28,3 +28,7 @@ EXPORTS
UninstallCommandNotFoundModuleCA UninstallCommandNotFoundModuleCA
UpgradeCommandNotFoundModuleCA UpgradeCommandNotFoundModuleCA
UnsetAdvancedPasteAPIKeyCA UnsetAdvancedPasteAPIKeyCA
CleanImageResizerRuntimeRegistryCA
CleanFileLocksmithRuntimeRegistryCA
CleanPowerRenameRuntimeRegistryCA
CleanNewPlusRuntimeRegistryCA

View File

@@ -0,0 +1,266 @@
// Shared runtime shell extension registration utility for PowerToys modules.
// Provides a generic EnsureRegistered function so individual modules only need
// to supply a specification (CLSID, sentinel, handler key paths, etc.).
#pragma once
#include <string>
#include <vector>
#include <windows.h>
#include <shlwapi.h>
#include "../logger/logger.h"
namespace runtime_shell_ext
{
struct Spec
{
// Mandatory
std::wstring clsid; // e.g. {GUID}
std::wstring sentinelKey; // e.g. Software\\Microsoft\\PowerToys\\ModuleName
std::wstring sentinelValue; // e.g. ContextMenuRegistered
std::vector<std::wstring> dllFileCandidates; // relative filenames (pick first existing)
std::vector<std::wstring> contextMenuHandlerKeyPaths; // full HKCU relative paths where default value = CLSID
// Optional
std::wstring friendlyName; // if non-empty written as default under CLSID root
bool writeOptInEmptyValue = true; // write ContextMenuOptIn="" under CLSID root (legacy pattern)
bool writeThreadingModel = true; // write Apartment threading model
std::vector<std::wstring> extraAssociationPaths; // additional key paths (DragDropHandlers etc.) default=CLSID
std::vector<std::wstring> systemFileAssocExtensions; // e.g. .png -> Software\\Classes\\SystemFileAssociations\\.png\\ShellEx\\ContextMenuHandlers\\<HandlerName>
std::wstring systemFileAssocHandlerName; // e.g. ImageResizer
std::wstring representativeSystemExt; // used to decide if associations need repair (.png)
bool logRepairs = true;
};
namespace detail
{
// Minimal RAII wrapper for HKEY
struct unique_hkey
{
HKEY h{ nullptr };
unique_hkey() = default;
explicit unique_hkey(HKEY handle) : h(handle) {}
~unique_hkey() { if (h) RegCloseKey(h); }
unique_hkey(const unique_hkey&) = delete;
unique_hkey& operator=(const unique_hkey&) = delete;
unique_hkey(unique_hkey&& other) noexcept : h(other.h) { other.h = nullptr; }
unique_hkey& operator=(unique_hkey&& other) noexcept { if (this != &other) { if (h) RegCloseKey(h); h = other.h; other.h = nullptr; } return *this; }
HKEY get() const { return h; }
HKEY* put() { if (h) { RegCloseKey(h); h = nullptr; } return &h; }
};
inline std::wstring base_dir_from_module(HMODULE h)
{
wchar_t buf[MAX_PATH];
if (GetModuleFileNameW(h, buf, MAX_PATH))
{
PathRemoveFileSpecW(buf);
return buf;
}
return L"";
}
inline std::wstring pick_existing_dll(const std::wstring& base, const std::vector<std::wstring>& candidates)
{
for (const auto& rel : candidates)
{
std::wstring full = base + L"\\" + rel;
if (GetFileAttributesW(full.c_str()) != INVALID_FILE_ATTRIBUTES)
{
return full;
}
}
if (!candidates.empty())
{
return base + L"\\" + candidates.front();
}
return L"";
}
inline bool sentinel_exists(const Spec& spec)
{
unique_hkey key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
return false;
DWORD v = 0; DWORD sz = sizeof(v);
return RegQueryValueExW(key.get(), spec.sentinelValue.c_str(), nullptr, nullptr, reinterpret_cast<LPBYTE>(&v), &sz) == ERROR_SUCCESS && v == 1;
}
inline void write_sentinel(const Spec& spec)
{
unique_hkey key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
{
DWORD one = 1;
RegSetValueExW(key.get(), spec.sentinelValue.c_str(), 0, REG_DWORD, reinterpret_cast<const BYTE*>(&one), sizeof(one));
}
}
inline void write_inproc_server(const Spec& spec, const std::wstring& dllPath)
{
using namespace std::string_literals;
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
std::wstring inprocKey = clsidRoot + L"\\InprocServer32";
{
unique_hkey key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, clsidRoot.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
{
if (!spec.friendlyName.empty())
{
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(spec.friendlyName.c_str()), static_cast<DWORD>((spec.friendlyName.size() + 1) * sizeof(wchar_t)));
}
if (spec.writeOptInEmptyValue)
{
const wchar_t* optIn = L"ContextMenuOptIn";
const wchar_t empty = L'\0';
RegSetValueExW(key.get(), optIn, 0, REG_SZ, reinterpret_cast<const BYTE*>(&empty), sizeof(empty));
}
}
}
unique_hkey key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
{
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(dllPath.c_str()), static_cast<DWORD>((dllPath.size() + 1) * sizeof(wchar_t)));
if (spec.writeThreadingModel)
{
const wchar_t* tm = L"Apartment";
RegSetValueExW(key.get(), L"ThreadingModel", 0, REG_SZ, reinterpret_cast<const BYTE*>(tm), static_cast<DWORD>((wcslen(tm) + 1) * sizeof(wchar_t)));
}
}
}
inline std::wstring read_inproc_server(const Spec& spec)
{
using namespace std::string_literals;
std::wstring inprocKey = L"Software\\Classes\\CLSID\\"s + spec.clsid + L"\\InprocServer32";
unique_hkey key;
if (RegOpenKeyExW(HKEY_CURRENT_USER, inprocKey.c_str(), 0, KEY_READ, key.put()) != ERROR_SUCCESS)
return L"";
wchar_t buf[MAX_PATH]; DWORD sz = sizeof(buf);
if (RegQueryValueExW(key.get(), nullptr, nullptr, nullptr, reinterpret_cast<LPBYTE>(buf), &sz) == ERROR_SUCCESS)
return std::wstring(buf);
return L"";
}
inline void write_default_value_key(const std::wstring& keyPath, const std::wstring& value)
{
unique_hkey key;
if (RegCreateKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, nullptr, 0, KEY_WRITE, nullptr, key.put(), nullptr) == ERROR_SUCCESS)
{
RegSetValueExW(key.get(), nullptr, 0, REG_SZ, reinterpret_cast<const BYTE*>(value.c_str()), static_cast<DWORD>((value.size() + 1) * sizeof(wchar_t)));
}
}
inline bool representative_association_exists(const Spec& spec)
{
using namespace std::string_literals;
if (spec.representativeSystemExt.empty() || spec.systemFileAssocHandlerName.empty())
return true;
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + spec.representativeSystemExt + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
unique_hkey key;
return RegOpenKeyExW(HKEY_CURRENT_USER, keyPath.c_str(), 0, KEY_READ, key.put()) == ERROR_SUCCESS;
}
}
inline bool EnsureRegistered(const Spec& spec, HMODULE moduleInstance)
{
using namespace std::string_literals;
auto base = detail::base_dir_from_module(moduleInstance);
auto dllPath = detail::pick_existing_dll(base, spec.dllFileCandidates);
if (dllPath.empty())
{
Logger::error(L"Runtime registration: cannot locate dll path for CLSID {}", spec.clsid);
return false;
}
bool exists = detail::sentinel_exists(spec);
bool repaired = false;
if (exists)
{
auto current = detail::read_inproc_server(spec);
if (_wcsicmp(current.c_str(), dllPath.c_str()) != 0)
{
detail::write_inproc_server(spec, dllPath);
repaired = true;
}
if (!detail::representative_association_exists(spec))
{
repaired = true;
}
}
if (!exists)
{
detail::write_inproc_server(spec, dllPath);
}
if (!exists || repaired)
{
for (const auto& path : spec.contextMenuHandlerKeyPaths)
{
detail::write_default_value_key(path, spec.clsid);
}
for (const auto& path : spec.extraAssociationPaths)
{
detail::write_default_value_key(path, spec.clsid);
}
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
{
for (const auto& ext : spec.systemFileAssocExtensions)
{
std::wstring path = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
detail::write_default_value_key(path, spec.clsid);
}
}
}
if (!exists)
{
detail::write_sentinel(spec);
Logger::info(L"Runtime registration completed for CLSID {}", spec.clsid);
}
else if (repaired && spec.logRepairs)
{
Logger::info(L"Runtime registration repaired for CLSID {}", spec.clsid);
}
return true;
}
inline bool Unregister(const Spec& spec)
{
using namespace std::string_literals;
// Remove handler key paths
for (const auto& path : spec.contextMenuHandlerKeyPaths)
{
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
}
// Remove extra association paths (e.g., drag & drop handlers)
for (const auto& path : spec.extraAssociationPaths)
{
RegDeleteTreeW(HKEY_CURRENT_USER, path.c_str());
}
// Remove per-extension system file association handler keys
if (!spec.systemFileAssocExtensions.empty() && !spec.systemFileAssocHandlerName.empty())
{
for (const auto& ext : spec.systemFileAssocExtensions)
{
std::wstring keyPath = L"Software\\Classes\\SystemFileAssociations\\"s + ext + L"\\ShellEx\\ContextMenuHandlers\\" + spec.systemFileAssocHandlerName;
RegDeleteTreeW(HKEY_CURRENT_USER, keyPath.c_str());
}
}
// Remove CLSID branch
if (!spec.clsid.empty())
{
std::wstring clsidRoot = L"Software\\Classes\\CLSID\\"s + spec.clsid;
RegDeleteTreeW(HKEY_CURRENT_USER, clsidRoot.c_str());
}
// Remove sentinel value (not deleting entire key to avoid disturbing other values)
if (!spec.sentinelKey.empty() && !spec.sentinelValue.empty())
{
HKEY hKey{};
if (RegOpenKeyExW(HKEY_CURRENT_USER, spec.sentinelKey.c_str(), 0, KEY_SET_VALUE, &hKey) == ERROR_SUCCESS)
{
RegDeleteValueW(hKey, spec.sentinelValue.c_str());
RegCloseKey(hKey);
}
}
Logger::info(L"Successfully unregistered CLSID {}", spec.clsid);
return true;
}
}

View File

@@ -112,7 +112,7 @@ private:
return {}; return {};
} }
static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject) static Hotkey parse_single_hotkey(const winrt::Windows::Data::Json::JsonObject& jsonHotkeyObject, bool isShown = true)
{ {
try try
{ {
@@ -122,6 +122,7 @@ private:
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT); hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL); hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE)); hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
hotkey.isShown = isShown;
return hotkey; return hotkey;
} }
catch (...) catch (...)
@@ -231,8 +232,10 @@ private:
return false; return false;
} }
void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue) void process_additional_action(const winrt::hstring& actionName, const winrt::Windows::Data::Json::IJsonValue& actionValue, bool actionsGroupIsShown = true)
{ {
bool actionIsShown = true;
if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object) if (actionValue.ValueType() != winrt::Windows::Data::Json::JsonValueType::Object)
{ {
return; return;
@@ -240,9 +243,9 @@ private:
const auto action = actionValue.GetObjectW(); const auto action = actionValue.GetObjectW();
if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) if (!action.GetNamedBoolean(JSON_KEY_IS_SHOWN, false) || !actionsGroupIsShown)
{ {
return; actionIsShown = false;
} }
if (action.HasKey(JSON_KEY_SHORTCUT)) if (action.HasKey(JSON_KEY_SHORTCUT))
@@ -250,7 +253,7 @@ private:
const AdditionalAction additionalAction const AdditionalAction additionalAction
{ {
actionName.c_str(), actionName.c_str(),
parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT)) parse_single_hotkey(action.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
}; };
m_additional_actions.push_back(additionalAction); m_additional_actions.push_back(additionalAction);
@@ -259,12 +262,12 @@ private:
{ {
for (const auto& [subActionName, subAction] : action) for (const auto& [subActionName, subAction] : action)
{ {
process_additional_action(subActionName, subAction); process_additional_action(subActionName, subAction, actionIsShown);
} }
} }
} }
void read_settings(PowerToysSettings::PowerToyValues& settings) void read_settings(PowerToysSettings::PowerToyValues& settings)
{ {
const auto settingsObject = settings.get_raw_json(); const auto settingsObject = settings.get_raw_json();
@@ -317,9 +320,21 @@ private:
{ {
const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS); const auto additionalActions = propertiesObject.GetNamedObject(JSON_KEY_ADDITIONAL_ACTIONS);
for (const auto& [actionName, additionalAction] : additionalActions) // Define the expected order to ensure consistent hotkey ID assignment
const std::vector<winrt::hstring> expectedOrder = {
L"image-to-text",
L"paste-as-file",
L"transcode"
};
// Process actions in the predefined order
for (auto& actionKey : expectedOrder)
{ {
process_additional_action(actionName, additionalAction); if (additionalActions.HasKey(actionKey))
{
const auto actionValue = additionalActions.GetNamedValue(actionKey);
process_additional_action(actionKey, actionValue);
}
} }
} }
@@ -331,17 +346,14 @@ private:
for (const auto& customAction : customActions) for (const auto& customAction : customActions)
{ {
const auto object = customAction.GetObjectW(); const auto object = customAction.GetObjectW();
bool actionIsShown = object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false);
if (object.GetNamedBoolean(JSON_KEY_IS_SHOWN, false)) const CustomAction customActionData{
{ static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
const CustomAction customActionData parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
{ };
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
};
m_custom_actions.push_back(customActionData); m_custom_actions.push_back(customActionData);
}
} }
} }
} }

View File

@@ -73,6 +73,7 @@
<ClInclude Include="ClassFactory.h" /> <ClInclude Include="ClassFactory.h" />
<ClInclude Include="dllmain.h" /> <ClInclude Include="dllmain.h" />
<ClInclude Include="ExplorerCommand.h" /> <ClInclude Include="ExplorerCommand.h" />
<ClInclude Include="RuntimeRegistration.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />
<None Include="packages.config" /> <None Include="packages.config" />
<None Include="resource.base.h" /> <None Include="resource.base.h" />

View File

@@ -27,6 +27,9 @@
<ClInclude Include="dllmain.h"> <ClInclude Include="dllmain.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="RuntimeRegistration.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<ClCompile Include="dllmain.cpp"> <ClCompile Include="dllmain.cpp">

View File

@@ -12,6 +12,7 @@
#include "FileLocksmithLib/Constants.h" #include "FileLocksmithLib/Constants.h"
#include "FileLocksmithLib/Settings.h" #include "FileLocksmithLib/Settings.h"
#include "FileLocksmithLib/Trace.h" #include "FileLocksmithLib/Trace.h"
#include "RuntimeRegistration.h"
#include "dllmain.h" #include "dllmain.h"
#include "Generated Files/resource.h" #include "Generated Files/resource.h"
@@ -82,12 +83,17 @@ public:
{ {
std::wstring path = get_module_folderpath(globals::instance); std::wstring path = get_module_folderpath(globals::instance);
std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix"; std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix";
if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName)) if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName))
{ {
package::RegisterSparsePackage(path, packageUri); package::RegisterSparsePackage(path, packageUri);
} }
} }
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::EnsureRegistered();
#endif
}
m_enabled = true; m_enabled = true;
} }
@@ -95,6 +101,13 @@ public:
virtual void disable() override virtual void disable() override
{ {
Logger::info(L"File Locksmith disabled"); Logger::info(L"File Locksmith disabled");
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
FileLocksmithRuntimeRegistration::Unregister();
Logger::info(L"File Locksmith context menu unregistered (Win10)");
#endif
}
m_enabled = false; m_enabled = false;
} }

View File

@@ -0,0 +1,36 @@
// Header-only runtime registration for FileLocksmith context menu extension.
#pragma once
#include <common/utils/shell_ext_registration.h>
namespace globals { extern HMODULE instance; }
namespace FileLocksmithRuntimeRegistration
{
namespace
{
inline runtime_shell_ext::Spec BuildSpec()
{
runtime_shell_ext::Spec spec;
spec.clsid = L"{84D68575-E186-46AD-B0CB-BAEB45EE29C0}";
spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\FileLocksmith";
spec.sentinelValue = L"ContextMenuRegistered";
spec.dllFileCandidates = { L"PowerToys.FileLocksmithExt.dll" };
spec.contextMenuHandlerKeyPaths = {
L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt",
L"Software\\Classes\\Drive\\ShellEx\\ContextMenuHandlers\\FileLocksmithExt" };
spec.friendlyName = L"File Locksmith Shell Extension";
return spec;
}
}
inline bool EnsureRegistered()
{
return runtime_shell_ext::EnsureRegistered(BuildSpec(), globals::instance);
}
inline void Unregister()
{
runtime_shell_ext::Unregister(BuildSpec());
}
}

View File

@@ -27,6 +27,73 @@ struct InclusiveCrosshairs
void SwitchActivationMode(); void SwitchActivationMode();
void ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRuntimeObjects); void ApplySettings(InclusiveCrosshairsSettings& settings, bool applyToRuntimeObjects);
public:
// Allow external callers to request a position update (thread-safe enqueue)
static void RequestUpdatePosition()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr)
{
instance->UpdateCrosshairsPosition();
}
});
}
}
static void EnsureOn()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr && !instance->m_drawing)
{
instance->StartDrawing();
}
});
}
}
static void EnsureOff()
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([]() {
if (instance != nullptr && instance->m_drawing)
{
instance->StopDrawing();
}
});
}
}
static void SetExternalControl(bool enabled)
{
if (instance != nullptr)
{
auto dispatcherQueue = instance->m_dispatcherQueueController.DispatcherQueue();
dispatcherQueue.TryEnqueue([enabled]() {
if (instance != nullptr)
{
instance->m_externalControl = enabled;
if (enabled && instance->m_mouseHook)
{
UnhookWindowsHookEx(instance->m_mouseHook);
instance->m_mouseHook = NULL;
}
else if (!enabled && instance->m_drawing && !instance->m_mouseHook)
{
instance->m_mouseHook = SetWindowsHookEx(WH_MOUSE_LL, MouseHookProc, instance->m_hinstance, 0);
}
}
});
}
}
private: private:
enum class MouseButton enum class MouseButton
{ {
@@ -69,6 +136,7 @@ private:
bool m_drawing = false; bool m_drawing = false;
bool m_destroyed = false; bool m_destroyed = false;
bool m_hiddenCursor = false; bool m_hiddenCursor = false;
bool m_externalControl = false;
void SetAutoHideTimer() noexcept; void SetAutoHideTimer() noexcept;
// Configurable Settings // Configurable Settings
@@ -264,9 +332,12 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP
if (nCode >= 0) if (nCode >= 0)
{ {
MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam); MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
if (wParam == WM_MOUSEMOVE) if (instance && !instance->m_externalControl)
{ {
instance->UpdateCrosshairsPosition(); if (wParam == WM_MOUSEMOVE)
{
instance->UpdateCrosshairsPosition();
}
} }
} }
return CallNextHookEx(0, nCode, wParam, lParam); return CallNextHookEx(0, nCode, wParam, lParam);
@@ -527,6 +598,26 @@ bool InclusiveCrosshairsIsEnabled()
return (InclusiveCrosshairs::instance != nullptr); return (InclusiveCrosshairs::instance != nullptr);
} }
void InclusiveCrosshairsRequestUpdatePosition()
{
InclusiveCrosshairs::RequestUpdatePosition();
}
void InclusiveCrosshairsEnsureOn()
{
InclusiveCrosshairs::EnsureOn();
}
void InclusiveCrosshairsEnsureOff()
{
InclusiveCrosshairs::EnsureOff();
}
void InclusiveCrosshairsSetExternalControl(bool enabled)
{
InclusiveCrosshairs::SetExternalControl(enabled);
}
int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings) int InclusiveCrosshairsMain(HINSTANCE hInstance, InclusiveCrosshairsSettings& settings)
{ {
Logger::info("Starting a crosshairs instance."); Logger::info("Starting a crosshairs instance.");

View File

@@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable();
bool InclusiveCrosshairsIsEnabled(); bool InclusiveCrosshairsIsEnabled();
void InclusiveCrosshairsSwitch(); void InclusiveCrosshairsSwitch();
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings); void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings);
void InclusiveCrosshairsRequestUpdatePosition();
void InclusiveCrosshairsEnsureOn();
void InclusiveCrosshairsEnsureOff();
void InclusiveCrosshairsSetExternalControl(bool enabled);

View File

@@ -4,6 +4,15 @@
#include "trace.h" #include "trace.h"
#include "InclusiveCrosshairs.h" #include "InclusiveCrosshairs.h"
#include "common/utils/color.h" #include "common/utils/color.h"
#include <atomic>
#include <thread>
#include <chrono>
#include <memory>
extern void InclusiveCrosshairsRequestUpdatePosition();
extern void InclusiveCrosshairsEnsureOn();
extern void InclusiveCrosshairsEnsureOff();
extern void InclusiveCrosshairsSetExternalControl(bool enabled);
// Non-Localizable strings // Non-Localizable strings
namespace namespace
@@ -11,6 +20,7 @@ namespace
const wchar_t JSON_KEY_PROPERTIES[] = L"properties"; const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
const wchar_t JSON_KEY_VALUE[] = L"value"; const wchar_t JSON_KEY_VALUE[] = L"value";
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut"; const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
const wchar_t JSON_KEY_GLIDING_ACTIVATION_SHORTCUT[] = L"gliding_cursor_activation_shortcut";
const wchar_t JSON_KEY_CROSSHAIRS_COLOR[] = L"crosshairs_color"; const wchar_t JSON_KEY_CROSSHAIRS_COLOR[] = L"crosshairs_color";
const wchar_t JSON_KEY_CROSSHAIRS_OPACITY[] = L"crosshairs_opacity"; const wchar_t JSON_KEY_CROSSHAIRS_OPACITY[] = L"crosshairs_opacity";
const wchar_t JSON_KEY_CROSSHAIRS_RADIUS[] = L"crosshairs_radius"; const wchar_t JSON_KEY_CROSSHAIRS_RADIUS[] = L"crosshairs_radius";
@@ -21,13 +31,15 @@ namespace
const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled"; const wchar_t JSON_KEY_CROSSHAIRS_IS_FIXED_LENGTH_ENABLED[] = L"crosshairs_is_fixed_length_enabled";
const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length"; const wchar_t JSON_KEY_CROSSHAIRS_FIXED_LENGTH[] = L"crosshairs_fixed_length";
const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate"; const wchar_t JSON_KEY_AUTO_ACTIVATE[] = L"auto_activate";
const wchar_t JSON_KEY_GLIDE_TRAVEL_SPEED[] = L"gliding_travel_speed";
const wchar_t JSON_KEY_GLIDE_DELAY_SPEED[] = L"gliding_delay_speed";
} }
extern "C" IMAGE_DOS_HEADER __ImageBase; extern "C" IMAGE_DOS_HEADER __ImageBase;
HMODULE m_hModule; HMODULE m_hModule;
BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID /*lpReserved*/)
{ {
m_hModule = hModule; m_hModule = hModule;
switch (ul_reason_for_call) switch (ul_reason_for_call)
@@ -57,8 +69,46 @@ private:
// The PowerToy state. // The PowerToy state.
bool m_enabled = false; bool m_enabled = false;
// Hotkey to invoke the module // Additional hotkeys (legacy API) to support multiple shortcuts
HotkeyEx m_hotkey; Hotkey m_activationHotkey{}; // Crosshairs toggle
Hotkey m_glidingHotkey{}; // Gliding cursor state machine
// Shared state for worker threads (decoupled from this lifetime)
struct State
{
std::atomic<bool> stopX{ false };
std::atomic<bool> stopY{ false };
// positions and speeds
int currentXPos{ 0 };
int currentYPos{ 0 };
int currentXSpeed{ 0 }; // pixels per base window
int currentYSpeed{ 0 }; // pixels per base window
int xPosSnapshot{ 0 }; // xPos captured at end of horizontal scan
// Fractional accumulators to spread movement across 10ms ticks
double xFraction{ 0.0 };
double yFraction{ 0.0 };
// Speeds represent pixels per 200ms (min 5, max 60 enforced by UI/settings)
int fastHSpeed{ 30 }; // pixels per base window
int slowHSpeed{ 5 }; // pixels per base window
int fastVSpeed{ 30 }; // pixels per base window
int slowVSpeed{ 5 }; // pixels per base window
};
std::shared_ptr<State> m_state;
// Worker threads
std::thread m_xThread;
std::thread m_yThread;
// Gliding cursor state machine
std::atomic<int> m_glideState{ 0 }; // 0..4 like the AHK script
// Timer configuration: 10ms tick, speeds are defined per 200ms base window
static constexpr int kTimerTickMs = 10;
static constexpr int kBaseSpeedTickMs = 200; // mapping period for configured pixel counts
// Mouse Pointer Crosshairs specific settings // Mouse Pointer Crosshairs specific settings
InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings; InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings;
@@ -68,12 +118,17 @@ public:
MousePointerCrosshairs() MousePointerCrosshairs()
{ {
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName); LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName);
m_state = std::make_shared<State>();
init_settings(); init_settings();
}; };
// Destroy the powertoy and free memory // Destroy the powertoy and free memory
virtual void destroy() override virtual void destroy() override
{ {
StopXTimer();
StopYTimer();
// Release shared state so worker threads (if any) exit when weak_ptr lock fails
m_state.reset();
delete this; delete this;
} }
@@ -107,9 +162,7 @@ public:
// Signal from the Settings editor to call a custom action. // Signal from the Settings editor to call a custom action.
// This can be used to spawn more complex editors. // This can be used to spawn more complex editors.
virtual void call_custom_action(const wchar_t* action) override virtual void call_custom_action(const wchar_t* /*action*/) override {}
{
}
// Called by the runner to pass the updated settings values as a serialized JSON. // Called by the runner to pass the updated settings values as a serialized JSON.
virtual void set_config(const wchar_t* config) override virtual void set_config(const wchar_t* config) override
@@ -143,6 +196,9 @@ public:
{ {
m_enabled = false; m_enabled = false;
Trace::EnableMousePointerCrosshairs(false); Trace::EnableMousePointerCrosshairs(false);
StopXTimer();
StopYTimer();
m_glideState = 0;
InclusiveCrosshairsDisable(); InclusiveCrosshairsDisable();
} }
@@ -158,15 +214,249 @@ public:
return false; return false;
} }
virtual std::optional<HotkeyEx> GetHotkeyEx() override // Legacy multi-hotkey support (like CropAndLock)
virtual size_t get_hotkeys(Hotkey* buffer, size_t buffer_size) override
{ {
return m_hotkey; if (buffer && buffer_size >= 2)
{
buffer[0] = m_activationHotkey; // Crosshairs toggle
buffer[1] = m_glidingHotkey; // Gliding cursor toggle
}
return 2;
} }
virtual void OnHotkeyEx() override virtual bool on_hotkey(size_t hotkeyId) override
{ {
InclusiveCrosshairsSwitch(); if (!m_enabled)
{
return false;
}
if (hotkeyId == 0)
{
InclusiveCrosshairsSwitch();
return true;
}
if (hotkeyId == 1)
{
HandleGlidingHotkey();
return true;
}
return false;
} }
private:
static void LeftClick()
{
INPUT inputs[2]{};
inputs[0].type = INPUT_MOUSE;
inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
inputs[1].type = INPUT_MOUSE;
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
SendInput(2, inputs, sizeof(INPUT));
}
// Stateless helpers operating on shared State
static void PositionCursorX(const std::shared_ptr<State>& s)
{
int screenW = GetSystemMetrics(SM_CXVIRTUALSCREEN);
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
s->currentYPos = screenH / 2;
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
const double perTick = (static_cast<double>(s->currentXSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
s->xFraction += perTick;
int step = static_cast<int>(s->xFraction);
if (step > 0)
{
s->xFraction -= step;
s->currentXPos += step;
}
s->xPosSnapshot = s->currentXPos;
if (s->currentXPos >= screenW)
{
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
s->xPosSnapshot = 0;
s->xFraction = 0.0; // reset fractional remainder on wrap
}
SetCursorPos(s->currentXPos, s->currentYPos);
// Ensure overlay crosshairs follow immediately
InclusiveCrosshairsRequestUpdatePosition();
}
static void PositionCursorY(const std::shared_ptr<State>& s)
{
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
// Keep X at snapshot
// Use s->xPosSnapshot captured during X pass
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
const double perTick = (static_cast<double>(s->currentYSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
s->yFraction += perTick;
int step = static_cast<int>(s->yFraction);
if (step > 0)
{
s->yFraction -= step;
s->currentYPos += step;
}
if (s->currentYPos >= screenH)
{
s->currentYPos = 0;
s->currentYSpeed = s->fastVSpeed;
s->yFraction = 0.0; // reset fractional remainder on wrap
}
SetCursorPos(s->xPosSnapshot, s->currentYPos);
// Ensure overlay crosshairs follow immediately
InclusiveCrosshairsRequestUpdatePosition();
}
void StartXTimer()
{
auto s = m_state;
if (!s)
{
return;
}
s->stopX = false;
std::weak_ptr<State> wp = s;
m_xThread = std::thread([wp]() {
while (true)
{
auto sp = wp.lock();
if (!sp || sp->stopX.load())
{
break;
}
PositionCursorX(sp);
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
}
});
}
void StopXTimer()
{
auto s = m_state;
if (s)
{
s->stopX = true;
}
if (m_xThread.joinable())
{
m_xThread.join();
}
}
void StartYTimer()
{
auto s = m_state;
if (!s)
{
return;
}
s->stopY = false;
std::weak_ptr<State> wp = s;
m_yThread = std::thread([wp]() {
while (true)
{
auto sp = wp.lock();
if (!sp || sp->stopY.load())
{
break;
}
PositionCursorY(sp);
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
}
});
}
void StopYTimer()
{
auto s = m_state;
if (s)
{
s->stopY = true;
}
if (m_yThread.joinable())
{
m_yThread.join();
}
}
void HandleGlidingHotkey()
{
auto s = m_state;
if (!s)
{
return;
}
// Simulate the AHK state machine
int state = m_glideState.load();
switch (state)
{
case 0:
{
// Ensure crosshairs on (do not toggle off if already on)
InclusiveCrosshairsEnsureOn();
// Disable internal mouse hook so we control position updates explicitly
InclusiveCrosshairsSetExternalControl(true);
s->currentXPos = 0;
s->currentXSpeed = s->fastHSpeed;
s->xFraction = 0.0;
s->yFraction = 0.0;
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
SetCursorPos(0, y);
InclusiveCrosshairsRequestUpdatePosition();
m_glideState = 1;
StartXTimer();
break;
}
case 1:
{
// Slow horizontal
s->currentXSpeed = s->slowHSpeed;
m_glideState = 2;
break;
}
case 2:
{
// Stop horizontal, start vertical (fast)
StopXTimer();
s->currentYSpeed = s->fastVSpeed;
s->currentYPos = 0;
s->yFraction = 0.0;
SetCursorPos(s->xPosSnapshot, s->currentYPos);
InclusiveCrosshairsRequestUpdatePosition();
m_glideState = 3;
StartYTimer();
break;
}
case 3:
{
// Slow vertical
s->currentYSpeed = s->slowVSpeed;
m_glideState = 4;
break;
}
case 4:
default:
{
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
StopYTimer();
m_glideState = 0;
LeftClick();
InclusiveCrosshairsEnsureOff();
InclusiveCrosshairsSetExternalControl(false);
s->xFraction = 0.0;
s->yFraction = 0.0;
break;
}
}
}
// Load the settings file. // Load the settings file.
void init_settings() void init_settings()
{ {
@@ -192,37 +482,44 @@ public:
{ {
try try
{ {
// Parse HotKey // Parse primary activation HotKey (for centralized hook)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT); auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject); auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_hotkey = HotkeyEx();
if (hotkey.win_pressed())
{
m_hotkey.modifiersMask |= MOD_WIN;
}
if (hotkey.ctrl_pressed()) // Map to legacy Hotkey for multi-hotkey API
{ m_activationHotkey.win = hotkey.win_pressed();
m_hotkey.modifiersMask |= MOD_CONTROL; m_activationHotkey.ctrl = hotkey.ctrl_pressed();
} m_activationHotkey.shift = hotkey.shift_pressed();
m_activationHotkey.alt = hotkey.alt_pressed();
if (hotkey.shift_pressed()) m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
{
m_hotkey.modifiersMask |= MOD_SHIFT;
}
if (hotkey.alt_pressed())
{
m_hotkey.modifiersMask |= MOD_ALT;
}
m_hotkey.vkCode = hotkey.get_code();
} }
catch (...) catch (...)
{ {
Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut"); Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
} }
try try
{
// Parse Gliding Cursor HotKey
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDING_ACTIVATION_SHORTCUT);
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
m_glidingHotkey.win = hotkey.win_pressed();
m_glidingHotkey.ctrl = hotkey.ctrl_pressed();
m_glidingHotkey.shift = hotkey.shift_pressed();
m_glidingHotkey.alt = hotkey.alt_pressed();
m_glidingHotkey.key = static_cast<unsigned char>(hotkey.get_code());
}
catch (...)
{
// note that this is also defined in src\settings-ui\Settings.UI.Library\MousePointerCrosshairsProperties.cs, DefaultGlidingCursorActivationShortcut
// both need to be kept in sync!
Logger::warn("Failed to initialize Gliding Cursor activation shortcut. Using default Win+Alt+.");
m_glidingHotkey.win = true;
m_glidingHotkey.alt = true;
m_glidingHotkey.ctrl = false;
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
}
try
{ {
// Parse Opacity // Parse Opacity
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY); auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY);
@@ -272,7 +569,6 @@ public:
{ {
throw std::runtime_error("Invalid Radius value"); throw std::runtime_error("Invalid Radius value");
} }
} }
catch (...) catch (...)
{ {
@@ -291,7 +587,6 @@ public:
{ {
throw std::runtime_error("Invalid Thickness value"); throw std::runtime_error("Invalid Thickness value");
} }
} }
catch (...) catch (...)
{ {
@@ -320,7 +615,7 @@ public:
{ {
// Parse border size // Parse border size
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE); auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_BORDER_SIZE);
int value = static_cast <int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE)); int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 0) if (value >= 0)
{ {
inclusiveCrosshairsSettings.crosshairsBorderSize = value; inclusiveCrosshairsSettings.crosshairsBorderSize = value;
@@ -383,20 +678,86 @@ public:
{ {
Logger::warn("Failed to initialize auto activate from settings. Will use default value"); Logger::warn("Failed to initialize auto activate from settings. Will use default value");
} }
try
{
// Parse Travel speed (fast speed mapping)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_TRAVEL_SPEED);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 5 && value <= 60)
{
m_state->fastHSpeed = value;
m_state->fastVSpeed = value;
}
else if (value < 5)
{
m_state->fastHSpeed = 5; m_state->fastVSpeed = 5;
}
else
{
m_state->fastHSpeed = 60; m_state->fastVSpeed = 60;
}
}
catch (...)
{
Logger::warn("Failed to initialize gliding travel speed from settings. Using default 25.");
if (m_state)
{
m_state->fastHSpeed = 25;
m_state->fastVSpeed = 25;
}
}
try
{
// Parse Delay speed (slow speed mapping)
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_GLIDE_DELAY_SPEED);
int value = static_cast<int>(jsonPropertiesObject.GetNamedNumber(JSON_KEY_VALUE));
if (value >= 5 && value <= 60)
{
m_state->slowHSpeed = value;
m_state->slowVSpeed = value;
}
else if (value < 5)
{
m_state->slowHSpeed = 5; m_state->slowVSpeed = 5;
}
else
{
m_state->slowHSpeed = 60; m_state->slowVSpeed = 60;
}
}
catch (...)
{
Logger::warn("Failed to initialize gliding delay speed from settings. Using default 5.");
if (m_state)
{
m_state->slowHSpeed = 5;
m_state->slowVSpeed = 5;
}
}
} }
else else
{ {
Logger::info("Mouse Pointer Crosshairs settings are empty"); Logger::info("Mouse Pointer Crosshairs settings are empty");
} }
if (!m_hotkey.modifiersMask)
if (m_activationHotkey.key == 0)
{ {
Logger::info("Mouse Pointer Crosshairs is going to use default shortcut"); m_activationHotkey.win = true;
m_hotkey.modifiersMask = MOD_WIN | MOD_ALT; m_activationHotkey.alt = true;
m_hotkey.vkCode = 0x50; // P key m_activationHotkey.ctrl = false;
m_activationHotkey.shift = false;
m_activationHotkey.key = 'P';
}
if (m_glidingHotkey.key == 0)
{
m_glidingHotkey.win = true;
m_glidingHotkey.alt = true;
m_glidingHotkey.ctrl = false;
m_glidingHotkey.shift = false;
m_glidingHotkey.key = VK_OEM_PERIOD;
} }
m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings; m_inclusiveCrosshairsSettings = inclusiveCrosshairsSettings;
} }
}; };
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create() extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()

View File

@@ -556,6 +556,61 @@ public:
return m_enabled; return m_enabled;
} }
virtual size_t get_hotkeys(Hotkey* hotkeys, size_t buffer_size) override
{
constexpr size_t num_hotkeys = 4; // We have 4 hotkeys
if (hotkeys && buffer_size >= num_hotkeys)
{
PowerToysSettings::PowerToyValues values = PowerToysSettings::PowerToyValues::load_from_settings_file(MODULE_NAME);
// Cache the raw JSON object to avoid multiple parsing
json::JsonObject root_json = values.get_raw_json();
json::JsonObject properties_json = root_json.GetNamedObject(L"properties", json::JsonObject{});
size_t hotkey_index = 0;
// Helper lambda to extract hotkey from JSON properties
auto extract_hotkey = [&](const wchar_t* property_name) -> Hotkey {
if (properties_json.HasKey(property_name))
{
try
{
json::JsonObject hotkey_json = properties_json.GetNamedObject(property_name);
// Extract hotkey properties directly from JSON
bool win = hotkey_json.GetNamedBoolean(L"win", false);
bool ctrl = hotkey_json.GetNamedBoolean(L"ctrl", false);
bool alt = hotkey_json.GetNamedBoolean(L"alt", false);
bool shift = hotkey_json.GetNamedBoolean(L"shift", false);
unsigned char key = static_cast<unsigned char>(
hotkey_json.GetNamedNumber(L"code", 0));
return { win, ctrl, shift, alt, key };
}
catch (...)
{
// If parsing individual hotkey fails, use defaults
return { false, false, false, false, 0 };
}
}
else
{
// Property doesn't exist, use defaults
return { false, false, false, false, 0 };
}
};
// Extract all hotkeys using the optimized helper
hotkeys[hotkey_index++] = extract_hotkey(L"ToggleEasyMouseShortcut"); // [0] Toggle Easy Mouse
hotkeys[hotkey_index++] = extract_hotkey(L"LockMachineShortcut"); // [1] Lock Machine
hotkeys[hotkey_index++] = extract_hotkey(L"Switch2AllPCShortcut"); // [2] Switch to All PCs
hotkeys[hotkey_index++] = extract_hotkey(L"ReconnectShortcut"); // [3] Reconnect
}
return num_hotkeys;
}
void launch_add_firewall_process() void launch_add_firewall_process()
{ {
Logger::trace(L"Starting Process to add firewall rule"); Logger::trace(L"Starting Process to add firewall rule");

View File

@@ -123,6 +123,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
<ClInclude Include="settings.h" /> <ClInclude Include="settings.h" />
<ClInclude Include="trace.h" /> <ClInclude Include="trace.h" />
<ClInclude Include="new_utilities.h" /> <ClInclude Include="new_utilities.h" />
<ClInclude Include="RuntimeRegistration.h" />
<ClInclude Include="resource.base.h" /> <ClInclude Include="resource.base.h" />
<ClInclude Include="template_folder.h" /> <ClInclude Include="template_folder.h" />
<ClInclude Include="pch.h" /> <ClInclude Include="pch.h" />

View File

@@ -84,6 +84,9 @@
<ClInclude Include="helpers_variables.h"> <ClInclude Include="helpers_variables.h">
<Filter>Header Files</Filter> <Filter>Header Files</Filter>
</ClInclude> </ClInclude>
<ClInclude Include="RuntimeRegistration.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
<None Include="packages.config" /> <None Include="packages.config" />

View File

@@ -0,0 +1,36 @@
// Header-only runtime registration for New+ Win10 context menu.
#pragma once
#include <windows.h>
#include <string>
#include <common/utils/shell_ext_registration.h>
// Provided by dll_main.cpp
extern HMODULE module_instance_handle;
namespace NewPlusRuntimeRegistration
{
namespace {
inline runtime_shell_ext::Spec BuildSpec()
{
runtime_shell_ext::Spec spec;
spec.clsid = L"{FF90D477-E32A-4BE8-8CC5-A502A97F5401}";
spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\NewPlus";
spec.sentinelValue = L"ContextMenuRegisteredWin10";
spec.dllFileCandidates = { L"PowerToys.NewPlus.ShellExtension.win10.dll" };
spec.contextMenuHandlerKeyPaths = { L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\NewPlusShellExtensionWin10" };
spec.friendlyName = L"NewPlus Shell Extension Win10";
return spec;
}
}
inline bool EnsureRegisteredWin10()
{
return runtime_shell_ext::EnsureRegistered(BuildSpec(), module_instance_handle);
}
inline void Unregister()
{
runtime_shell_ext::Unregister(BuildSpec());
}
}

View File

@@ -16,6 +16,7 @@
#include "trace.h" #include "trace.h"
#include "new_utilities.h" #include "new_utilities.h"
#include "Generated Files/resource.h" #include "Generated Files/resource.h"
#include "RuntimeRegistration.h"
// Note: Settings are managed via Settings and UI Settings // Note: Settings are managed via Settings and UI Settings
class NewModule : public PowertoyModuleIface class NewModule : public PowertoyModuleIface
@@ -93,8 +94,16 @@ public:
// Log telemetry // Log telemetry
Trace::EventToggleOnOff(true); Trace::EventToggleOnOff(true);
if (package::IsWin11OrGreater())
newplus::utilities::register_msix_package(); {
newplus::utilities::register_msix_package();
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
#endif
}
powertoy_new_enabled = true; powertoy_new_enabled = true;
} }
@@ -141,6 +150,13 @@ private:
{ {
Trace::EventToggleOnOff(false); Trace::EventToggleOnOff(false);
} }
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
NewPlusRuntimeRegistration::Unregister();
Logger::info(L"New+ context menu unregistered (Win10)");
#endif
}
powertoy_new_enabled = false; powertoy_new_enabled = false;
} }

View File

@@ -189,7 +189,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
} }
else else
{ {
return new SeparatorContextItemViewModel() as IContextItemViewModel; return new SeparatorViewModel() as IContextItemViewModel;
} }
}) })
.ToList(); .ToList();
@@ -350,7 +350,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
} }
else else
{ {
return new SeparatorContextItemViewModel() as IContextItemViewModel; return new SeparatorViewModel() as IContextItemViewModel;
} }
}) })
.ToList(); .ToList();

View File

@@ -119,7 +119,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
} }
else else
{ {
return new SeparatorContextItemViewModel(); return new SeparatorViewModel();
} }
}) })
.ToList(); .ToList();
@@ -178,7 +178,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
} }
else else
{ {
return new SeparatorContextItemViewModel(); return new SeparatorViewModel();
} }
}) })
.ToList(); .ToList();

View File

@@ -0,0 +1,57 @@
// 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.
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class FilterItemViewModel : ExtensionObjectViewModel, IFilterItemViewModel
{
private ExtensionObject<IFilter> _model;
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public IconInfoViewModel Icon { get; set; } = new(null);
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized);
public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error);
public FilterItemViewModel(IFilter filter, WeakReference<IPageContext> context)
: base(context)
{
_model = new(filter);
}
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
var filter = _model.Unsafe;
if (filter == null)
{
return; // throw?
}
Id = filter.Id;
Name = filter.Name;
Icon = new(filter.Icon);
if (Icon is not null)
{
Icon.InitializeProperties();
}
UpdateProperty(nameof(Id));
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Icon));
}
}

View File

@@ -0,0 +1,81 @@
// 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.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class FiltersViewModel : ExtensionObjectViewModel
{
private readonly ExtensionObject<IFilters> _filtersModel = new(null);
[ObservableProperty]
public partial string CurrentFilterId { get; set; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShouldShowFilters))]
public partial IFilterItemViewModel[] Filters { get; set; } = [];
public bool ShouldShowFilters => Filters.Length > 0;
public FiltersViewModel(ExtensionObject<IFilters> filters, WeakReference<IPageContext> context)
: base(context)
{
_filtersModel = filters;
}
public override void InitializeProperties()
{
try
{
if (_filtersModel.Unsafe is not null)
{
var filters = _filtersModel.Unsafe.GetFilters();
Filters = filters.Select<IFilterItem, IFilterItemViewModel>(filter =>
{
var filterItem = filter as IFilter;
if (filterItem != null)
{
var filterVM = new FilterItemViewModel(filterItem!, PageContext);
filterVM.InitializeProperties();
return filterVM;
}
else
{
return new SeparatorViewModel();
}
}).ToArray();
CurrentFilterId = _filtersModel.Unsafe.CurrentFilterId;
return;
}
}
catch (Exception ex)
{
ShowException(ex, _filtersModel.Unsafe?.GetType().Name);
}
Filters = [];
CurrentFilterId = string.Empty;
}
public override void SafeCleanup()
{
base.SafeCleanup();
foreach (var filter in Filters)
{
if (filter is FilterItemViewModel filterVM)
{
filterVM.SafeCleanup();
}
}
Filters = [];
}
}

View File

@@ -2,12 +2,7 @@
// 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.Collections.Generic;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Microsoft.CmdPal.Core.ViewModels; namespace Microsoft.CmdPal.Core.ViewModels;

View File

@@ -0,0 +1,9 @@
// 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.
namespace Microsoft.CmdPal.Core.ViewModels;
public interface IFilterItemViewModel
{
}

View File

@@ -26,6 +26,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
[ObservableProperty] [ObservableProperty]
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = []; public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
public FiltersViewModel? Filters { get; set; }
private ObservableCollection<ListItemViewModel> Items { get; set; } = []; private ObservableCollection<ListItemViewModel> Items { get; set; } = [];
private readonly ExtensionObject<IListPage> _model; private readonly ExtensionObject<IListPage> _model;
@@ -86,7 +88,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
// TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching? // TODO: Does this need to hop to a _different_ thread, so that we don't block the extension while we're fetching?
private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems(); private void Model_ItemsChanged(object sender, IItemsChangedEventArgs args) => FetchItems();
protected override void OnFilterUpdated(string filter) protected override void OnSearchTextBoxUpdated(string searchTextBox)
{ {
//// TODO: Just temp testing, need to think about where we want to filter, as AdvancedCollectionView in View could be done, but then grouping need CollectionViewSource, maybe we do grouping in view //// TODO: Just temp testing, need to think about where we want to filter, as AdvancedCollectionView in View could be done, but then grouping need CollectionViewSource, maybe we do grouping in view
//// and manage filtering below, but we should be smarter about this and understand caching and other requirements... //// and manage filtering below, but we should be smarter about this and understand caching and other requirements...
@@ -104,7 +106,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
{ {
if (_model.Unsafe is IDynamicListPage dynamic) if (_model.Unsafe is IDynamicListPage dynamic)
{ {
dynamic.SearchText = filter; dynamic.SearchText = searchTextBox;
} }
} }
catch (Exception ex) catch (Exception ex)
@@ -127,6 +129,26 @@ public partial class ListViewModel : PageViewModel, IDisposable
} }
} }
public void UpdateCurrentFilter(string currentFilterId)
{
// We're getting called on the UI thread.
// Hop off to a BG thread to update the extension.
_ = Task.Run(() =>
{
try
{
if (_model.Unsafe is IListPage listPage)
{
listPage.Filters?.CurrentFilterId = currentFilterId;
}
}
catch (Exception ex)
{
ShowException(ex, _model?.Unsafe?.Name);
}
});
}
//// Run on background thread, from InitializeAsync or Model_ItemsChanged //// Run on background thread, from InitializeAsync or Model_ItemsChanged
private void FetchItems() private void FetchItems()
{ {
@@ -305,7 +327,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
/// Apply our current filter text to the list of items, and update /// Apply our current filter text to the list of items, and update
/// FilteredItems to match the results. /// FilteredItems to match the results.
/// </summary> /// </summary>
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter)); private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox));
/// <summary> /// <summary>
/// Helper to generate a weighting for a given list item, based on title, /// Helper to generate a weighting for a given list item, based on title,
@@ -507,6 +529,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(model.EmptyContent), PageContext); EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties(); EmptyContent.SlowInitializeProperties();
Filters = new(new(model.Filters), PageContext);
Filters.InitializeProperties();
UpdateProperty(nameof(Filters));
FetchItems(); FetchItems();
model.ItemsChanged += Model_ItemsChanged; model.ItemsChanged += Model_ItemsChanged;
} }
@@ -578,6 +604,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
EmptyContent = new(new(model.EmptyContent), PageContext); EmptyContent = new(new(model.EmptyContent), PageContext);
EmptyContent.SlowInitializeProperties(); EmptyContent.SlowInitializeProperties();
break; break;
case nameof(Filters):
Filters = new(new(model.Filters), PageContext);
Filters.InitializeProperties();
break;
case nameof(IsLoading): case nameof(IsLoading):
UpdateEmptyContent(); UpdateEmptyContent();
break; break;
@@ -641,6 +671,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
FilteredItems.Clear(); FilteredItems.Clear();
} }
Filters?.SafeCleanup();
var model = _model.Unsafe; var model = _model.Unsafe;
if (model is not null) if (model is not null)
{ {

View File

@@ -32,7 +32,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
// This is set from the SearchBar // This is set from the SearchBar
[ObservableProperty] [ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShowSuggestion))] [NotifyPropertyChangedFor(nameof(ShowSuggestion))]
public partial string Filter { get; set; } = string.Empty; public partial string SearchTextBox { get; set; } = string.Empty;
[ObservableProperty] [ObservableProperty]
public virtual partial string PlaceholderText { get; private set; } = "Type here to search..."; public virtual partial string PlaceholderText { get; private set; } = "Type here to search...";
@@ -41,7 +41,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
[NotifyPropertyChangedFor(nameof(ShowSuggestion))] [NotifyPropertyChangedFor(nameof(ShowSuggestion))]
public virtual partial string TextToSuggest { get; protected set; } = string.Empty; public virtual partial string TextToSuggest { get; protected set; } = string.Empty;
public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != Filter; public bool ShowSuggestion => !string.IsNullOrEmpty(TextToSuggest) && TextToSuggest != SearchTextBox;
[ObservableProperty] [ObservableProperty]
public partial AppExtensionHost ExtensionHost { get; private set; } public partial AppExtensionHost ExtensionHost { get; private set; }
@@ -167,9 +167,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
} }
} }
partial void OnFilterChanged(string oldValue, string newValue) => OnFilterUpdated(newValue); partial void OnSearchTextBoxChanged(string oldValue, string newValue) => OnSearchTextBoxUpdated(newValue);
protected virtual void OnFilterUpdated(string filter) protected virtual void OnSearchTextBoxUpdated(string searchTextBox)
{ {
// The base page has no notion of data, so we do nothing here... // The base page has no notion of data, so we do nothing here...
// subclasses should override. // subclasses should override.

View File

@@ -9,6 +9,10 @@ using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels; namespace Microsoft.CmdPal.Core.ViewModels;
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)] [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem public partial class SeparatorViewModel() :
IContextItemViewModel,
IFilterItemViewModel,
ISeparatorContextItem,
ISeparatorFilterItem
{ {
} }

View File

@@ -97,14 +97,27 @@ public partial class AliasManager : ObservableObject
} }
} }
// Look for the old alias, and remove it
List<CommandAlias> toRemove = []; List<CommandAlias> toRemove = [];
foreach (var kv in _aliases) foreach (var kv in _aliases)
{ {
// Look for the old aliases for the command, and remove it
if (kv.Value.CommandId == commandId) if (kv.Value.CommandId == commandId)
{ {
toRemove.Add(kv.Value); toRemove.Add(kv.Value);
} }
// Look for the alias belonging to another command, and remove it
if (newAlias is not null && kv.Value.Alias == newAlias.Alias)
{
toRemove.Add(kv.Value);
// Remove alias from other TopLevelViewModels it may be assigned to
var topLevelCommand = _topLevelCommandManager.LookupCommand(kv.Value.CommandId);
if (topLevelCommand is not null)
{
topLevelCommand.AliasText = string.Empty;
}
}
} }
foreach (var alias in toRemove) foreach (var alias in toRemove)

View File

@@ -2,6 +2,7 @@
// 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.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using AdaptiveCards.ObjectModel.WinUI3; using AdaptiveCards.ObjectModel.WinUI3;
using AdaptiveCards.Templating; using AdaptiveCards.Templating;
@@ -96,109 +97,40 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
UpdateProperty(nameof(Card)); UpdateProperty(nameof(Card));
} }
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveOpenUrlAction))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveSubmitAction))]
[DynamicDependency(DynamicallyAccessedMemberTypes.All, typeof(AdaptiveExecuteAction))]
public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs) public void HandleSubmit(IAdaptiveActionElement action, JsonObject inputs)
{ {
// BODGY circa GH #40979 if (action is AdaptiveOpenUrlAction openUrlAction)
// Usually, you're supposed to try to cast the action to a specific
// type, and use those objects to get the data you need.
// However, there's something weird with AdaptiveCards and the way it
// works when we consume it when built in Release, with AOT (and
// trimming) enabled. Any sort of `action.As<IAdaptiveSubmitAction>()`
// or similar will throw a System.InvalidCastException.
//
// Instead we have this horror show.
//
// The `action.ToJson()` blob ACTUALLY CONTAINS THE `type` field, which
// we can use to determine what kind of action it is. Then we can parse
// the JSON manually based on the type.
var actionJson = action.ToJson();
if (actionJson.TryGetValue("type", out var actionTypeValue))
{ {
var actionTypeString = actionTypeValue.GetString(); WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
Logger.LogTrace($"atString={actionTypeString}"); return;
var actionType = actionTypeString switch
{
"Action.Submit" => ActionType.Submit,
"Action.Execute" => ActionType.Execute,
"Action.OpenUrl" => ActionType.OpenUrl,
_ => ActionType.Unsupported,
};
Logger.LogDebug($"{actionTypeString}->{actionType}");
switch (actionType)
{
case ActionType.OpenUrl:
{
HandleOpenUrlAction(action, actionJson);
}
break;
case ActionType.Submit:
case ActionType.Execute:
{
HandleSubmitAction(action, actionJson, inputs);
}
break;
default:
Logger.LogError($"{actionType} was an unexpected action `type`");
break;
}
}
else
{
Logger.LogError($"actionJson.TryGetValue(type) failed");
}
}
private void HandleOpenUrlAction(IAdaptiveActionElement action, JsonObject actionJson)
{
if (actionJson.TryGetValue("url", out var actionUrlValue))
{
var actionUrl = actionUrlValue.GetString() ?? string.Empty;
if (Uri.TryCreate(actionUrl, default(UriCreationOptions), out var uri))
{
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(uri));
}
else
{
Logger.LogError($"Failed to produce URI for {actionUrlValue}");
}
}
}
private void HandleSubmitAction(
IAdaptiveActionElement action,
JsonObject actionJson,
JsonObject inputs)
{
var dataString = string.Empty;
if (actionJson.TryGetValue("data", out var actionDataValue))
{
dataString = actionDataValue.Stringify() ?? string.Empty;
} }
var inputString = inputs.Stringify(); if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
_ = Task.Run(() =>
{ {
try // Get the data and inputs
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
var inputString = inputs.Stringify();
_ = Task.Run(() =>
{ {
var model = _formModel.Unsafe!; try
if (model != null)
{ {
var result = model.SubmitForm(inputString, dataString); var model = _formModel.Unsafe!;
Logger.LogDebug($"SubmitForm() returned {result}"); if (model != null)
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result))); {
var result = model.SubmitForm(inputString, dataString);
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
}
} }
} catch (Exception ex)
catch (Exception ex) {
{ ShowException(ex);
ShowException(ex); }
} });
}); }
} }
private static readonly string ErrorCardJson = """ private static readonly string ErrorCardJson = """

View File

@@ -110,6 +110,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
get => Alias?.Alias ?? string.Empty; get => Alias?.Alias ?? string.Empty;
set set
{ {
var previousAlias = Alias?.Alias ?? string.Empty;
if (string.IsNullOrEmpty(value)) if (string.IsNullOrEmpty(value))
{ {
Alias = null; Alias = null;
@@ -126,9 +128,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
} }
} }
HandleChangeAlias(); // Only call HandleChangeAlias if there was an actual change.
OnPropertyChanged(nameof(AliasText)); if (previousAlias != Alias?.Alias)
OnPropertyChanged(nameof(IsDirectAlias)); {
HandleChangeAlias();
OnPropertyChanged(nameof(AliasText));
OnPropertyChanged(nameof(IsDirectAlias));
}
} }
} }

View File

@@ -41,14 +41,18 @@
<Style.Setters> <Style.Setters>
<Setter Property="Margin" Value="0" /> <Setter Property="Margin" Value="0" />
<Setter Property="Padding" Value="0" /> <Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
</Style.Setters> </Style.Setters>
</Style> </Style>
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
<Flyout <Flyout
x:Name="ContextMenuFlyout" x:Name="ContextMenuFlyout"
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}" FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
Opened="ContextMenuFlyout_Opened" Opened="ContextMenuFlyout_Opened"
ShouldConstrainToRootBounds="False"> ShouldConstrainToRootBounds="False"
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
<cpcontrols:ContextMenu x:Name="ContextControl" /> <cpcontrols:ContextMenu x:Name="ContextControl" />
</Flyout> </Flyout>
@@ -161,6 +165,7 @@
x:Name="PrimaryButton" x:Name="PrimaryButton"
Padding="6,4,4,4" Padding="6,4,4,4"
x:Load="{x:Bind IsLoaded, Mode=OneWay}" x:Load="{x:Bind IsLoaded, Mode=OneWay}"
AutomationProperties.AutomationId="PrimaryCommandButton"
AutomationProperties.Name="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}" AutomationProperties.Name="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}"
Background="Transparent" Background="Transparent"
Click="PrimaryButton_Clicked" Click="PrimaryButton_Clicked"
@@ -180,6 +185,7 @@
x:Name="SecondaryButton" x:Name="SecondaryButton"
Padding="6,4,4,4" Padding="6,4,4,4"
x:Load="{x:Bind IsLoaded, Mode=OneWay}" x:Load="{x:Bind IsLoaded, Mode=OneWay}"
AutomationProperties.AutomationId="SecondaryCommandButton"
AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}" AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}"
Click="SecondaryButton_Clicked" Click="SecondaryButton_Clicked"
Style="{StaticResource SubtleButtonStyle}" Style="{StaticResource SubtleButtonStyle}"
@@ -203,6 +209,7 @@
x:Name="MoreCommandsButton" x:Name="MoreCommandsButton"
x:Uid="MoreCommandsButton" x:Uid="MoreCommandsButton"
Padding="6,4,4,4" Padding="6,4,4,4"
AutomationProperties.AutomationId="MoreContextMenuButton"
Click="MoreCommandsButton_Clicked" Click="MoreCommandsButton_Clicked"
Style="{StaticResource SubtleButtonStyle}" Style="{StaticResource SubtleButtonStyle}"
ToolTipService.ToolTip="Ctrl+K" ToolTipService.ToolTip="Ctrl+K"

View File

@@ -108,7 +108,7 @@
</DataTemplate> </DataTemplate>
<!-- Template for context item separators --> <!-- Template for context item separators -->
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorContextItemViewModel"> <DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
<Rectangle <Rectangle
Height="1" Height="1"
Margin="-16,-12,-12,-12" Margin="-16,-12,-12,-12"

View File

@@ -270,7 +270,7 @@ public sealed partial class ContextMenu : UserControl,
private bool IsSeparator(object item) private bool IsSeparator(object item)
{ {
return item is SeparatorContextItemViewModel; return item is SeparatorViewModel;
} }
private void UpdateUiForStackChange() private void UpdateUiForStackChange()

View File

@@ -0,0 +1,89 @@
<?xml version="1.0" encoding="utf-8" ?>
<UserControl
x:Class="Microsoft.CmdPal.UI.Controls.FiltersDropDown"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:cmdpalUI="using:Microsoft.CmdPal.UI"
xmlns:converters="using:CommunityToolkit.WinUI.Converters"
xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels"
xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:help="using:Microsoft.CmdPal.UI.Helpers"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Background="Transparent"
mc:Ignorable="d">
<UserControl.Resources>
<ResourceDictionary>
<converters:BoolToVisibilityConverter x:Key="BoolToVisibilityConverter" />
<cmdpalUI:FilterTemplateSelector
x:Key="FilterTemplateSelector"
Default="{StaticResource FilterItemViewModelTemplate}"
Separator="{StaticResource SeparatorViewModelTemplate}" />
<Style
x:Name="ComboBoxStyle"
BasedOn="{StaticResource DefaultComboBoxStyle}"
TargetType="ComboBox">
<Style.Setters>
<Setter Property="Visibility" Value="Collapsed" />
<Setter Property="Margin" Value="0,0,12,0" />
<Setter Property="Padding" Value="16,4" />
</Style.Setters>
</Style>
<!-- Template for the filter items -->
<DataTemplate x:Key="FilterItemViewModelTemplate" x:DataType="coreViewModels:FilterItemViewModel">
<Grid AutomationProperties.Name="{x:Bind Name, Mode=OneWay}">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="32" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<cpcontrols:IconBox
Width="16"
Margin="4,0"
HorizontalAlignment="Left"
VerticalAlignment="Center"
SourceKey="{x:Bind Icon}"
SourceRequested="{x:Bind help:IconCacheProvider.SourceRequested}" />
<TextBlock
Grid.Column="1"
VerticalAlignment="Center"
Text="{x:Bind Name}" />
</Grid>
</DataTemplate>
<!-- Template for separators -->
<DataTemplate x:Key="SeparatorViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
<Rectangle
Height="1"
Margin="-16,-12,-12,-12"
Fill="{ThemeResource MenuFlyoutSeparatorThemeBrush}" />
</DataTemplate>
</ResourceDictionary>
</UserControl.Resources>
<ComboBox
Name="FiltersComboBox"
x:Uid="FiltersComboBox"
VerticalAlignment="Center"
ItemTemplateSelector="{StaticResource FilterTemplateSelector}"
ItemsSource="{x:Bind ViewModel.Filters, Mode=OneWay}"
PlaceholderText="Filters"
PreviewKeyDown="FiltersComboBox_PreviewKeyDown"
SelectionChanged="FiltersComboBox_SelectionChanged"
Style="{StaticResource ComboBoxStyle}"
Visibility="{x:Bind ViewModel.ShouldShowFilters, Mode=OneWay, Converter={StaticResource BoolToVisibilityConverter}}">
<ComboBox.ItemContainerStyle>
<Style BasedOn="{StaticResource DefaultComboBoxItemStyle}" TargetType="ComboBoxItem">
<Setter Property="MinHeight" Value="0" />
<Setter Property="Padding" Value="12,8" />
</Style>
</ComboBox.ItemContainerStyle>
<ComboBox.ItemContainerTransitions>
<TransitionCollection />
</ComboBox.ItemContainerTransitions>
</ComboBox>
</UserControl>

View File

@@ -0,0 +1,189 @@
// 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.
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.CmdPal.UI.Views;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
using Windows.System;
namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class FiltersDropDown : UserControl,
ICurrentPageAware
{
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
set => SetValue(CurrentPageViewModelProperty, value);
}
public static readonly DependencyProperty CurrentPageViewModelProperty =
DependencyProperty.Register(nameof(CurrentPageViewModel), typeof(PageViewModel), typeof(FiltersDropDown), new PropertyMetadata(null, OnCurrentPageViewModelChanged));
private static void OnCurrentPageViewModelChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var @this = (FiltersDropDown)d;
if (@this != null
&& e.OldValue is PageViewModel old)
{
old.PropertyChanged -= @this.Page_PropertyChanged;
}
// If this new page does not implement ListViewModel or if
// it doesn't contain Filters, we need to clear any filters
// that may have been set.
if (@this != null)
{
if (e.NewValue is ListViewModel listViewModel)
{
@this.ViewModel = listViewModel.Filters;
}
else
{
@this.ViewModel = null;
}
}
if (@this != null
&& e.NewValue is PageViewModel page)
{
page.PropertyChanged += @this.Page_PropertyChanged;
}
}
public FiltersViewModel? ViewModel
{
get => (FiltersViewModel?)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
public static readonly DependencyProperty ViewModelProperty =
DependencyProperty.Register(nameof(ViewModel), typeof(FiltersViewModel), typeof(FiltersDropDown), new PropertyMetadata(null));
public FiltersDropDown()
{
this.InitializeComponent();
}
// Used to handle the case when a ListPage's `Filters` may have changed
private void Page_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
var property = e.PropertyName;
if (CurrentPageViewModel is ListViewModel list)
{
if (property == nameof(ListViewModel.Filters))
{
ViewModel = list.Filters;
}
}
}
private void FiltersComboBox_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (CurrentPageViewModel is ListViewModel listViewModel &&
FiltersComboBox.SelectedItem is FilterItemViewModel filterItem)
{
listViewModel.UpdateCurrentFilter(filterItem.Id);
}
}
private void FiltersComboBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
{
if (e.Key == VirtualKey.Up)
{
NavigateUp();
e.Handled = true;
}
else if (e.Key == VirtualKey.Down)
{
NavigateDown();
e.Handled = true;
}
}
private void NavigateUp()
{
var newIndex = FiltersComboBox.SelectedIndex;
if (FiltersComboBox.SelectedIndex > 0)
{
newIndex--;
while (
newIndex >= 0 &&
IsSeparator(FiltersComboBox.Items[newIndex]) &&
newIndex != FiltersComboBox.SelectedIndex)
{
newIndex--;
}
if (newIndex < 0)
{
newIndex = FiltersComboBox.Items.Count - 1;
while (
newIndex >= 0 &&
IsSeparator(FiltersComboBox.Items[newIndex]) &&
newIndex != FiltersComboBox.SelectedIndex)
{
newIndex--;
}
}
}
else
{
newIndex = FiltersComboBox.Items.Count - 1;
}
FiltersComboBox.SelectedIndex = newIndex;
}
private void NavigateDown()
{
var newIndex = FiltersComboBox.SelectedIndex;
if (FiltersComboBox.SelectedIndex == FiltersComboBox.Items.Count - 1)
{
newIndex = 0;
}
else
{
newIndex++;
while (
newIndex < FiltersComboBox.Items.Count &&
IsSeparator(FiltersComboBox.Items[newIndex]) &&
newIndex != FiltersComboBox.SelectedIndex)
{
newIndex++;
}
if (newIndex >= FiltersComboBox.Items.Count)
{
newIndex = 0;
while (
newIndex < FiltersComboBox.Items.Count &&
IsSeparator(FiltersComboBox.Items[newIndex]) &&
newIndex != FiltersComboBox.SelectedIndex)
{
newIndex++;
}
}
}
FiltersComboBox.SelectedIndex = newIndex;
}
private bool IsSeparator(object item)
{
return item is SeparatorViewModel;
}
}

View File

@@ -22,6 +22,7 @@
MinHeight="32" MinHeight="32"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch" VerticalContentAlignment="Stretch"
AutomationProperties.AutomationId="MainSearchBox"
KeyDown="FilterBox_KeyDown" KeyDown="FilterBox_KeyDown"
PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}" PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}"
PreviewKeyDown="FilterBox_PreviewKeyDown" PreviewKeyDown="FilterBox_PreviewKeyDown"

View File

@@ -62,7 +62,7 @@ public sealed partial class SearchBar : UserControl,
{ {
// TODO: In some cases we probably want commands to clear a filter // TODO: In some cases we probably want commands to clear a filter
// somewhere in the process, so we need to figure out when that is. // somewhere in the process, so we need to figure out when that is.
@this.FilterBox.Text = page.Filter; @this.FilterBox.Text = page.SearchTextBox;
@this.FilterBox.Select(@this.FilterBox.Text.Length, 0); @this.FilterBox.Select(@this.FilterBox.Text.Length, 0);
page.PropertyChanged += @this.Page_PropertyChanged; page.PropertyChanged += @this.Page_PropertyChanged;
@@ -87,7 +87,7 @@ public sealed partial class SearchBar : UserControl,
if (CurrentPageViewModel is not null) if (CurrentPageViewModel is not null)
{ {
CurrentPageViewModel.Filter = string.Empty; CurrentPageViewModel.SearchTextBox = string.Empty;
} }
})); }));
} }
@@ -145,7 +145,7 @@ public sealed partial class SearchBar : UserControl,
// hack TODO GH #245 // hack TODO GH #245
if (CurrentPageViewModel is not null) if (CurrentPageViewModel is not null)
{ {
CurrentPageViewModel.Filter = FilterBox.Text; CurrentPageViewModel.SearchTextBox = FilterBox.Text;
} }
} }
@@ -156,7 +156,7 @@ public sealed partial class SearchBar : UserControl,
// hack TODO GH #245 // hack TODO GH #245
if (CurrentPageViewModel is not null) if (CurrentPageViewModel is not null)
{ {
CurrentPageViewModel.Filter = FilterBox.Text; CurrentPageViewModel.SearchTextBox = FilterBox.Text;
} }
} }
} }
@@ -320,7 +320,7 @@ public sealed partial class SearchBar : UserControl,
// Actually plumb Filtering to the view model // Actually plumb Filtering to the view model
if (CurrentPageViewModel is not null) if (CurrentPageViewModel is not null)
{ {
CurrentPageViewModel.Filter = FilterBox.Text; CurrentPageViewModel.SearchTextBox = FilterBox.Text;
} }
} }

View File

@@ -28,7 +28,7 @@ internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
{ {
li.IsEnabled = true; li.IsEnabled = true;
if (item is SeparatorContextItemViewModel) if (item is SeparatorViewModel)
{ {
li.IsEnabled = false; li.IsEnabled = false;
li.AllowFocusWhenDisabled = false; li.AllowFocusWhenDisabled = false;

View File

@@ -0,0 +1,36 @@
// 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.
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
internal sealed partial class FilterTemplateSelector : DataTemplateSelector
{
public DataTemplate? Default { get; set; }
public DataTemplate? Separator { get; set; }
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{
DataTemplate? dataTemplate = Default;
if (dependencyObject is ComboBoxItem comboBoxItem)
{
comboBoxItem.IsEnabled = true;
if (item is SeparatorViewModel)
{
comboBoxItem.IsEnabled = false;
comboBoxItem.AllowFocusWhenDisabled = false;
comboBoxItem.AllowFocusOnInteraction = false;
dataTemplate = Separator;
}
}
return dataTemplate;
}
}

View File

@@ -92,7 +92,7 @@ internal sealed partial class TrayIconService
{ {
_popupMenu = PInvoke.CreatePopupMenu_SafeHandle(); _popupMenu = PInvoke.CreatePopupMenu_SafeHandle();
PInvoke.InsertMenu(_popupMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 1, RS_.GetString("TrayMenu_Settings")); PInvoke.InsertMenu(_popupMenu, 0, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 1, RS_.GetString("TrayMenu_Settings"));
PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, RS_.GetString("TrayMenu_Exit")); PInvoke.InsertMenu(_popupMenu, 1, MENU_ITEM_FLAGS.MF_BYPOSITION | MENU_ITEM_FLAGS.MF_STRING, PInvoke.WM_USER + 2, RS_.GetString("TrayMenu_Close"));
} }
} }
else else

View File

@@ -176,6 +176,7 @@
<Grid.ColumnDefinitions> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" /> <ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" /> <ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<!-- Back button --> <!-- Back button -->
@@ -320,6 +321,18 @@
</TransitionCollection> </TransitionCollection>
</Grid.Transitions> </Grid.Transitions>
</Grid> </Grid>
<!-- Filter: wrapped in a grid to enable RepositionThemeTransitions -->
<Grid Grid.Column="2" HorizontalAlignment="Right">
<cpcontrols:FiltersDropDown
x:Name="FiltersDropDown"
HorizontalAlignment="Right"
CurrentPageViewModel="{x:Bind ViewModel.CurrentPage, Mode=OneWay}" />
<Grid.Transitions>
<TransitionCollection>
<RepositionThemeTransition />
</TransitionCollection>
</Grid.Transitions>
</Grid>
</Grid> </Grid>
<ProgressBar <ProgressBar

View File

@@ -419,8 +419,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="TrayMenu_Settings" xml:space="preserve"> <data name="TrayMenu_Settings" xml:space="preserve">
<value>Settings</value> <value>Settings</value>
</data> </data>
<data name="TrayMenu_Exit" xml:space="preserve"> <data name="TrayMenu_Close" xml:space="preserve">
<value>Exit</value> <value>Close</value>
<comment>Close as a verb, as in Close the application</comment>
</data> </data>
<data name="Settings_ExtensionPage_Alias_ToggleSwitch.OnContent" xml:space="preserve"> <data name="Settings_ExtensionPage_Alias_ToggleSwitch.OnContent" xml:space="preserve">
<value>Direct</value> <value>Direct</value>

View File

@@ -0,0 +1,119 @@
// 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.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class AllAppsCommandProviderTests : AppsTestBase
{
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var provider = new AllAppsCommandProvider();
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new AllAppsCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new AllAppsCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void LookupAppWithEmptyNameReturnsNotNull()
{
// Setup
var mockApp = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
MockCache.AddWin32Program(mockApp);
var page = new AllAppsPage(MockCache);
var provider = new AllAppsCommandProvider(page);
// Act
var result = provider.LookupApp(string.Empty);
// Assert
Assert.IsNotNull(result);
}
[TestMethod]
public async Task ProviderWithMockData_LookupApp_ReturnsCorrectApp()
{
// Arrange
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
MockCache.AddWin32Program(testApp);
var provider = new AllAppsCommandProvider(Page);
// Wait for initialization to complete
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("TestApp");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual("TestApp", result.Title);
}
[TestMethod]
public async Task ProviderWithMockData_LookupApp_ReturnsNullForNonExistentApp()
{
// Arrange
var testApp = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
MockCache.AddWin32Program(testApp);
var provider = new AllAppsCommandProvider(Page);
// Wait for initialization to complete
await WaitForPageInitializationAsync();
// Act
var result = provider.LookupApp("NonExistentApp");
// Assert
Assert.IsNull(result);
}
[TestMethod]
public void ProviderWithMockData_TopLevelCommands_IncludesListItem()
{
// Arrange
var provider = new AllAppsCommandProvider(Page);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length >= 1); // At least the list item should be present
}
}

View File

@@ -0,0 +1,97 @@
// 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.
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class AllAppsPageTests : AppsTestBase
{
[TestMethod]
public void AllAppsPage_Constructor_ThrowsOnNullAppCache()
{
// Act & Assert
Assert.ThrowsException<ArgumentNullException>(() => new AllAppsPage(null!));
}
[TestMethod]
public void AllAppsPage_WithMockCache_InitializesSuccessfully()
{
// Arrange
var mockCache = new MockAppCache();
// Act
var page = new AllAppsPage(mockCache);
// Assert
Assert.IsNotNull(page);
Assert.IsNotNull(page.Name);
Assert.IsNotNull(page.Icon);
}
[TestMethod]
public async Task AllAppsPage_GetItems_ReturnsEmptyWithEmptyCache()
{
// Act - Wait for initialization to complete
await WaitForPageInitializationAsync();
var items = Page.GetItems();
// Assert
Assert.IsNotNull(items);
Assert.AreEqual(0, items.Length);
}
[TestMethod]
public async Task AllAppsPage_GetItems_ReturnsAppsFromCacheAsync()
{
// Arrange
var mockCache = new MockAppCache();
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
mockCache.AddWin32Program(win32App);
mockCache.AddUWPApplication(uwpApp);
var page = new AllAppsPage(mockCache);
// Wait a bit for initialization to complete
await Task.Delay(100);
// Act
var items = page.GetItems();
// Assert
Assert.IsNotNull(items);
Assert.AreEqual(2, items.Length);
// we need to loop the items to ensure we got the correct ones
Assert.IsTrue(items.Any(i => i.Title == "Notepad"));
Assert.IsTrue(items.Any(i => i.Title == "Calculator"));
}
[TestMethod]
public async Task AllAppsPage_GetPinnedApps_ReturnsEmptyWhenNoAppsArePinned()
{
// Arrange
var mockCache = new MockAppCache();
var app = TestDataHelper.CreateTestWin32Program("TestApp", "C:\\TestApp.exe");
mockCache.AddWin32Program(app);
var page = new AllAppsPage(mockCache);
// Wait a bit for initialization to complete
await Task.Delay(100);
// Act
var pinnedApps = page.GetPinnedApps();
// Assert
Assert.IsNotNull(pinnedApps);
Assert.AreEqual(0, pinnedApps.Length);
}
}

View File

@@ -0,0 +1,67 @@
// 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.
using System.Threading.Tasks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Base class for Apps unit tests that provides common setup and teardown functionality.
/// </summary>
public abstract class AppsTestBase
{
/// <summary>
/// Gets the mock application cache used in tests.
/// </summary>
protected MockAppCache MockCache { get; private set; } = null!;
/// <summary>
/// Gets the AllAppsPage instance used in tests.
/// </summary>
protected AllAppsPage Page { get; private set; } = null!;
/// <summary>
/// Sets up the test environment before each test method.
/// </summary>
/// <returns>A task representing the asynchronous setup operation.</returns>
[TestInitialize]
public virtual async Task Setup()
{
MockCache = new MockAppCache();
Page = new AllAppsPage(MockCache);
// Ensure initialization is complete
await MockCache.RefreshAsync();
}
/// <summary>
/// Cleans up the test environment after each test method.
/// </summary>
[TestCleanup]
public virtual void Cleanup()
{
MockCache?.Dispose();
}
/// <summary>
/// Forces synchronous initialization of the page for testing.
/// </summary>
protected void EnsurePageInitialized()
{
// Trigger BuildListItems by accessing items
_ = Page.GetItems();
}
/// <summary>
/// Waits for page initialization with timeout.
/// </summary>
/// <param name="timeoutMs">The timeout in milliseconds.</param>
/// <returns>A task representing the asynchronous wait operation.</returns>
protected async Task WaitForPageInitializationAsync(int timeoutMs = 1000)
{
await MockCache.RefreshAsync();
EnsurePageInitialized();
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Apps.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Apps\Microsoft.CmdPal.Ext.Apps.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,113 @@
// 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.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Mock implementation of IAppCache for unit testing.
/// </summary>
public class MockAppCache : IAppCache
{
private readonly List<Win32Program> _win32s = new();
private readonly List<IUWPApplication> _uwps = new();
private bool _disposed;
private bool _shouldReload;
/// <summary>
/// Gets the collection of Win32 programs.
/// </summary>
public IList<Win32Program> Win32s => _win32s.AsReadOnly();
/// <summary>
/// Gets the collection of UWP applications.
/// </summary>
public IList<IUWPApplication> UWPs => _uwps.AsReadOnly();
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>
/// <returns>True if cache should be reloaded, false otherwise.</returns>
public bool ShouldReload() => _shouldReload;
/// <summary>
/// Resets the reload flag.
/// </summary>
public void ResetReloadFlag() => _shouldReload = false;
/// <summary>
/// Asynchronously refreshes the cache.
/// </summary>
/// <returns>A task representing the asynchronous refresh operation.</returns>
public async Task RefreshAsync()
{
// Simulate minimal async operation for testing
await Task.Delay(1);
}
/// <summary>
/// Adds a Win32 program to the cache.
/// </summary>
/// <param name="program">The Win32 program to add.</param>
/// <exception cref="ArgumentNullException">Thrown when program is null.</exception>
public void AddWin32Program(Win32Program program)
{
ArgumentNullException.ThrowIfNull(program);
_win32s.Add(program);
}
/// <summary>
/// Adds a UWP application to the cache.
/// </summary>
/// <param name="app">The UWP application to add.</param>
/// <exception cref="ArgumentNullException">Thrown when app is null.</exception>
public void AddUWPApplication(IUWPApplication app)
{
ArgumentNullException.ThrowIfNull(app);
_uwps.Add(app);
}
/// <summary>
/// Clears all applications from the cache.
/// </summary>
public void ClearAll()
{
_win32s.Clear();
_uwps.Clear();
}
/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
/// <summary>
/// Releases unmanaged and - optionally - managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected virtual void Dispose(bool disposing)
{
if (!_disposed)
{
if (disposing)
{
// Clean up managed resources
_win32s.Clear();
_uwps.Clear();
}
_disposed = true;
}
}
}

View File

@@ -0,0 +1,140 @@
// 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.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Utils;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Mock implementation of IUWPApplication for unit testing.
/// </summary>
public class MockUWPApplication : IUWPApplication
{
/// <summary>
/// Gets or sets the app list entry.
/// </summary>
public string AppListEntry { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the unique identifier.
/// </summary>
public string UniqueIdentifier { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the display name.
/// </summary>
public string DisplayName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the description.
/// </summary>
public string Description { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the user model ID.
/// </summary>
public string UserModelId { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the background color.
/// </summary>
public string BackgroundColor { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the entry point.
/// </summary>
public string EntryPoint { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the application is enabled.
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// Gets or sets a value indicating whether the application can run elevated.
/// </summary>
public bool CanRunElevated { get; set; }
/// <summary>
/// Gets or sets the logo path.
/// </summary>
public string LogoPath { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the logo type.
/// </summary>
public LogoType LogoType { get; set; } = LogoType.Colored;
/// <summary>
/// Gets or sets the UWP package.
/// </summary>
public UWP Package { get; set; } = null!;
/// <summary>
/// Gets the name of the application.
/// </summary>
public string Name => DisplayName;
/// <summary>
/// Gets the location of the application.
/// </summary>
public string Location => Package?.Location ?? string.Empty;
/// <summary>
/// Gets the localized location of the application.
/// </summary>
public string LocationLocalized => Package?.LocationLocalized ?? string.Empty;
/// <summary>
/// Gets the application identifier.
/// </summary>
/// <returns>The user model ID of the application.</returns>
public string GetAppIdentifier()
{
return UserModelId;
}
/// <summary>
/// Gets the commands available for this application.
/// </summary>
/// <returns>A list of context items.</returns>
public List<IContextItem> GetCommands()
{
return new List<IContextItem>();
}
/// <summary>
/// Updates the logo path based on the specified theme.
/// </summary>
/// <param name="theme">The theme to use for the logo.</param>
public void UpdateLogoPath(Theme theme)
{
// Mock implementation - no-op for testing
}
/// <summary>
/// Converts this UWP application to an AppItem.
/// </summary>
/// <returns>An AppItem representation of this UWP application.</returns>
public AppItem ToAppItem()
{
var iconPath = LogoType != LogoType.Error ? LogoPath : string.Empty;
return new AppItem()
{
Name = Name,
Subtitle = Description,
Type = "Packaged Application", // Equivalent to UWPApplication.Type()
IcoPath = iconPath,
DirPath = Location,
UserModelId = UserModelId,
IsPackaged = true,
Commands = GetCommands(),
AppIdentifier = GetAppIdentifier(),
};
}
}

View File

@@ -0,0 +1,45 @@
// 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.
using System.Linq;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[TestMethod]
public void QueryReturnsExpectedResults()
{
// Arrange
var mockCache = new MockAppCache();
var win32App = TestDataHelper.CreateTestWin32Program("Notepad", "C:\\Windows\\System32\\notepad.exe");
var uwpApp = TestDataHelper.CreateTestUWPApplication("Calculator");
mockCache.AddWin32Program(win32App);
mockCache.AddUWPApplication(uwpApp);
for (var i = 0; i < 10; i++)
{
mockCache.AddWin32Program(TestDataHelper.CreateTestWin32Program($"App{i}"));
mockCache.AddUWPApplication(TestDataHelper.CreateTestUWPApplication($"UWP App {i}"));
}
var page = new AllAppsPage(mockCache);
var provider = new AllAppsCommandProvider(page);
// Act
var allItems = page.GetItems();
// Assert
var notepadResult = Query("notepad", allItems).FirstOrDefault();
Assert.IsNotNull(notepadResult);
Assert.AreEqual("Notepad", notepadResult.Title);
var calculatorResult = Query("cal", allItems).FirstOrDefault();
Assert.IsNotNull(calculatorResult);
Assert.AreEqual("Calculator", calculatorResult.Title);
}
}

View File

@@ -0,0 +1,58 @@
// 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.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Apps.Helpers;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
public class Settings : ISettingsInterface
{
private readonly bool enableStartMenuSource;
private readonly bool enableDesktopSource;
private readonly bool enableRegistrySource;
private readonly bool enablePathEnvironmentVariableSource;
private readonly List<string> programSuffixes;
private readonly List<string> runCommandSuffixes;
public Settings(
bool enableStartMenuSource = true,
bool enableDesktopSource = true,
bool enableRegistrySource = true,
bool enablePathEnvironmentVariableSource = true,
List<string> programSuffixes = null,
List<string> runCommandSuffixes = null)
{
this.enableStartMenuSource = enableStartMenuSource;
this.enableDesktopSource = enableDesktopSource;
this.enableRegistrySource = enableRegistrySource;
this.enablePathEnvironmentVariableSource = enablePathEnvironmentVariableSource;
this.programSuffixes = programSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url" };
this.runCommandSuffixes = runCommandSuffixes ?? new List<string> { "bat", "appref-ms", "exe", "lnk", "url", "cpl", "msc" };
}
public bool EnableStartMenuSource => enableStartMenuSource;
public bool EnableDesktopSource => enableDesktopSource;
public bool EnableRegistrySource => enableRegistrySource;
public bool EnablePathEnvironmentVariableSource => enablePathEnvironmentVariableSource;
public List<string> ProgramSuffixes => programSuffixes;
public List<string> RunCommandSuffixes => runCommandSuffixes;
public static Settings CreateDefaultSettings() => new Settings();
public static Settings CreateDisabledSourcesSettings() => new Settings(
enableStartMenuSource: false,
enableDesktopSource: false,
enableRegistrySource: false,
enablePathEnvironmentVariableSource: false);
public static Settings CreateCustomSuffixesSettings() => new Settings(
programSuffixes: new List<string> { "exe", "bat" },
runCommandSuffixes: new List<string> { "exe", "bat", "cmd" });
}

View File

@@ -0,0 +1,128 @@
// 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.
using Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps.UnitTests;
/// <summary>
/// Helper class to create test data for unit tests.
/// </summary>
public static class TestDataHelper
{
/// <summary>
/// Creates a test Win32 program with the specified parameters.
/// </summary>
/// <param name="name">The name of the application.</param>
/// <param name="fullPath">The full path to the application executable.</param>
/// <param name="enabled">A value indicating whether the application is enabled.</param>
/// <param name="valid">A value indicating whether the application is valid.</param>
/// <returns>A new Win32Program instance with the specified parameters.</returns>
public static Win32Program CreateTestWin32Program(
string name = "Test App",
string fullPath = "C:\\TestApp\\app.exe",
bool enabled = true,
bool valid = true)
{
return new Win32Program
{
Name = name,
FullPath = fullPath,
Enabled = enabled,
Valid = valid,
UniqueIdentifier = $"win32_{name}",
Description = $"Test description for {name}",
ExecutableName = "app.exe",
ParentDirectory = "C:\\TestApp",
AppType = Win32Program.ApplicationType.Win32Application,
};
}
/// <summary>
/// Creates a test UWP application with the specified parameters.
/// </summary>
/// <param name="displayName">The display name of the application.</param>
/// <param name="userModelId">The user model ID of the application.</param>
/// <param name="enabled">A value indicating whether the application is enabled.</param>
/// <returns>A new IUWPApplication instance with the specified parameters.</returns>
public static IUWPApplication CreateTestUWPApplication(
string displayName = "Test UWP App",
string userModelId = "TestPublisher.TestUWPApp_1.0.0.0_neutral__8wekyb3d8bbwe",
bool enabled = true)
{
return new MockUWPApplication
{
DisplayName = displayName,
UserModelId = userModelId,
Enabled = enabled,
UniqueIdentifier = $"uwp_{userModelId}",
Description = $"Test UWP description for {displayName}",
AppListEntry = "default",
BackgroundColor = "#000000",
EntryPoint = "TestApp.App",
CanRunElevated = false,
LogoPath = string.Empty,
Package = CreateMockUWPPackage(displayName, userModelId),
};
}
/// <summary>
/// Creates a mock UWP package for testing purposes.
/// </summary>
/// <param name="displayName">The display name of the package.</param>
/// <param name="userModelId">The user model ID of the package.</param>
/// <returns>A new UWP package instance.</returns>
private static UWP CreateMockUWPPackage(string displayName, string userModelId)
{
var mockPackage = new MockPackage
{
Name = displayName,
FullName = userModelId,
FamilyName = $"{displayName}_8wekyb3d8bbwe",
InstalledLocation = $"C:\\Program Files\\WindowsApps\\{displayName}",
};
return new UWP(mockPackage)
{
Location = mockPackage.InstalledLocation,
LocationLocalized = mockPackage.InstalledLocation,
};
}
/// <summary>
/// Mock implementation of IPackage for testing purposes.
/// </summary>
private sealed class MockPackage : IPackage
{
/// <summary>
/// Gets or sets the name of the package.
/// </summary>
public string Name { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the full name of the package.
/// </summary>
public string FullName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the family name of the package.
/// </summary>
public string FamilyName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets a value indicating whether the package is a framework package.
/// </summary>
public bool IsFramework { get; set; }
/// <summary>
/// Gets or sets a value indicating whether the package is in development mode.
/// </summary>
public bool IsDevelopmentMode { get; set; }
/// <summary>
/// Gets or sets the installed location of the package.
/// </summary>
public string InstalledLocation { get; set; } = string.Empty;
}
}

View File

@@ -0,0 +1,42 @@
// 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.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarkDataTests
{
[TestMethod]
public void BookmarkDataWebUrlDetection()
{
// Act
var webBookmark = new BookmarkData
{
Name = "Test Site",
Bookmark = "https://test.com",
};
var nonWebBookmark = new BookmarkData
{
Name = "Local File",
Bookmark = "C:\\temp\\file.txt",
};
var placeholderBookmark = new BookmarkData
{
Name = "Placeholder",
Bookmark = "{Placeholder}",
};
// Assert
Assert.IsTrue(webBookmark.IsWebUrl());
Assert.IsFalse(webBookmark.IsPlaceholder);
Assert.IsFalse(nonWebBookmark.IsWebUrl());
Assert.IsFalse(nonWebBookmark.IsPlaceholder);
Assert.IsTrue(placeholderBookmark.IsPlaceholder);
}
}

View File

@@ -0,0 +1,535 @@
// 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.
using System.Collections.Generic;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarkJsonParserTests
{
private BookmarkJsonParser _parser;
[TestInitialize]
public void Setup()
{
_parser = new BookmarkJsonParser();
}
[TestMethod]
public void ParseBookmarks_ValidJson_ReturnsBookmarks()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Google",
"Bookmark": "https://www.google.com"
},
{
"Name": "Local File",
"Bookmark": "C:\\temp\\file.txt"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Data.Count);
Assert.AreEqual("Google", result.Data[0].Name);
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
Assert.AreEqual("Local File", result.Data[1].Name);
Assert.AreEqual("C:\\temp\\file.txt", result.Data[1].Bookmark);
}
[TestMethod]
public void ParseBookmarks_EmptyJson_ReturnsEmptyBookmarks()
{
// Arrange
var json = "{}";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_NullJson_ReturnsEmptyBookmarks()
{
// Act
var result = _parser.ParseBookmarks(null);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_WhitespaceJson_ReturnsEmptyBookmarks()
{
// Act
var result = _parser.ParseBookmarks(" ");
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_EmptyString_ReturnsEmptyBookmarks()
{
// Act
var result = _parser.ParseBookmarks(string.Empty);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_InvalidJson_ReturnsEmptyBookmarks()
{
// Arrange
var invalidJson = "{invalid json}";
// Act
var result = _parser.ParseBookmarks(invalidJson);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_MalformedJson_ReturnsEmptyBookmarks()
{
// Arrange
var malformedJson = """
{
"Data": [
{
"Name": "Google",
"Bookmark": "https://www.google.com"
},
{
"Name": "Incomplete entry"
""";
// Act
var result = _parser.ParseBookmarks(malformedJson);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(0, result.Data.Count);
}
[TestMethod]
public void ParseBookmarks_JsonWithTrailingCommas_ParsesSuccessfully()
{
// Arrange - JSON with trailing commas (should be handled by AllowTrailingCommas option)
var json = """
{
"Data": [
{
"Name": "Google",
"Bookmark": "https://www.google.com",
},
{
"Name": "Local File",
"Bookmark": "C:\\temp\\file.txt",
},
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(2, result.Data.Count);
Assert.AreEqual("Google", result.Data[0].Name);
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
}
[TestMethod]
public void ParseBookmarks_JsonWithDifferentCasing_ParsesSuccessfully()
{
// Arrange - JSON with different property name casing (should be handled by PropertyNameCaseInsensitive option)
var json = """
{
"data": [
{
"name": "Google",
"bookmark": "https://www.google.com"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(1, result.Data.Count);
Assert.AreEqual("Google", result.Data[0].Name);
Assert.AreEqual("https://www.google.com", result.Data[0].Bookmark);
}
[TestMethod]
public void SerializeBookmarks_ValidBookmarks_ReturnsJsonString()
{
// Arrange
var bookmarks = new Bookmarks
{
Data = new List<BookmarkData>
{
new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" },
new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" },
},
};
// Act
var result = _parser.SerializeBookmarks(bookmarks);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Google"));
Assert.IsTrue(result.Contains("https://www.google.com"));
Assert.IsTrue(result.Contains("Local File"));
Assert.IsTrue(result.Contains("C:\\\\temp\\\\file.txt")); // Escaped backslashes in JSON
Assert.IsTrue(result.Contains("Data"));
}
[TestMethod]
public void SerializeBookmarks_EmptyBookmarks_ReturnsValidJson()
{
// Arrange
var bookmarks = new Bookmarks();
// Act
var result = _parser.SerializeBookmarks(bookmarks);
// Assert
Assert.IsNotNull(result);
Assert.IsTrue(result.Contains("Data"));
Assert.IsTrue(result.Contains("[]"));
}
[TestMethod]
public void SerializeBookmarks_NullBookmarks_ReturnsEmptyString()
{
// Act
var result = _parser.SerializeBookmarks(null);
// Assert
Assert.AreEqual(string.Empty, result);
}
[TestMethod]
public void ParseBookmarks_RoundTripSerialization_PreservesData()
{
// Arrange
var originalBookmarks = new Bookmarks
{
Data = new List<BookmarkData>
{
new BookmarkData { Name = "Google", Bookmark = "https://www.google.com" },
new BookmarkData { Name = "Local File", Bookmark = "C:\\temp\\file.txt" },
new BookmarkData { Name = "Placeholder", Bookmark = "Open {file} in editor" },
},
};
// Act - Serialize then parse
var serializedJson = _parser.SerializeBookmarks(originalBookmarks);
var parsedBookmarks = _parser.ParseBookmarks(serializedJson);
// Assert
Assert.IsNotNull(parsedBookmarks);
Assert.AreEqual(originalBookmarks.Data.Count, parsedBookmarks.Data.Count);
for (var i = 0; i < originalBookmarks.Data.Count; i++)
{
Assert.AreEqual(originalBookmarks.Data[i].Name, parsedBookmarks.Data[i].Name);
Assert.AreEqual(originalBookmarks.Data[i].Bookmark, parsedBookmarks.Data[i].Bookmark);
Assert.AreEqual(originalBookmarks.Data[i].IsPlaceholder, parsedBookmarks.Data[i].IsPlaceholder);
}
}
[TestMethod]
public void ParseBookmarks_JsonWithPlaceholderBookmarks_CorrectlyIdentifiesPlaceholders()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Regular URL",
"Bookmark": "https://www.google.com"
},
{
"Name": "Placeholder Command",
"Bookmark": "notepad {file}"
},
{
"Name": "Multiple Placeholders",
"Bookmark": "copy {source} {destination}"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(3, result.Data.Count);
Assert.IsFalse(result.Data[0].IsPlaceholder);
Assert.IsTrue(result.Data[1].IsPlaceholder);
Assert.IsTrue(result.Data[2].IsPlaceholder);
}
[TestMethod]
public void ParseBookmarks_IsWebUrl_CorrectlyIdentifiesWebUrls()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "HTTPS Website",
"Bookmark": "https://www.google.com"
},
{
"Name": "HTTP Website",
"Bookmark": "http://example.com"
},
{
"Name": "Website without protocol",
"Bookmark": "www.github.com"
},
{
"Name": "Local File Path",
"Bookmark": "C:\\Users\\test\\Documents\\file.txt"
},
{
"Name": "Network Path",
"Bookmark": "\\\\server\\share\\file.txt"
},
{
"Name": "Executable",
"Bookmark": "notepad.exe"
},
{
"Name": "File URI",
"Bookmark": "file:///C:/temp/file.txt"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(7, result.Data.Count);
// Web URLs should return true
Assert.IsTrue(result.Data[0].IsWebUrl(), "HTTPS URL should be identified as web URL");
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTP URL should be identified as web URL");
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
// Assert.IsTrue(result.Data[2].IsWebUrl(), "Domain without protocol should be identified as web URL");
// Non-web URLs should return false
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file path should not be identified as web URL");
Assert.IsFalse(result.Data[4].IsWebUrl(), "Network path should not be identified as web URL");
Assert.IsFalse(result.Data[5].IsWebUrl(), "Executable should not be identified as web URL");
Assert.IsFalse(result.Data[6].IsWebUrl(), "File URI should not be identified as web URL");
}
[TestMethod]
public void ParseBookmarks_IsPlaceholder_CorrectlyIdentifiesPlaceholders()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Simple Placeholder",
"Bookmark": "notepad {file}"
},
{
"Name": "Multiple Placeholders",
"Bookmark": "copy {source} to {destination}"
},
{
"Name": "Web URL with Placeholder",
"Bookmark": "https://search.com?q={query}"
},
{
"Name": "Complex Placeholder",
"Bookmark": "cmd /c echo {message} > {output_file}"
},
{
"Name": "No Placeholder - Regular URL",
"Bookmark": "https://www.google.com"
},
{
"Name": "No Placeholder - Local File",
"Bookmark": "C:\\temp\\file.txt"
},
{
"Name": "False Positive - Only Opening Brace",
"Bookmark": "test { incomplete"
},
{
"Name": "False Positive - Only Closing Brace",
"Bookmark": "test } incomplete"
},
{
"Name": "Empty Placeholder",
"Bookmark": "command {}"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(9, result.Data.Count);
// Should be identified as placeholders
Assert.IsTrue(result.Data[0].IsPlaceholder, "Simple placeholder should be identified");
Assert.IsTrue(result.Data[1].IsPlaceholder, "Multiple placeholders should be identified");
Assert.IsTrue(result.Data[2].IsPlaceholder, "Web URL with placeholder should be identified");
Assert.IsTrue(result.Data[3].IsPlaceholder, "Complex placeholder should be identified");
Assert.IsTrue(result.Data[8].IsPlaceholder, "Empty placeholder should be identified");
// Should NOT be identified as placeholders
Assert.IsFalse(result.Data[4].IsPlaceholder, "Regular URL should not be placeholder");
Assert.IsFalse(result.Data[5].IsPlaceholder, "Local file should not be placeholder");
Assert.IsFalse(result.Data[6].IsPlaceholder, "Only opening brace should not be placeholder");
Assert.IsFalse(result.Data[7].IsPlaceholder, "Only closing brace should not be placeholder");
}
[TestMethod]
public void ParseBookmarks_MixedProperties_CorrectlyIdentifiesBothWebUrlAndPlaceholder()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "Web URL with Placeholder",
"Bookmark": "https://google.com/search?q={query}"
},
{
"Name": "Web URL without Placeholder",
"Bookmark": "https://github.com"
},
{
"Name": "Local File with Placeholder",
"Bookmark": "notepad {file}"
},
{
"Name": "Local File without Placeholder",
"Bookmark": "C:\\Windows\\notepad.exe"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(4, result.Data.Count);
// Web URL with placeholder
Assert.IsTrue(result.Data[0].IsWebUrl(), "Web URL with placeholder should be identified as web URL");
Assert.IsTrue(result.Data[0].IsPlaceholder, "Web URL with placeholder should be identified as placeholder");
// Web URL without placeholder
Assert.IsTrue(result.Data[1].IsWebUrl(), "Web URL without placeholder should be identified as web URL");
Assert.IsFalse(result.Data[1].IsPlaceholder, "Web URL without placeholder should not be identified as placeholder");
// Local file with placeholder
Assert.IsFalse(result.Data[2].IsWebUrl(), "Local file with placeholder should not be identified as web URL");
Assert.IsTrue(result.Data[2].IsPlaceholder, "Local file with placeholder should be identified as placeholder");
// Local file without placeholder
Assert.IsFalse(result.Data[3].IsWebUrl(), "Local file without placeholder should not be identified as web URL");
Assert.IsFalse(result.Data[3].IsPlaceholder, "Local file without placeholder should not be identified as placeholder");
}
[TestMethod]
public void ParseBookmarks_EdgeCaseUrls_CorrectlyIdentifiesWebUrls()
{
// Arrange
var json = """
{
"Data": [
{
"Name": "FTP URL",
"Bookmark": "ftp://files.example.com"
},
{
"Name": "HTTPS with port",
"Bookmark": "https://localhost:8080"
},
{
"Name": "IP Address",
"Bookmark": "http://192.168.1.1"
},
{
"Name": "Subdomain",
"Bookmark": "https://api.github.com"
},
{
"Name": "Domain only",
"Bookmark": "example.com"
},
{
"Name": "Not a URL - no dots",
"Bookmark": "localhost"
}
]
}
""";
// Act
var result = _parser.ParseBookmarks(json);
// Assert
Assert.IsNotNull(result);
Assert.AreEqual(6, result.Data.Count);
Assert.IsFalse(result.Data[0].IsWebUrl(), "FTP URL should not be identified as web URL");
Assert.IsTrue(result.Data[1].IsWebUrl(), "HTTPS with port should be identified as web URL");
Assert.IsTrue(result.Data[2].IsWebUrl(), "IP Address with HTTP should be identified as web URL");
Assert.IsTrue(result.Data[3].IsWebUrl(), "Subdomain should be identified as web URL");
// This case will fail. We need to consider if we need to support pure domain value in bookmark.
// Assert.IsTrue(result.Data[4].IsWebUrl(), "Domain only should be identified as web URL");
Assert.IsFalse(result.Data[5].IsWebUrl(), "Single word without dots should not be identified as web URL");
}
}

View File

@@ -0,0 +1,137 @@
// 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.
using System.Collections.Generic;
using System.Linq;
using Microsoft.CmdPal.Ext.Bookmarks;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class BookmarksCommandProviderTests
{
[TestMethod]
public void ProviderHasCorrectId()
{
// Setup
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.AreEqual("Bookmarks", provider.Id);
}
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var mockDataSource = new MockBookmarkDataSource();
var provider = new BookmarksCommandProvider(mockDataSource);
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new BookmarksCommandProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new BookmarksCommandProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
[TestMethod]
public void ProviderWithMockData_LoadsBookmarksCorrectly()
{
// Arrange
var jsonData = @"{
""Data"": [
{
""Name"": ""Test Bookmark"",
""Bookmark"": ""https://test.com""
},
{
""Name"": ""Another Bookmark"",
""Bookmark"": ""https://another.com""
}
]
}";
var dataSource = new MockBookmarkDataSource(jsonData);
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
var testBookmark = commands.Where(c => c.Title.Contains("Test Bookmark")).FirstOrDefault();
// Should have three commandsAdd + two custom bookmarks
Assert.AreEqual(3, commands.Length);
Assert.IsNotNull(addCommand);
Assert.IsNotNull(testBookmark);
}
[TestMethod]
public void ProviderWithEmptyData_HasOnlyAddCommand()
{
// Arrange
var dataSource = new MockBookmarkDataSource(@"{ ""Data"": [] }");
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
// Only have Add command
Assert.AreEqual(1, commands.Length);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
[TestMethod]
public void ProviderWithInvalidData_HandlesGracefully()
{
// Arrange
var dataSource = new MockBookmarkDataSource("invalid json");
var provider = new BookmarksCommandProvider(dataSource);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
// Only have one command. Will ignore json parse error.
Assert.AreEqual(1, commands.Length);
var addCommand = commands.Where(c => c.Title.Contains("Add bookmark")).FirstOrDefault();
Assert.IsNotNull(addCommand);
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Bookmarks.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Bookmark\Microsoft.CmdPal.Ext.Bookmarks.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,24 @@
// 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.
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
internal sealed class MockBookmarkDataSource : IBookmarkDataSource
{
private string _jsonData;
public MockBookmarkDataSource(string initialJsonData = "[]")
{
_jsonData = initialJsonData;
}
public string GetBookmarkData()
{
return _jsonData;
}
public void SaveBookmarkData(string jsonData)
{
_jsonData = jsonData;
}
}

View File

@@ -0,0 +1,55 @@
// 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.
using System.Linq;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[TestMethod]
public void ValidateBookmarksCreation()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
// Assert
Assert.IsNotNull(bookmarks);
Assert.IsNotNull(bookmarks.Data);
Assert.AreEqual(2, bookmarks.Data.Count);
}
[TestMethod]
public void ValidateBookmarkData()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
// Act
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
var githubBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "GitHub");
// Assert
Assert.IsNotNull(microsoftBookmark);
Assert.AreEqual("https://www.microsoft.com", microsoftBookmark.Bookmark);
Assert.IsNotNull(githubBookmark);
Assert.AreEqual("https://github.com", githubBookmark.Bookmark);
}
[TestMethod]
public void ValidateWebUrlDetection()
{
// Setup
var bookmarks = Settings.CreateDefaultBookmarks();
var microsoftBookmark = bookmarks.Data.FirstOrDefault(b => b.Name == "Microsoft");
// Assert
Assert.IsNotNull(microsoftBookmark);
Assert.IsTrue(microsoftBookmark.IsWebUrl());
}
}

View File

@@ -0,0 +1,28 @@
// 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.
namespace Microsoft.CmdPal.Ext.Bookmarks.UnitTests;
public static class Settings
{
public static Bookmarks CreateDefaultBookmarks()
{
var bookmarks = new Bookmarks();
// Add some test bookmarks
bookmarks.Data.Add(new BookmarkData
{
Name = "Microsoft",
Bookmark = "https://www.microsoft.com",
});
bookmarks.Data.Add(new BookmarkData
{
Name = "GitHub",
Bookmark = "https://github.com",
});
return bookmarks;
}
}

View File

@@ -0,0 +1,60 @@
// 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.
using System.Globalization;
using System.Linq;
using Microsoft.CmdPal.Ext.Calc.Helper;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Windows.Foundation;
namespace Microsoft.CmdPal.Ext.Calc.UnitTests;
[TestClass]
public class CloseOnEnterTests
{
[TestMethod]
public void PrimaryIsCopy_WhenCloseOnEnterTrue()
{
var settings = new Settings(closeOnEnter: true);
TypedEventHandler<object, object> handleSave = (s, e) => { };
var item = ResultHelper.CreateResult(
4m,
CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture,
"2+2",
settings,
handleSave);
Assert.IsNotNull(item);
Assert.IsInstanceOfType(item.Command, typeof(CopyTextCommand));
var firstMore = item.MoreCommands.First();
Assert.IsInstanceOfType(firstMore, typeof(CommandContextItem));
Assert.IsInstanceOfType(((CommandItem)firstMore).Command, typeof(SaveCommand));
}
[TestMethod]
public void PrimaryIsSave_WhenCloseOnEnterFalse()
{
var settings = new Settings(closeOnEnter: false);
TypedEventHandler<object, object> handleSave = (s, e) => { };
var item = ResultHelper.CreateResult(
4m,
CultureInfo.CurrentCulture,
CultureInfo.CurrentCulture,
"2+2",
settings,
handleSave);
Assert.IsNotNull(item);
Assert.IsInstanceOfType(item.Command, typeof(SaveCommand));
var firstMore = item.MoreCommands.First();
Assert.IsInstanceOfType(firstMore, typeof(CommandContextItem));
Assert.IsInstanceOfType(((CommandItem)firstMore).Command, typeof(CopyTextCommand));
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.Shell.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.Shell\Microsoft.CmdPal.Ext.Shell.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,146 @@
// 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.
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CmdPal.Common.Services;
using Microsoft.CmdPal.Ext.Shell.Pages;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Microsoft.CmdPal.Ext.Shell.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
private static Mock<IRunHistoryService> CreateMockHistoryService(IList<string> historyItems = null)
{
var mockHistoryService = new Mock<IRunHistoryService>();
var history = historyItems ?? new List<string>();
mockHistoryService.Setup(x => x.GetRunHistory())
.Returns(() => history.ToList().AsReadOnly());
mockHistoryService.Setup(x => x.AddRunHistoryItem(It.IsAny<string>()))
.Callback<string>(item =>
{
if (!string.IsNullOrWhiteSpace(item))
{
history.Remove(item);
history.Insert(0, item);
}
});
mockHistoryService.Setup(x => x.ClearRunHistory())
.Callback(() => history.Clear());
return mockHistoryService;
}
private static Mock<IRunHistoryService> CreateMockHistoryServiceWithCommonCommands()
{
var commonCommands = new List<string>
{
"ping google.com",
"ipconfig /all",
"curl https://api.github.com",
"dir",
"cd ..",
"git status",
"npm install",
"python --version",
};
return CreateMockHistoryService(commonCommands);
}
[TestMethod]
public void ValidateHistoryFunctionality()
{
// Setup
var settings = Settings.CreateDefaultSettings();
// Act
settings.AddCmdHistory("test-command");
// Assert
Assert.AreEqual(1, settings.Count["test-command"]);
}
[TestMethod]
[DataRow("ping bing.com", "ping.exe")]
[DataRow("curl bing.com", "curl.exe")]
[DataRow("ipconfig /all", "ipconfig.exe")]
public async Task QueryWithoutHistoryCommand(string command, string exeName)
{
// Setup
var settings = Settings.CreateDefaultSettings();
var mockHistory = CreateMockHistoryService();
var pages = new ShellListPage(settings, mockHistory.Object);
pages.UpdateSearchText(string.Empty, command);
// wait for about 1s.
await Task.Delay(1000);
var commandList = pages.GetItems();
Assert.AreEqual(1, commandList.Length);
var executeCommand = commandList.FirstOrDefault();
Assert.IsNotNull(executeCommand);
Assert.IsNotNull(executeCommand.Icon);
Assert.IsTrue(executeCommand.Title.Contains(exeName), $"expect ${exeName} but got ${executeCommand.Title}");
}
[TestMethod]
[DataRow("ping bing.com", "ping.exe")]
[DataRow("curl bing.com", "curl.exe")]
[DataRow("ipconfig /all", "ipconfig.exe")]
public async Task QueryWithHistoryCommands(string command, string exeName)
{
// Setup
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object);
// Test: Search for a command that exists in history
pages.UpdateSearchText(string.Empty, command);
await Task.Delay(1000);
var commandList = pages.GetItems();
// Should find at least the ping command from history
Assert.IsTrue(commandList.Length > 1);
var expectedCommand = commandList.FirstOrDefault();
Assert.IsNotNull(expectedCommand);
Assert.IsNotNull(expectedCommand.Icon);
Assert.IsTrue(expectedCommand.Title.Contains(exeName), $"expect ${exeName} but got ${expectedCommand.Title}");
}
[TestMethod]
public async Task EmptyQueryWithHistoryCommands()
{
// Setup
var settings = Settings.CreateDefaultSettings();
var mockHistoryService = CreateMockHistoryServiceWithCommonCommands();
var pages = new ShellListPage(settings, mockHistoryService.Object);
pages.UpdateSearchText("abcdefg", string.Empty);
await Task.Delay(1000);
var commandList = pages.GetItems();
// Should find at least the ping command from history
Assert.IsTrue(commandList.Length > 1);
}
}

View File

@@ -0,0 +1,49 @@
// 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.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Shell.Helpers;
namespace Microsoft.CmdPal.Ext.Shell.UnitTests;
public class Settings : ISettingsInterface
{
private readonly bool leaveShellOpen;
private readonly string shellCommandExecution;
private readonly bool runAsAdministrator;
private readonly Dictionary<string, int> count;
public Settings(
bool leaveShellOpen = false,
string shellCommandExecution = "0",
bool runAsAdministrator = false,
Dictionary<string, int> count = null)
{
this.leaveShellOpen = leaveShellOpen;
this.shellCommandExecution = shellCommandExecution;
this.runAsAdministrator = runAsAdministrator;
this.count = count ?? new Dictionary<string, int>();
}
public bool LeaveShellOpen => leaveShellOpen;
public string ShellCommandExecution => shellCommandExecution;
public bool RunAsAdministrator => runAsAdministrator;
public Dictionary<string, int> Count => count;
public void AddCmdHistory(string cmdName)
{
count[cmdName] = count.TryGetValue(cmdName, out var currentCount) ? currentCount + 1 : 1;
}
public static Settings CreateDefaultSettings() => new Settings();
public static Settings CreateLeaveShellOpenSettings() => new Settings(leaveShellOpen: true);
public static Settings CreatePowerShellSettings() => new Settings(shellCommandExecution: "1");
public static Settings CreateAdministratorSettings() => new Settings(runAsAdministrator: true);
}

View File

@@ -0,0 +1,51 @@
// 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.
using Microsoft.CmdPal.Common.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Microsoft.CmdPal.Ext.Shell.UnitTests;
[TestClass]
public class ShellCommandProviderTests
{
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
}

View File

@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk">
<!-- Look at Directory.Build.props in root for common stuff as well -->
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
<PropertyGroup>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<RootNamespace>Microsoft.CmdPal.Ext.WebSearch.UnitTests</RootNamespace>
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\</OutputPath>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Moq" />
<PackageReference Include="MSTest" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\ext\Microsoft.CmdPal.Ext.WebSearch\Microsoft.CmdPal.Ext.WebSearch.csproj" />
<ProjectReference Include="..\Microsoft.CmdPal.Ext.UnitTestsBase\Microsoft.CmdPal.Ext.UnitTestBase.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,73 @@
// 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.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
public class MockSettingsInterface : ISettingsInterface
{
private readonly List<HistoryItem> _historyItems;
public bool GlobalIfURI { get; set; }
public string ShowHistory { get; set; }
public MockSettingsInterface(string showHistory = "none", bool globalIfUri = true, List<HistoryItem> mockHistory = null)
{
_historyItems = mockHistory ?? new List<HistoryItem>();
GlobalIfURI = globalIfUri;
ShowHistory = showHistory;
}
public List<ListItem> LoadHistory()
{
var listItems = new List<ListItem>();
foreach (var historyItem in _historyItems)
{
listItems.Add(new ListItem(new SearchWebCommand(historyItem.SearchString, this))
{
Title = historyItem.SearchString,
Subtitle = historyItem.Timestamp.ToString("g", System.Globalization.CultureInfo.InvariantCulture),
});
}
listItems.Reverse();
return listItems;
}
public void SaveHistory(HistoryItem historyItem)
{
if (historyItem is null)
{
return;
}
_historyItems.Add(historyItem);
// Simulate the same logic as SettingsManager
if (int.TryParse(ShowHistory, out var maxHistoryItems) && maxHistoryItems > 0)
{
while (_historyItems.Count > maxHistoryItems)
{
_historyItems.RemoveAt(0); // Remove the oldest item
}
}
}
// Helper method for testing
public void ClearHistory()
{
_historyItems.Clear();
}
// Helper method for testing
public int GetHistoryCount()
{
return _historyItems.Count;
}
}

View File

@@ -0,0 +1,141 @@
// 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.
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.UnitTestBase;
using Microsoft.CmdPal.Ext.WebSearch.Commands;
using Microsoft.CmdPal.Ext.WebSearch.Helpers;
using Microsoft.CmdPal.Ext.WebSearch.Pages;
using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
[TestClass]
public class QueryTests : CommandPaletteUnitTestBase
{
[TestMethod]
[DataRow("microsoft")]
[DataRow("windows")]
public async Task SearchInWebSearchPage(string query)
{
// Setup
var settings = new MockSettingsInterface();
var page = new WebSearchListPage(settings);
// Act
page.UpdateSearchText(string.Empty, query);
await Task.Delay(1000);
var listItem = page.GetItems();
Assert.IsNotNull(listItem);
Assert.AreEqual(1, listItem.Length);
var expectedItem = listItem.FirstOrDefault();
Assert.IsNotNull(expectedItem);
Assert.IsTrue(expectedItem.Subtitle.Contains("Search the web in"), $"Expected \"search the web in chrome/edge\" but got {expectedItem.Subtitle}");
Assert.AreEqual(query, expectedItem.Title);
}
[TestMethod]
public async Task LoadHistoryReturnsExpectedItems()
{
// Setup
var mockHistoryItems = new List<HistoryItem>
{
new HistoryItem("test search", DateTime.Parse("2024-01-01 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
var page = new WebSearchListPage(settings);
// Act
page.UpdateSearchText("abcdef", string.Empty);
await Task.Delay(1000);
var listItem = page.GetItems();
// Assert
Assert.IsNotNull(listItem);
Assert.AreEqual(2, listItem.Length);
foreach (var item in listItem)
{
Assert.IsNotNull(item);
Assert.IsNotEmpty(item.Title);
Assert.IsNotEmpty(item.Subtitle);
}
}
[TestMethod]
public async Task LoadHistoryMoreThanLimitation()
{
// Setup
var mockHistoryItems = new List<HistoryItem>
{
new HistoryItem("test search", DateTime.Parse("2024-01-01 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search1", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search2", DateTime.Parse("2024-01-03 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search3", DateTime.Parse("2024-01-04 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "5");
var page = new WebSearchListPage(settings);
mockHistoryItems.Add(new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)));
// Act
page.UpdateSearchText("abcdef", string.Empty);
await Task.Delay(1000);
var listItem = page.GetItems();
// Assert
Assert.IsNotNull(listItem);
// Make sure only load five item.
Assert.AreEqual(5, listItem.Length);
}
[TestMethod]
public async Task LoadHistoryWithDisableSetting()
{
// Setup
var mockHistoryItems = new List<HistoryItem>
{
new HistoryItem("test search", DateTime.Parse("2024-01-01 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search1", DateTime.Parse("2024-01-02 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search2", DateTime.Parse("2024-01-03 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search3", DateTime.Parse("2024-01-04 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search4", DateTime.Parse("2024-01-05 13:00:00", CultureInfo.CurrentCulture)),
new HistoryItem("another search5", DateTime.Parse("2024-01-06 13:00:00", CultureInfo.CurrentCulture)),
};
var settings = new MockSettingsInterface(mockHistory: mockHistoryItems, showHistory: "None");
var page = new WebSearchListPage(settings);
// Act
page.UpdateSearchText("abcdef", string.Empty);
await Task.Delay(1000);
var listItem = page.GetItems();
// Assert
Assert.IsNotNull(listItem);
// Make sure only load five item.
Assert.AreEqual(0, listItem.Length);
}
}

View File

@@ -0,0 +1,56 @@
// 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.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
[TestClass]
public class WebSearchCommandProviderTests
{
[TestMethod]
public void ProviderHasCorrectId()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Assert
Assert.AreEqual("WebSearch", provider.Id);
}
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
}

View File

@@ -19,29 +19,22 @@ public class CommandPaletteTestBase : UITestBase
{ {
} }
protected void SetSearchBox(string text) protected void SetSearchBox(string text) => SetSearchBoxText(text);
{
Assert.AreEqual(this.Find<TextBox>("Type here to search...").SetText(text, true).Text, text);
}
protected void SetFilesExtensionSearchBox(string text) protected void SetFilesExtensionSearchBox(string text) => SetSearchBoxText(text);
{
Assert.AreEqual(this.Find<TextBox>("Search for files and folders...").SetText(text, true).Text, text);
}
protected void SetCalculatorExtensionSearchBox(string text) protected void SetCalculatorExtensionSearchBox(string text) => SetSearchBoxText(text);
{
Assert.AreEqual(this.Find<TextBox>("Type an equation...").SetText(text, true).Text, text);
}
protected void SetTimeAndDaterExtensionSearchBox(string text) protected void SetTimeAndDaterExtensionSearchBox(string text) => SetSearchBoxText(text);
private void SetSearchBoxText(string text)
{ {
Assert.AreEqual(this.Find<TextBox>("Search values or type a custom time stamp...").SetText(text, true).Text, text); Assert.AreEqual(this.Find<TextBox>(By.AccessibilityId("MainSearchBox")).SetText(text, true).Text, text);
} }
protected void OpenContextMenu() protected void OpenContextMenu()
{ {
var contextMenuButton = this.Find<Button>("More"); var contextMenuButton = this.Find<Button>(By.AccessibilityId("MoreContextMenuButton"));
Assert.IsNotNull(contextMenuButton, "Context menu button not found."); Assert.IsNotNull(contextMenuButton, "Context menu button not found.");
contextMenuButton.Click(); contextMenuButton.Click();
} }

View File

@@ -69,7 +69,7 @@ public class IndexerTests : CommandPaletteTestBase
searchItem.Click(); searchItem.Click();
var openButton = this.Find<Button>("Open with"); var openButton = this.Find<Button>(By.AccessibilityId("PrimaryCommandButton"));
Assert.IsNotNull(openButton); Assert.IsNotNull(openButton);
openButton.Click(); openButton.Click();
@@ -144,7 +144,7 @@ public class IndexerTests : CommandPaletteTestBase
Assert.IsNotNull(searchItem); Assert.IsNotNull(searchItem);
searchItem.Click(); searchItem.Click();
var openButton = this.Find<Button>("Browse"); var openButton = this.Find<Button>(By.AccessibilityId("SecondaryCommandButton"));
Assert.IsNotNull(openButton); Assert.IsNotNull(openButton);
openButton.Click(); openButton.Click();

View File

@@ -19,16 +19,23 @@ public partial class AllAppsCommandProvider : CommandProvider
public static readonly AllAppsPage Page = new(); public static readonly AllAppsPage Page = new();
private readonly AllAppsPage _page;
private readonly CommandItem _listItem; private readonly CommandItem _listItem;
public AllAppsCommandProvider() public AllAppsCommandProvider()
: this(Page)
{ {
}
public AllAppsCommandProvider(AllAppsPage page)
{
_page = page ?? throw new ArgumentNullException(nameof(page));
Id = WellKnownId; Id = WellKnownId;
DisplayName = Resources.installed_apps; DisplayName = Resources.installed_apps;
Icon = Icons.AllAppsIcon; Icon = Icons.AllAppsIcon;
Settings = AllAppsSettings.Instance.Settings; Settings = AllAppsSettings.Instance.Settings;
_listItem = new(Page) _listItem = new(_page)
{ {
Subtitle = Resources.search_installed_apps, Subtitle = Resources.search_installed_apps,
MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)],
@@ -38,11 +45,11 @@ public partial class AllAppsCommandProvider : CommandProvider
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
} }
public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()]; public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
public ICommandItem? LookupApp(string displayName) public ICommandItem? LookupApp(string displayName)
{ {
var items = Page.GetItems(); var items = _page.GetItems();
// We're going to do this search in two directions: // We're going to do this search in two directions:
// First, is this name a substring of any app... // First, is this name a substring of any app...

View File

@@ -2,6 +2,7 @@
// 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.Collections.Generic; using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
@@ -19,13 +20,20 @@ namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AllAppsPage : ListPage public sealed partial class AllAppsPage : ListPage
{ {
private readonly Lock _listLock = new(); private readonly Lock _listLock = new();
private readonly IAppCache _appCache;
private AppItem[] allApps = []; private AppItem[] allApps = [];
private AppListItem[] unpinnedApps = []; private AppListItem[] unpinnedApps = [];
private AppListItem[] pinnedApps = []; private AppListItem[] pinnedApps = [];
public AllAppsPage() public AllAppsPage()
: this(AppCache.Instance.Value)
{ {
}
public AllAppsPage(IAppCache appCache)
{
_appCache = appCache ?? throw new ArgumentNullException(nameof(appCache));
this.Name = Resources.all_apps; this.Name = Resources.all_apps;
this.Icon = Icons.AllAppsIcon; this.Icon = Icons.AllAppsIcon;
this.ShowDetails = true; this.ShowDetails = true;
@@ -59,7 +67,7 @@ public sealed partial class AllAppsPage : ListPage
private void BuildListItems() private void BuildListItems()
{ {
if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload()) if (allApps.Length == 0 || _appCache.ShouldReload())
{ {
lock (_listLock) lock (_listLock)
{ {
@@ -75,7 +83,7 @@ public sealed partial class AllAppsPage : ListPage
this.IsLoading = false; this.IsLoading = false;
AppCache.Instance.Value.ResetReloadFlag(); _appCache.ResetReloadFlag();
stopwatch.Stop(); stopwatch.Stop();
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms"); Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
@@ -85,11 +93,11 @@ public sealed partial class AllAppsPage : ListPage
private AppItem[] GetAllApps() private AppItem[] GetAllApps()
{ {
var uwpResults = AppCache.Instance.Value.UWPs var uwpResults = _appCache.UWPs
.Where((application) => application.Enabled) .Where((application) => application.Enabled)
.Select(app => app.ToAppItem()); .Select(app => app.ToAppItem());
var win32Results = AppCache.Instance.Value.Win32s var win32Results = _appCache.Win32s
.Where((application) => application.Enabled && application.Valid) .Where((application) => application.Enabled && application.Valid)
.Select(app => app.ToAppItem()); .Select(app => app.ToAppItem());

View File

@@ -5,13 +5,14 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Microsoft.CmdPal.Ext.Apps.Helpers;
using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Programs;
using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Properties;
using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps; namespace Microsoft.CmdPal.Ext.Apps;
public class AllAppsSettings : JsonSettingsManager public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
{ {
private static readonly string _namespace = "apps"; private static readonly string _namespace = "apps";

View File

@@ -12,7 +12,7 @@ using Microsoft.CmdPal.Ext.Apps.Utils;
namespace Microsoft.CmdPal.Ext.Apps; namespace Microsoft.CmdPal.Ext.Apps;
public sealed partial class AppCache : IDisposable public sealed partial class AppCache : IAppCache, IDisposable
{ {
private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper; private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper;
@@ -24,7 +24,7 @@ public sealed partial class AppCache : IDisposable
public IList<Win32Program> Win32s => _win32ProgramRepository.Items; public IList<Win32Program> Win32s => _win32ProgramRepository.Items;
public IList<UWPApplication> UWPs => _packageRepository.Items; public IList<IUWPApplication> UWPs => _packageRepository.Items;
public static readonly Lazy<AppCache> Instance = new(() => new()); public static readonly Lazy<AppCache> Instance = new(() => new());

View File

@@ -26,6 +26,11 @@ public sealed partial class AppCommand : InvokableCommand
Name = Resources.run_command_action; Name = Resources.run_command_action;
Id = GenerateId(); Id = GenerateId();
if (!string.IsNullOrEmpty(app.IcoPath))
{
Icon = new(app.IcoPath);
}
} }
internal static async Task StartApp(string aumid) internal static async Task StartApp(string aumid)

View File

@@ -131,7 +131,7 @@ internal sealed partial class AppListItem : ListItem
var newCommands = new List<IContextItem>(); var newCommands = new List<IContextItem>();
newCommands.AddRange(commands); newCommands.AddRange(commands);
newCommands.Add(new SeparatorContextItem()); newCommands.Add(new Separator());
// 0x50 = P // 0x50 = P
// Full key chord would be Ctrl+P // Full key chord would be Ctrl+P

View File

@@ -0,0 +1,22 @@
// 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.
using System.Collections.Generic;
namespace Microsoft.CmdPal.Ext.Apps.Helpers;
public interface ISettingsInterface
{
public bool EnableStartMenuSource { get; }
public bool EnableDesktopSource { get; }
public bool EnableRegistrySource { get; }
public bool EnablePathEnvironmentVariableSource { get; }
public List<string> ProgramSuffixes { get; }
public List<string> RunCommandSuffixes { get; }
}

View File

@@ -0,0 +1,37 @@
// 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.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.CmdPal.Ext.Apps.Programs;
namespace Microsoft.CmdPal.Ext.Apps;
/// <summary>
/// Interface for application cache that provides access to Win32 and UWP applications.
/// </summary>
public interface IAppCache : IDisposable
{
/// <summary>
/// Gets the collection of Win32 programs.
/// </summary>
IList<Win32Program> Win32s { get; }
/// <summary>
/// Gets the collection of UWP applications.
/// </summary>
IList<IUWPApplication> UWPs { get; }
/// <summary>
/// Determines whether the cache should be reloaded.
/// </summary>
/// <returns>True if cache should be reloaded, false otherwise.</returns>
bool ShouldReload();
/// <summary>
/// Resets the reload flag.
/// </summary>
void ResetReloadFlag();
}

View File

@@ -0,0 +1,43 @@
// 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.
using System.Collections.Generic;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Apps.Programs;
/// <summary>
/// Interface for UWP applications to enable testing and mocking
/// </summary>
public interface IUWPApplication : IProgram
{
string AppListEntry { get; set; }
string DisplayName { get; set; }
string UserModelId { get; set; }
string BackgroundColor { get; set; }
string EntryPoint { get; set; }
bool CanRunElevated { get; set; }
string LogoPath { get; set; }
LogoType LogoType { get; set; }
UWP Package { get; set; }
string LocationLocalized { get; }
string GetAppIdentifier();
List<IContextItem> GetCommands();
void UpdateLogoPath(Utils.Theme theme);
AppItem ToAppItem();
}

View File

@@ -22,7 +22,7 @@ using Theme = Microsoft.CmdPal.Ext.Apps.Utils.Theme;
namespace Microsoft.CmdPal.Ext.Apps.Programs; namespace Microsoft.CmdPal.Ext.Apps.Programs;
[Serializable] [Serializable]
public class UWPApplication : IProgram public class UWPApplication : IUWPApplication
{ {
private static readonly IFileSystem FileSystem = new FileSystem(); private static readonly IFileSystem FileSystem = new FileSystem();
private static readonly IPath Path = FileSystem.Path; private static readonly IPath Path = FileSystem.Path;
@@ -517,7 +517,7 @@ public class UWPApplication : IProgram
} }
} }
internal AppItem ToAppItem() public AppItem ToAppItem()
{ {
var app = this; var app = this;
var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty; var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;

View File

@@ -0,0 +1,7 @@
// 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.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Apps.UnitTests")]

View File

@@ -15,7 +15,7 @@ namespace Microsoft.CmdPal.Ext.Apps.Storage;
/// A repository for storing packaged applications such as UWP apps or appx packaged desktop apps. /// A repository for storing packaged applications such as UWP apps or appx packaged desktop apps.
/// This repository will also monitor for changes to the PackageCatalog and update the repository accordingly /// This repository will also monitor for changes to the PackageCatalog and update the repository accordingly
/// </summary> /// </summary>
internal sealed partial class PackageRepository : ListRepository<UWPApplication>, IProgramRepository internal sealed partial class PackageRepository : ListRepository<IUWPApplication>, IProgramRepository
{ {
private readonly IPackageCatalog _packageCatalog; private readonly IPackageCatalog _packageCatalog;

View File

@@ -0,0 +1,45 @@
// 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.
using System.Collections.Generic;
using System.Text.Json;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class BookmarkJsonParser
{
public BookmarkJsonParser()
{
}
public Bookmarks ParseBookmarks(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return new Bookmarks();
}
try
{
var bookmarks = JsonSerializer.Deserialize<Bookmarks>(json, BookmarkSerializationContext.Default.Bookmarks);
return bookmarks ?? new Bookmarks();
}
catch (JsonException ex)
{
ExtensionHost.LogMessage($"parse bookmark data failed. ex: {ex.Message}");
return new Bookmarks();
}
}
public string SerializeBookmarks(Bookmarks? bookmarks)
{
if (bookmarks == null)
{
return string.Empty;
}
return JsonSerializer.Serialize(bookmarks, BookmarkSerializationContext.Default.Bookmarks);
}
}

View File

@@ -11,34 +11,4 @@ namespace Microsoft.CmdPal.Ext.Bookmarks;
public sealed class Bookmarks public sealed class Bookmarks
{ {
public List<BookmarkData> Data { get; set; } = []; public List<BookmarkData> Data { get; set; } = [];
private static readonly JsonSerializerOptions _jsonOptions = new()
{
IncludeFields = true,
};
public static Bookmarks ReadFromFile(string path)
{
var data = new Bookmarks();
// if the file exists, load it and append the new item
if (File.Exists(path))
{
var jsonStringReading = File.ReadAllText(path);
if (!string.IsNullOrEmpty(jsonStringReading))
{
data = JsonSerializer.Deserialize<Bookmarks>(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks();
}
}
return data;
}
public static void WriteToFile(string path, Bookmarks data)
{
var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks);
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
}
} }

View File

@@ -20,10 +20,20 @@ public partial class BookmarksCommandProvider : CommandProvider
private readonly AddBookmarkPage _addNewCommand = new(null); private readonly AddBookmarkPage _addNewCommand = new(null);
private readonly IBookmarkDataSource _dataSource;
private readonly BookmarkJsonParser _parser;
private Bookmarks? _bookmarks; private Bookmarks? _bookmarks;
public BookmarksCommandProvider() public BookmarksCommandProvider()
: this(new FileBookmarkDataSource(StateJsonPath()))
{ {
}
internal BookmarksCommandProvider(IBookmarkDataSource dataSource)
{
_dataSource = dataSource;
_parser = new BookmarkJsonParser();
Id = "Bookmarks"; Id = "Bookmarks";
DisplayName = Resources.bookmarks_display_name; DisplayName = Resources.bookmarks_display_name;
Icon = Icons.PinIcon; Icon = Icons.PinIcon;
@@ -49,10 +59,14 @@ public partial class BookmarksCommandProvider : CommandProvider
private void SaveAndUpdateCommands() private void SaveAndUpdateCommands()
{ {
if (_bookmarks is not null) try
{ {
var jsonPath = BookmarksCommandProvider.StateJsonPath(); var jsonData = _parser.SerializeBookmarks(_bookmarks);
Bookmarks.WriteToFile(jsonPath, _bookmarks); _dataSource.SaveBookmarkData(jsonData);
}
catch (Exception ex)
{
Logger.LogError($"Failed to save bookmarks: {ex.Message}");
} }
LoadCommands(); LoadCommands();
@@ -82,11 +96,8 @@ public partial class BookmarksCommandProvider : CommandProvider
{ {
try try
{ {
var jsonFile = StateJsonPath(); var jsonData = _dataSource.GetBookmarkData();
if (File.Exists(jsonFile)) _bookmarks = _parser.ParseBookmarks(jsonData);
{
_bookmarks = Bookmarks.ReadFromFile(jsonFile);
}
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@@ -0,0 +1,49 @@
// 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.
using System;
using System.IO;
using Microsoft.CommandPalette.Extensions.Toolkit;
namespace Microsoft.CmdPal.Ext.Bookmarks;
public class FileBookmarkDataSource : IBookmarkDataSource
{
private readonly string _filePath;
public FileBookmarkDataSource(string filePath)
{
_filePath = filePath;
}
public string GetBookmarkData()
{
if (!File.Exists(_filePath))
{
return string.Empty;
}
try
{
return File.ReadAllText(_filePath);
}
catch (Exception ex)
{
ExtensionHost.LogMessage($"Read bookmark data failed. ex: {ex.Message}");
return string.Empty;
}
}
public void SaveBookmarkData(string jsonData)
{
try
{
File.WriteAllText(_filePath, jsonData);
}
catch (Exception ex)
{
ExtensionHost.LogMessage($"Failed to save bookmark data: {ex}");
}
}
}

View File

@@ -0,0 +1,11 @@
// 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.
namespace Microsoft.CmdPal.Ext.Bookmarks;
public interface IBookmarkDataSource
{
string GetBookmarkData();
void SaveBookmarkData(string jsonData);
}

View File

@@ -0,0 +1,7 @@
// 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.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Bookmarks.UnitTests")]

View File

@@ -14,14 +14,14 @@ namespace Microsoft.CmdPal.Ext.Shell.Commands;
internal sealed partial class ExecuteItem : InvokableCommand internal sealed partial class ExecuteItem : InvokableCommand
{ {
private readonly SettingsManager _settings; private readonly ISettingsInterface _settings;
private readonly RunAsType _runas; private readonly RunAsType _runas;
public string Cmd { get; internal set; } = string.Empty; public string Cmd { get; internal set; } = string.Empty;
private static readonly char[] Separator = [' ']; private static readonly char[] Separator = [' '];
public ExecuteItem(string cmd, SettingsManager settings, RunAsType type = RunAsType.None) public ExecuteItem(string cmd, ISettingsInterface settings, RunAsType type = RunAsType.None)
{ {
if (type == RunAsType.Administrator) if (type == RunAsType.Administrator)
{ {

View File

@@ -0,0 +1,20 @@
// 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.
using System.Collections.Generic;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
public interface ISettingsInterface
{
public bool LeaveShellOpen { get; }
public string ShellCommandExecution { get; }
public bool RunAsAdministrator { get; }
public Dictionary<string, int> Count { get; }
public void AddCmdHistory(string cmdName);
}

Some files were not shown because too many files have changed in this diff Show More