mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-16 11:48:06 +01:00
Merge remote-tracking branch 'origin/main' into dev/snickler/net10-upgrade
This commit is contained in:
44
.github/actions/spell-check/expect.txt
vendored
44
.github/actions/spell-check/expect.txt
vendored
@@ -25,11 +25,14 @@ ADMINS
|
||||
adml
|
||||
admx
|
||||
advancedpaste
|
||||
advancedpasteui
|
||||
advancedpasteuishortcut
|
||||
advfirewall
|
||||
AFeature
|
||||
affordances
|
||||
AFX
|
||||
AGGREGATABLE
|
||||
AHK
|
||||
AHybrid
|
||||
akv
|
||||
ALarger
|
||||
@@ -40,6 +43,7 @@ ALLINPUT
|
||||
Allman
|
||||
Allmodule
|
||||
ALLOWUNDO
|
||||
allpc
|
||||
ALLVIEW
|
||||
ALPHATYPE
|
||||
AModifier
|
||||
@@ -629,6 +633,7 @@ HKCU
|
||||
hkey
|
||||
HKLM
|
||||
HKM
|
||||
hkmng
|
||||
HKPD
|
||||
HKU
|
||||
HMD
|
||||
@@ -646,7 +651,11 @@ Hostx
|
||||
hotfixes
|
||||
hotkeycontrol
|
||||
HOTKEYF
|
||||
hotkeylockmachine
|
||||
hotkeyreconnect
|
||||
hotkeys
|
||||
hotkeyswitch
|
||||
hotkeytoggleeasymouse
|
||||
hotlight
|
||||
hotspot
|
||||
HPAINTBUFFER
|
||||
@@ -659,6 +668,7 @@ HROW
|
||||
hsb
|
||||
HSCROLL
|
||||
hsi
|
||||
HSpeed
|
||||
HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
@@ -704,9 +714,12 @@ IMAGERESIZERCONTEXTMENU
|
||||
IMAGERESIZEREXT
|
||||
imageresizerinput
|
||||
imageresizersettings
|
||||
imagetotext
|
||||
imagetotextshortcut
|
||||
imagingdevices
|
||||
ime
|
||||
imgflip
|
||||
inapp
|
||||
inbox
|
||||
INCONTACT
|
||||
Indo
|
||||
@@ -760,6 +773,7 @@ istep
|
||||
ith
|
||||
ITHUMBNAIL
|
||||
IUI
|
||||
IUWP
|
||||
IWIC
|
||||
jfif
|
||||
jgeosdfsdsgmkedfgdfgdfgbkmhcgcflmi
|
||||
@@ -789,6 +803,7 @@ keyvault
|
||||
KILLFOCUS
|
||||
killrunner
|
||||
kmph
|
||||
kvp
|
||||
Kybd
|
||||
lastcodeanalysissucceeded
|
||||
LASTEXITCODE
|
||||
@@ -827,6 +842,7 @@ localappdata
|
||||
localpackage
|
||||
LOCALSYSTEM
|
||||
LOCATIONCHANGE
|
||||
LOCKMACHINE
|
||||
LOCKTYPE
|
||||
LOGFONT
|
||||
LOGFONTW
|
||||
@@ -912,6 +928,7 @@ MDL
|
||||
mdtext
|
||||
mdtxt
|
||||
mdwn
|
||||
measuretool
|
||||
meme
|
||||
memicmp
|
||||
MENUITEMINFO
|
||||
@@ -961,6 +978,7 @@ MOUSEHWHEEL
|
||||
MOUSEINPUT
|
||||
mousejump
|
||||
mousepointer
|
||||
mousepointercrosshairs
|
||||
mouseutils
|
||||
MOVESIZEEND
|
||||
MOVESIZESTART
|
||||
@@ -1100,6 +1118,7 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
@@ -1161,6 +1180,18 @@ PARENTRELATIVEFORADDRESSBAR
|
||||
PARENTRELATIVEPARSING
|
||||
parray
|
||||
PARTIALCONFIRMATIONDIALOGTITLE
|
||||
pasteashtmlfile
|
||||
pasteashtmlfileshortcut
|
||||
pasteasjson
|
||||
pasteasjsonshortcut
|
||||
pasteasmarkdown
|
||||
pasteasmarkdownshortcut
|
||||
pasteasplaintext
|
||||
pasteasplaintextshortcut
|
||||
pasteaspngfile
|
||||
pasteaspngfileshortcut
|
||||
pasteastxtfile
|
||||
pasteastxtfileshortcut
|
||||
PATCOPY
|
||||
PATHMUSTEXIST
|
||||
PATINVERT
|
||||
@@ -1228,6 +1259,7 @@ Pomodoro
|
||||
Popups
|
||||
POPUPWINDOW
|
||||
POSITIONITEM
|
||||
powerocr
|
||||
POWERRENAMECONTEXTMENU
|
||||
powerrenameinput
|
||||
POWERRENAMETEST
|
||||
@@ -1368,6 +1400,7 @@ Removelnk
|
||||
renamable
|
||||
RENAMEONCOLLISION
|
||||
reparented
|
||||
reparenthotkey
|
||||
reparenting
|
||||
reportfileaccesses
|
||||
requery
|
||||
@@ -1617,6 +1650,7 @@ STYLECHANGED
|
||||
STYLECHANGING
|
||||
subkeys
|
||||
sublang
|
||||
Subdomain
|
||||
SUBMODULEUPDATE
|
||||
subresource
|
||||
Superbar
|
||||
@@ -1687,6 +1721,7 @@ THH
|
||||
THICKFRAME
|
||||
THISCOMPONENT
|
||||
throughs
|
||||
thumbnailhotkey
|
||||
TILEDWINDOW
|
||||
TILLSON
|
||||
timedate
|
||||
@@ -1701,6 +1736,7 @@ tlb
|
||||
tlbimp
|
||||
tlc
|
||||
TNP
|
||||
TOGGLEEASYMOUSE
|
||||
Toolhelp
|
||||
toolkitconverters
|
||||
toolwindow
|
||||
@@ -1714,6 +1750,7 @@ tracelogging
|
||||
tracerpt
|
||||
trackbar
|
||||
trafficmanager
|
||||
transcodetomp
|
||||
transicc
|
||||
TRAYMOUSEMESSAGE
|
||||
triaging
|
||||
@@ -1828,6 +1865,7 @@ VSINSTALLDIR
|
||||
VSM
|
||||
vso
|
||||
vsonline
|
||||
VSpeed
|
||||
vstemplate
|
||||
vstest
|
||||
VSTHRD
|
||||
@@ -1964,10 +2002,13 @@ XNamespace
|
||||
Xoshiro
|
||||
XPels
|
||||
XPixel
|
||||
XPos
|
||||
XResource
|
||||
xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
xxxxxx
|
||||
@@ -1977,7 +2018,10 @@ YIncrement
|
||||
yinle
|
||||
yinyue
|
||||
YPels
|
||||
YPos
|
||||
YResolution
|
||||
YSpeed
|
||||
YTimer
|
||||
YStr
|
||||
YVIRTUALSCREEN
|
||||
ZEROINIT
|
||||
|
||||
@@ -57,12 +57,19 @@ $totalList = $projFiles | ForEach-Object -Parallel {
|
||||
|
||||
$p = -split $p
|
||||
$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;
|
||||
}
|
||||
|
||||
@@ -21,4 +21,13 @@ if (-not $?)
|
||||
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
|
||||
|
||||
@@ -65,7 +65,8 @@
|
||||
<!-- Moq to stay below v4.20 due to behavior change. need to be sure fixed -->
|
||||
<PackageVersion Include="Moq" Version="4.18.4" />
|
||||
<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.Schema" Version="5.2.8" />
|
||||
<PackageVersion Include="OpenAI" Version="2.0.0" />
|
||||
@@ -103,7 +104,7 @@
|
||||
<PackageVersion Include="System.Text.RegularExpressions" Version="4.3.1" />
|
||||
<PackageVersion Include="UnicodeInformation" Version="2.6.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="WPF-UI" Version="3.0.5" />
|
||||
<PackageVersion Include="WyHash" Version="1.0.5" />
|
||||
|
||||
137
NOTICE.md
137
NOTICE.md
@@ -1491,93 +1491,50 @@ SOFTWARE.
|
||||
|
||||
## NuGet Packages used by PowerToys
|
||||
|
||||
- AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta
|
||||
- AdaptiveCards.Rendering.WinUI3 2.1.0-beta
|
||||
- AdaptiveCards.Templating 2.0.5
|
||||
- Appium.WebDriver 4.4.5
|
||||
- Azure.AI.OpenAI 1.0.0-beta.17
|
||||
- CoenM.ImageSharp.ImageHash 1.3.6
|
||||
- CommunityToolkit.Common 8.4.0
|
||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock 0.1.250703-build.2173
|
||||
- CommunityToolkit.Mvvm 8.4.0
|
||||
- CommunityToolkit.WinUI.Animations 8.2.250402
|
||||
- CommunityToolkit.WinUI.Collections 8.2.250402
|
||||
- CommunityToolkit.WinUI.Controls.Primitives 8.2.250402
|
||||
- CommunityToolkit.WinUI.Controls.Segmented 8.2.250402
|
||||
- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250402
|
||||
- CommunityToolkit.WinUI.Controls.Sizers 8.2.250402
|
||||
- CommunityToolkit.WinUI.Converters 8.2.250402
|
||||
- CommunityToolkit.WinUI.Extensions 8.2.250402
|
||||
- CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2
|
||||
- CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2
|
||||
- ControlzEx 6.0.0
|
||||
- HelixToolkit 2.24.0
|
||||
- HelixToolkit.Core.Wpf 2.24.0
|
||||
- hyjiacan.pinyin4net 4.1.1
|
||||
- Interop.Microsoft.Office.Interop.OneNote 1.1.0.2
|
||||
- LazyCache 2.4.0
|
||||
- Mages 3.0.0
|
||||
- Markdig.Signed 0.34.0
|
||||
- MessagePack 3.1.3
|
||||
- Microsoft.Bcl.AsyncInterfaces 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 10.0.0-preview.25380.108
|
||||
- Microsoft.Data.Sqlite 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.DotNet.ILCompiler (A)
|
||||
- Microsoft.Extensions.DependencyInjection 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Extensions.Hosting 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Extensions.Logging 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Extensions.Logging.Abstractions 10.0.0-preview.7.25380.108
|
||||
- Microsoft.NET.ILLink.Tasks (A)
|
||||
- Microsoft.SemanticKernel 1.15.0
|
||||
- Microsoft.Toolkit.Uwp.Notifications 7.1.2
|
||||
- Microsoft.Web.WebView2 1.0.2903.40
|
||||
- Microsoft.Win32.SystemEvents 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Windows.Compatibility 10.0.0-preview.7.25380.108
|
||||
- Microsoft.Windows.CsWin32 0.3.183
|
||||
- Microsoft.Windows.CsWinRT 2.2.0
|
||||
- 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
|
||||
- AdaptiveCards.ObjectModel.WinUI3
|
||||
- AdaptiveCards.Rendering.WinUI3
|
||||
- AdaptiveCards.Templating
|
||||
- Appium.WebDriver
|
||||
- Azure.AI.OpenAI
|
||||
- CoenM.ImageSharp.ImageHash
|
||||
- CommunityToolkit.Common
|
||||
- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
|
||||
- CommunityToolkit.Mvvm
|
||||
- CommunityToolkit.WinUI.Animations
|
||||
- CommunityToolkit.WinUI.Collections
|
||||
- CommunityToolkit.WinUI.Controls.Primitives
|
||||
- CommunityToolkit.WinUI.Controls.Segmented
|
||||
- CommunityToolkit.WinUI.Controls.SettingsControls
|
||||
- CommunityToolkit.WinUI.Controls.Sizers
|
||||
- CommunityToolkit.WinUI.Converters
|
||||
- CommunityToolkit.WinUI.Extensions
|
||||
- CommunityToolkit.WinUI.UI.Controls.DataGrid
|
||||
- CommunityToolkit.WinUI.UI.Controls.Markdown
|
||||
- ControlzEx
|
||||
- HelixToolkit
|
||||
- HelixToolkit.Core.Wpf
|
||||
- hyjiacan.pinyin4net
|
||||
- Interop.Microsoft.Office.Interop.OneNote
|
||||
- LazyCache
|
||||
- Mages
|
||||
- Markdig.Signed
|
||||
- MessagePack
|
||||
- ModernWpfUI
|
||||
- Moq
|
||||
- MSTest
|
||||
- NLog
|
||||
- NLog.Extensions.Logging
|
||||
- NLog.Schema
|
||||
- OpenAI
|
||||
- ReverseMarkdown
|
||||
- ScipBe.Common.Office.OneNote
|
||||
- SharpCompress
|
||||
- SkiaSharp.Views.WinUI
|
||||
- StreamJsonRpc
|
||||
- StyleCop.Analyzers
|
||||
- UnicodeInformation
|
||||
- UnitsNet
|
||||
- UTF.Unknown
|
||||
- WinUIEx
|
||||
- WPF-UI
|
||||
- WyHash
|
||||
@@ -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\EventWaiter.h = src\common\utils\EventWaiter.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\game_mode.h = src\common\utils\game_mode.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
|
||||
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
|
||||
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
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
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|x64.ActiveCfg = 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
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
@@ -3161,6 +3202,10 @@ Global
|
||||
{E816D7AF-4688-4ECB-97CC-3D8E798F3828} = {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}
|
||||
{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
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
|
||||
|
||||
@@ -71,6 +71,41 @@ When the user changes settings in the UI:
|
||||
3. The runner calls the `set_config` function on the appropriate module
|
||||
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 module’s 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 module’s 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 module’s Views call `OnPageLoaded()`
|
||||
Once the module’s view is loaded, make sure to invoke the ViewModel’s `OnPageLoaded()` method:
|
||||
```cs
|
||||
Loaded += (s, e) => ViewModel.OnPageLoaded();
|
||||
```
|
||||
- For reference, see: `src/settings-ui/Settings.UI/SettingsXAML/Views/AdvancedPaste.xaml.cs`
|
||||
|
||||
## Debugging Settings
|
||||
|
||||
To debug settings issues:
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 7.4 KiB After Width: | Height: | Size: 18 KiB |
@@ -14,21 +14,6 @@
|
||||
<DirectoryRef Id="FileLocksmithAssetsInstallFolder" FileSource="$(var.FileLocksmithAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--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>
|
||||
|
||||
<ComponentGroup Id="FileLocksmithComponentGroup">
|
||||
@@ -38,7 +23,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderFileLocksmithAssetsFolder" Directory="FileLocksmithAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_FileLocksmith" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -16,71 +16,6 @@
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--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>
|
||||
|
||||
<ComponentGroup Id="ImageResizerComponentGroup">
|
||||
@@ -90,7 +25,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderImageResizerAssetsFolder" Directory="ImageResizerAssetsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_ImageResizer_Registry" />
|
||||
</ComponentGroup>
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -18,19 +18,6 @@
|
||||
<DirectoryRef Id="NewPlusAssetsInstallFolder" FileSource="$(var.NewPlusAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--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>
|
||||
|
||||
<ComponentGroup Id="NewPlusComponentGroup">
|
||||
@@ -40,7 +27,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderNewPlusAssetsFolder" Directory="NewPlusAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="NewPlus_ShellExtension_win10" />
|
||||
</ComponentGroup>
|
||||
|
||||
|
||||
|
||||
@@ -14,22 +14,6 @@
|
||||
<DirectoryRef Id="PowerRenameAssetsFolder" FileSource="$(var.PowerRenameAssetsFilesPath)">
|
||||
<!-- Generated by generateFileComponents.ps1 -->
|
||||
<!--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>
|
||||
|
||||
<ComponentGroup Id="PowerRenameComponentGroup">
|
||||
@@ -39,7 +23,6 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderPowerRenameAssetsFolder" Directory="PowerRenameAssetsFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="Module_PowerRename" />
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
|
||||
@@ -176,6 +176,18 @@
|
||||
<Custom Action="UnRegisterContextMenuPackages" Before="RemoveFiles">
|
||||
Installed AND (REMOVE="ALL")
|
||||
</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">
|
||||
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
|
||||
</Custom>
|
||||
@@ -439,6 +451,35 @@
|
||||
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"
|
||||
Return="ignore"
|
||||
Impersonate="yes"
|
||||
|
||||
@@ -1153,6 +1153,113 @@ UINT __stdcall UnRegisterContextMenuPackagesCA(MSIHANDLE hInstall)
|
||||
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)
|
||||
{
|
||||
HRESULT hr = S_OK;
|
||||
|
||||
@@ -28,3 +28,7 @@ EXPORTS
|
||||
UninstallCommandNotFoundModuleCA
|
||||
UpgradeCommandNotFoundModuleCA
|
||||
UnsetAdvancedPasteAPIKeyCA
|
||||
CleanImageResizerRuntimeRegistryCA
|
||||
CleanFileLocksmithRuntimeRegistryCA
|
||||
CleanPowerRenameRuntimeRegistryCA
|
||||
CleanNewPlusRuntimeRegistryCA
|
||||
|
||||
266
src/common/utils/shell_ext_registration.h
Normal file
266
src/common/utils/shell_ext_registration.h
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ private:
|
||||
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
|
||||
{
|
||||
@@ -122,6 +122,7 @@ private:
|
||||
hotkey.shift = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_SHIFT);
|
||||
hotkey.ctrl = jsonHotkeyObject.GetNamedBoolean(JSON_KEY_CTRL);
|
||||
hotkey.key = static_cast<unsigned char>(jsonHotkeyObject.GetNamedNumber(JSON_KEY_CODE));
|
||||
hotkey.isShown = isShown;
|
||||
return hotkey;
|
||||
}
|
||||
catch (...)
|
||||
@@ -231,8 +232,10 @@ private:
|
||||
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)
|
||||
{
|
||||
return;
|
||||
@@ -240,9 +243,9 @@ private:
|
||||
|
||||
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))
|
||||
@@ -250,7 +253,7 @@ private:
|
||||
const AdditionalAction additionalAction
|
||||
{
|
||||
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);
|
||||
@@ -259,7 +262,7 @@ private:
|
||||
{
|
||||
for (const auto& [subActionName, subAction] : action)
|
||||
{
|
||||
process_additional_action(subActionName, subAction);
|
||||
process_additional_action(subActionName, subAction, actionIsShown);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -317,9 +320,21 @@ private:
|
||||
{
|
||||
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,13 +346,11 @@ private:
|
||||
for (const auto& customAction : customActions)
|
||||
{
|
||||
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
|
||||
{
|
||||
const CustomAction customActionData{
|
||||
static_cast<int>(object.GetNamedNumber(JSON_KEY_ID)),
|
||||
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT))
|
||||
parse_single_hotkey(object.GetNamedObject(JSON_KEY_SHORTCUT), actionIsShown)
|
||||
};
|
||||
|
||||
m_custom_actions.push_back(customActionData);
|
||||
@@ -347,7 +360,6 @@ private:
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (settingsObject.GetView().Size())
|
||||
{
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
<ClInclude Include="ClassFactory.h" />
|
||||
<ClInclude Include="dllmain.h" />
|
||||
<ClInclude Include="ExplorerCommand.h" />
|
||||
<ClInclude Include="RuntimeRegistration.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
<None Include="packages.config" />
|
||||
<None Include="resource.base.h" />
|
||||
|
||||
@@ -27,6 +27,9 @@
|
||||
<ClInclude Include="dllmain.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="RuntimeRegistration.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ClCompile Include="dllmain.cpp">
|
||||
|
||||
@@ -12,6 +12,7 @@
|
||||
#include "FileLocksmithLib/Constants.h"
|
||||
#include "FileLocksmithLib/Settings.h"
|
||||
#include "FileLocksmithLib/Trace.h"
|
||||
#include "RuntimeRegistration.h"
|
||||
|
||||
#include "dllmain.h"
|
||||
#include "Generated Files/resource.h"
|
||||
@@ -82,12 +83,17 @@ public:
|
||||
{
|
||||
std::wstring path = get_module_folderpath(globals::instance);
|
||||
std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix";
|
||||
|
||||
if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName))
|
||||
{
|
||||
package::RegisterSparsePackage(path, packageUri);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
FileLocksmithRuntimeRegistration::EnsureRegistered();
|
||||
#endif
|
||||
}
|
||||
|
||||
m_enabled = true;
|
||||
}
|
||||
@@ -95,6 +101,13 @@ public:
|
||||
virtual void disable() override
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,73 @@ struct InclusiveCrosshairs
|
||||
void SwitchActivationMode();
|
||||
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:
|
||||
enum class MouseButton
|
||||
{
|
||||
@@ -69,6 +136,7 @@ private:
|
||||
bool m_drawing = false;
|
||||
bool m_destroyed = false;
|
||||
bool m_hiddenCursor = false;
|
||||
bool m_externalControl = false;
|
||||
void SetAutoHideTimer() noexcept;
|
||||
|
||||
// Configurable Settings
|
||||
@@ -264,11 +332,14 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP
|
||||
if (nCode >= 0)
|
||||
{
|
||||
MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
|
||||
if (instance && !instance->m_externalControl)
|
||||
{
|
||||
if (wParam == WM_MOUSEMOVE)
|
||||
{
|
||||
instance->UpdateCrosshairsPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(0, nCode, wParam, lParam);
|
||||
}
|
||||
|
||||
@@ -527,6 +598,26 @@ bool InclusiveCrosshairsIsEnabled()
|
||||
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)
|
||||
{
|
||||
Logger::info("Starting a crosshairs instance.");
|
||||
|
||||
@@ -31,3 +31,7 @@ void InclusiveCrosshairsDisable();
|
||||
bool InclusiveCrosshairsIsEnabled();
|
||||
void InclusiveCrosshairsSwitch();
|
||||
void InclusiveCrosshairsApplySettings(InclusiveCrosshairsSettings& settings);
|
||||
void InclusiveCrosshairsRequestUpdatePosition();
|
||||
void InclusiveCrosshairsEnsureOn();
|
||||
void InclusiveCrosshairsEnsureOff();
|
||||
void InclusiveCrosshairsSetExternalControl(bool enabled);
|
||||
|
||||
@@ -4,6 +4,15 @@
|
||||
#include "trace.h"
|
||||
#include "InclusiveCrosshairs.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
|
||||
namespace
|
||||
@@ -11,6 +20,7 @@ namespace
|
||||
const wchar_t JSON_KEY_PROPERTIES[] = L"properties";
|
||||
const wchar_t JSON_KEY_VALUE[] = L"value";
|
||||
const wchar_t JSON_KEY_ACTIVATION_SHORTCUT[] = L"activation_shortcut";
|
||||
const wchar_t JSON_KEY_GLIDING_ACTIVATION_SHORTCUT[] = L"gliding_cursor_activation_shortcut";
|
||||
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_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_FIXED_LENGTH[] = L"crosshairs_fixed_length";
|
||||
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;
|
||||
|
||||
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;
|
||||
switch (ul_reason_for_call)
|
||||
@@ -57,8 +69,46 @@ private:
|
||||
// The PowerToy state.
|
||||
bool m_enabled = false;
|
||||
|
||||
// Hotkey to invoke the module
|
||||
HotkeyEx m_hotkey;
|
||||
// Additional hotkeys (legacy API) to support multiple shortcuts
|
||||
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
|
||||
InclusiveCrosshairsSettings m_inclusiveCrosshairsSettings;
|
||||
@@ -68,12 +118,17 @@ public:
|
||||
MousePointerCrosshairs()
|
||||
{
|
||||
LoggerHelpers::init_logger(MODULE_NAME, L"ModuleInterface", LogSettings::mousePointerCrosshairsLoggerName);
|
||||
m_state = std::make_shared<State>();
|
||||
init_settings();
|
||||
};
|
||||
|
||||
// Destroy the powertoy and free memory
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -107,9 +162,7 @@ public:
|
||||
|
||||
// Signal from the Settings editor to call a custom action.
|
||||
// This can be used to spawn more complex editors.
|
||||
virtual void call_custom_action(const wchar_t* action) override
|
||||
{
|
||||
}
|
||||
virtual void call_custom_action(const wchar_t* /*action*/) override {}
|
||||
|
||||
// Called by the runner to pass the updated settings values as a serialized JSON.
|
||||
virtual void set_config(const wchar_t* config) override
|
||||
@@ -143,6 +196,9 @@ public:
|
||||
{
|
||||
m_enabled = false;
|
||||
Trace::EnableMousePointerCrosshairs(false);
|
||||
StopXTimer();
|
||||
StopYTimer();
|
||||
m_glideState = 0;
|
||||
InclusiveCrosshairsDisable();
|
||||
}
|
||||
|
||||
@@ -158,15 +214,249 @@ public:
|
||||
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
|
||||
{
|
||||
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.
|
||||
void init_settings()
|
||||
{
|
||||
@@ -192,37 +482,44 @@ public:
|
||||
{
|
||||
try
|
||||
{
|
||||
// Parse HotKey
|
||||
// Parse primary activation HotKey (for centralized hook)
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_ACTIVATION_SHORTCUT);
|
||||
auto hotkey = PowerToysSettings::HotkeyObject::from_json(jsonPropertiesObject);
|
||||
m_hotkey = HotkeyEx();
|
||||
if (hotkey.win_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_WIN;
|
||||
}
|
||||
|
||||
if (hotkey.ctrl_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_CONTROL;
|
||||
}
|
||||
|
||||
if (hotkey.shift_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_SHIFT;
|
||||
}
|
||||
|
||||
if (hotkey.alt_pressed())
|
||||
{
|
||||
m_hotkey.modifiersMask |= MOD_ALT;
|
||||
}
|
||||
|
||||
m_hotkey.vkCode = hotkey.get_code();
|
||||
// Map to legacy Hotkey for multi-hotkey API
|
||||
m_activationHotkey.win = hotkey.win_pressed();
|
||||
m_activationHotkey.ctrl = hotkey.ctrl_pressed();
|
||||
m_activationHotkey.shift = hotkey.shift_pressed();
|
||||
m_activationHotkey.alt = hotkey.alt_pressed();
|
||||
m_activationHotkey.key = static_cast<unsigned char>(hotkey.get_code());
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
Logger::warn("Failed to initialize Mouse Pointer Crosshairs activation shortcut");
|
||||
}
|
||||
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
|
||||
auto jsonPropertiesObject = settingsObject.GetNamedObject(JSON_KEY_PROPERTIES).GetNamedObject(JSON_KEY_CROSSHAIRS_OPACITY);
|
||||
@@ -272,7 +569,6 @@ public:
|
||||
{
|
||||
throw std::runtime_error("Invalid Radius value");
|
||||
}
|
||||
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -291,7 +587,6 @@ public:
|
||||
{
|
||||
throw std::runtime_error("Invalid Thickness value");
|
||||
}
|
||||
|
||||
}
|
||||
catch (...)
|
||||
{
|
||||
@@ -320,7 +615,7 @@ public:
|
||||
{
|
||||
// Parse 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)
|
||||
{
|
||||
inclusiveCrosshairsSettings.crosshairsBorderSize = value;
|
||||
@@ -383,20 +678,86 @@ public:
|
||||
{
|
||||
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
|
||||
{
|
||||
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_hotkey.modifiersMask = MOD_WIN | MOD_ALT;
|
||||
m_hotkey.vkCode = 0x50; // P key
|
||||
m_activationHotkey.win = true;
|
||||
m_activationHotkey.alt = true;
|
||||
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;
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
|
||||
|
||||
@@ -556,6 +556,61 @@ public:
|
||||
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()
|
||||
{
|
||||
Logger::trace(L"Starting Process to add firewall rule");
|
||||
|
||||
@@ -123,6 +123,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv</Command>
|
||||
<ClInclude Include="settings.h" />
|
||||
<ClInclude Include="trace.h" />
|
||||
<ClInclude Include="new_utilities.h" />
|
||||
<ClInclude Include="RuntimeRegistration.h" />
|
||||
<ClInclude Include="resource.base.h" />
|
||||
<ClInclude Include="template_folder.h" />
|
||||
<ClInclude Include="pch.h" />
|
||||
|
||||
@@ -84,6 +84,9 @@
|
||||
<ClInclude Include="helpers_variables.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
<ClInclude Include="RuntimeRegistration.h">
|
||||
<Filter>Header Files</Filter>
|
||||
</ClInclude>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="packages.config" />
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "trace.h"
|
||||
#include "new_utilities.h"
|
||||
#include "Generated Files/resource.h"
|
||||
#include "RuntimeRegistration.h"
|
||||
|
||||
// Note: Settings are managed via Settings and UI Settings
|
||||
class NewModule : public PowertoyModuleIface
|
||||
@@ -93,8 +94,16 @@ public:
|
||||
|
||||
// Log telemetry
|
||||
Trace::EventToggleOnOff(true);
|
||||
|
||||
if (package::IsWin11OrGreater())
|
||||
{
|
||||
newplus::utilities::register_msix_package();
|
||||
}
|
||||
else
|
||||
{
|
||||
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
|
||||
NewPlusRuntimeRegistration::EnsureRegisteredWin10();
|
||||
#endif
|
||||
}
|
||||
|
||||
powertoy_new_enabled = true;
|
||||
}
|
||||
@@ -141,6 +150,13 @@ private:
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -189,7 +189,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||
return new SeparatorViewModel() as IContextItemViewModel;
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
@@ -350,7 +350,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||
return new SeparatorViewModel() as IContextItemViewModel;
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -119,7 +119,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel();
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
@@ -178,7 +178,7 @@ public abstract partial class ContentPageViewModel : PageViewModel, ICommandBarC
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel();
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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 = [];
|
||||
}
|
||||
}
|
||||
@@ -2,12 +2,7 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -26,6 +26,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
[ObservableProperty]
|
||||
public partial ObservableCollection<ListItemViewModel> FilteredItems { get; set; } = [];
|
||||
|
||||
public FiltersViewModel? Filters { get; set; }
|
||||
|
||||
private ObservableCollection<ListItemViewModel> Items { get; set; } = [];
|
||||
|
||||
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?
|
||||
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
|
||||
//// 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)
|
||||
{
|
||||
dynamic.SearchText = filter;
|
||||
dynamic.SearchText = searchTextBox;
|
||||
}
|
||||
}
|
||||
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
|
||||
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
|
||||
/// FilteredItems to match the results.
|
||||
/// </summary>
|
||||
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter));
|
||||
private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox));
|
||||
|
||||
/// <summary>
|
||||
/// 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.SlowInitializeProperties();
|
||||
|
||||
Filters = new(new(model.Filters), PageContext);
|
||||
Filters.InitializeProperties();
|
||||
UpdateProperty(nameof(Filters));
|
||||
|
||||
FetchItems();
|
||||
model.ItemsChanged += Model_ItemsChanged;
|
||||
}
|
||||
@@ -578,6 +604,10 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
EmptyContent = new(new(model.EmptyContent), PageContext);
|
||||
EmptyContent.SlowInitializeProperties();
|
||||
break;
|
||||
case nameof(Filters):
|
||||
Filters = new(new(model.Filters), PageContext);
|
||||
Filters.InitializeProperties();
|
||||
break;
|
||||
case nameof(IsLoading):
|
||||
UpdateEmptyContent();
|
||||
break;
|
||||
@@ -641,6 +671,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
|
||||
FilteredItems.Clear();
|
||||
}
|
||||
|
||||
Filters?.SafeCleanup();
|
||||
|
||||
var model = _model.Unsafe;
|
||||
if (model is not null)
|
||||
{
|
||||
|
||||
@@ -32,7 +32,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
|
||||
// This is set from the SearchBar
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(ShowSuggestion))]
|
||||
public partial string Filter { get; set; } = string.Empty;
|
||||
public partial string SearchTextBox { get; set; } = string.Empty;
|
||||
|
||||
[ObservableProperty]
|
||||
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))]
|
||||
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]
|
||||
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...
|
||||
// subclasses should override.
|
||||
|
||||
@@ -9,6 +9,10 @@ using Microsoft.CommandPalette.Extensions;
|
||||
namespace Microsoft.CmdPal.Core.ViewModels;
|
||||
|
||||
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.All)]
|
||||
public partial class SeparatorContextItemViewModel() : IContextItemViewModel, ISeparatorContextItem
|
||||
public partial class SeparatorViewModel() :
|
||||
IContextItemViewModel,
|
||||
IFilterItemViewModel,
|
||||
ISeparatorContextItem,
|
||||
ISeparatorFilterItem
|
||||
{
|
||||
}
|
||||
@@ -97,14 +97,27 @@ public partial class AliasManager : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
// Look for the old alias, and remove it
|
||||
List<CommandAlias> toRemove = [];
|
||||
foreach (var kv in _aliases)
|
||||
{
|
||||
// Look for the old aliases for the command, and remove it
|
||||
if (kv.Value.CommandId == commandId)
|
||||
{
|
||||
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)
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using System.Text.Json;
|
||||
using AdaptiveCards.ObjectModel.WinUI3;
|
||||
using AdaptiveCards.Templating;
|
||||
@@ -96,92 +97,23 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
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)
|
||||
{
|
||||
// BODGY circa GH #40979
|
||||
// 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))
|
||||
if (action is AdaptiveOpenUrlAction openUrlAction)
|
||||
{
|
||||
var actionTypeString = actionTypeValue.GetString();
|
||||
Logger.LogTrace($"atString={actionTypeString}");
|
||||
|
||||
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);
|
||||
WeakReferenceMessenger.Default.Send<LaunchUriMessage>(new(openUrlAction.Url));
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
case ActionType.Submit:
|
||||
case ActionType.Execute:
|
||||
if (action is AdaptiveSubmitAction or AdaptiveExecuteAction)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
// Get the data and inputs
|
||||
var dataString = (action as AdaptiveSubmitAction)?.DataJson.Stringify() ?? string.Empty;
|
||||
var inputString = inputs.Stringify();
|
||||
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
try
|
||||
@@ -190,7 +122,6 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
if (model != null)
|
||||
{
|
||||
var result = model.SubmitForm(inputString, dataString);
|
||||
Logger.LogDebug($"SubmitForm() returned {result}");
|
||||
WeakReferenceMessenger.Default.Send<HandleCommandResultMessage>(new(new(result)));
|
||||
}
|
||||
}
|
||||
@@ -200,6 +131,7 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference<IPag
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly string ErrorCardJson = """
|
||||
{
|
||||
|
||||
@@ -110,6 +110,8 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
get => Alias?.Alias ?? string.Empty;
|
||||
set
|
||||
{
|
||||
var previousAlias = Alias?.Alias ?? string.Empty;
|
||||
|
||||
if (string.IsNullOrEmpty(value))
|
||||
{
|
||||
Alias = null;
|
||||
@@ -126,11 +128,15 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
}
|
||||
}
|
||||
|
||||
// Only call HandleChangeAlias if there was an actual change.
|
||||
if (previousAlias != Alias?.Alias)
|
||||
{
|
||||
HandleChangeAlias();
|
||||
OnPropertyChanged(nameof(AliasText));
|
||||
OnPropertyChanged(nameof(IsDirectAlias));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsDirectAlias
|
||||
{
|
||||
|
||||
@@ -41,14 +41,18 @@
|
||||
<Style.Setters>
|
||||
<Setter Property="Margin" Value="0" />
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Background" Value="{ThemeResource DesktopAcrylicTransparentBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{ThemeResource DividerStrokeColorDefaultBrush}" />
|
||||
</Style.Setters>
|
||||
</Style>
|
||||
|
||||
<!-- Backdrop requires ShouldConstrainToRootBounds="False" -->
|
||||
<Flyout
|
||||
x:Name="ContextMenuFlyout"
|
||||
FlyoutPresenterStyle="{StaticResource ContextMenuFlyoutStyle}"
|
||||
Opened="ContextMenuFlyout_Opened"
|
||||
ShouldConstrainToRootBounds="False">
|
||||
ShouldConstrainToRootBounds="False"
|
||||
SystemBackdrop="{ThemeResource AcrylicBackgroundFillColorDefaultBackdrop}">
|
||||
<cpcontrols:ContextMenu x:Name="ContextControl" />
|
||||
</Flyout>
|
||||
|
||||
@@ -161,6 +165,7 @@
|
||||
x:Name="PrimaryButton"
|
||||
Padding="6,4,4,4"
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
AutomationProperties.AutomationId="PrimaryCommandButton"
|
||||
AutomationProperties.Name="{x:Bind ViewModel.PrimaryCommand.Name, Mode=OneWay}"
|
||||
Background="Transparent"
|
||||
Click="PrimaryButton_Clicked"
|
||||
@@ -180,6 +185,7 @@
|
||||
x:Name="SecondaryButton"
|
||||
Padding="6,4,4,4"
|
||||
x:Load="{x:Bind IsLoaded, Mode=OneWay}"
|
||||
AutomationProperties.AutomationId="SecondaryCommandButton"
|
||||
AutomationProperties.Name="{x:Bind ViewModel.SecondaryCommand.Name, Mode=OneWay}"
|
||||
Click="SecondaryButton_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
@@ -203,6 +209,7 @@
|
||||
x:Name="MoreCommandsButton"
|
||||
x:Uid="MoreCommandsButton"
|
||||
Padding="6,4,4,4"
|
||||
AutomationProperties.AutomationId="MoreContextMenuButton"
|
||||
Click="MoreCommandsButton_Clicked"
|
||||
Style="{StaticResource SubtleButtonStyle}"
|
||||
ToolTipService.ToolTip="Ctrl+K"
|
||||
|
||||
@@ -108,7 +108,7 @@
|
||||
</DataTemplate>
|
||||
|
||||
<!-- Template for context item separators -->
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorContextItemViewModel">
|
||||
<DataTemplate x:Key="SeparatorContextMenuViewModelTemplate" x:DataType="coreViewModels:SeparatorViewModel">
|
||||
<Rectangle
|
||||
Height="1"
|
||||
Margin="-16,-12,-12,-12"
|
||||
|
||||
@@ -270,7 +270,7 @@ public sealed partial class ContextMenu : UserControl,
|
||||
|
||||
private bool IsSeparator(object item)
|
||||
{
|
||||
return item is SeparatorContextItemViewModel;
|
||||
return item is SeparatorViewModel;
|
||||
}
|
||||
|
||||
private void UpdateUiForStackChange()
|
||||
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@
|
||||
MinHeight="32"
|
||||
VerticalAlignment="Stretch"
|
||||
VerticalContentAlignment="Stretch"
|
||||
AutomationProperties.AutomationId="MainSearchBox"
|
||||
KeyDown="FilterBox_KeyDown"
|
||||
PlaceholderText="{x:Bind CurrentPageViewModel.PlaceholderText, Converter={StaticResource PlaceholderTextConverter}, Mode=OneWay}"
|
||||
PreviewKeyDown="FilterBox_PreviewKeyDown"
|
||||
|
||||
@@ -62,7 +62,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
{
|
||||
// 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.
|
||||
@this.FilterBox.Text = page.Filter;
|
||||
@this.FilterBox.Text = page.SearchTextBox;
|
||||
@this.FilterBox.Select(@this.FilterBox.Text.Length, 0);
|
||||
|
||||
page.PropertyChanged += @this.Page_PropertyChanged;
|
||||
@@ -87,7 +87,7 @@ public sealed partial class SearchBar : UserControl,
|
||||
|
||||
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
|
||||
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
|
||||
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
|
||||
if (CurrentPageViewModel is not null)
|
||||
{
|
||||
CurrentPageViewModel.Filter = FilterBox.Text;
|
||||
CurrentPageViewModel.SearchTextBox = FilterBox.Text;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,7 @@ internal sealed partial class ContextItemTemplateSelector : DataTemplateSelector
|
||||
{
|
||||
li.IsEnabled = true;
|
||||
|
||||
if (item is SeparatorContextItemViewModel)
|
||||
if (item is SeparatorViewModel)
|
||||
{
|
||||
li.IsEnabled = false;
|
||||
li.AllowFocusWhenDisabled = false;
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -92,7 +92,7 @@ internal sealed partial class TrayIconService
|
||||
{
|
||||
_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, 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
|
||||
|
||||
@@ -176,6 +176,7 @@
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Back button -->
|
||||
@@ -320,6 +321,18 @@
|
||||
</TransitionCollection>
|
||||
</Grid.Transitions>
|
||||
</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>
|
||||
|
||||
<ProgressBar
|
||||
|
||||
@@ -419,8 +419,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="TrayMenu_Settings" xml:space="preserve">
|
||||
<value>Settings</value>
|
||||
</data>
|
||||
<data name="TrayMenu_Exit" xml:space="preserve">
|
||||
<value>Exit</value>
|
||||
<data name="TrayMenu_Close" xml:space="preserve">
|
||||
<value>Close</value>
|
||||
<comment>Close as a verb, as in Close the application</comment>
|
||||
</data>
|
||||
<data name="Settings_ExtensionPage_Alias_ToggleSwitch.OnContent" xml:space="preserve">
|
||||
<value>Direct</value>
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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" });
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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 commands:Add + 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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -19,29 +19,22 @@ public class CommandPaletteTestBase : UITestBase
|
||||
{
|
||||
}
|
||||
|
||||
protected void SetSearchBox(string text)
|
||||
{
|
||||
Assert.AreEqual(this.Find<TextBox>("Type here to search...").SetText(text, true).Text, text);
|
||||
}
|
||||
protected void SetSearchBox(string text) => SetSearchBoxText(text);
|
||||
|
||||
protected void SetFilesExtensionSearchBox(string text)
|
||||
{
|
||||
Assert.AreEqual(this.Find<TextBox>("Search for files and folders...").SetText(text, true).Text, text);
|
||||
}
|
||||
protected void SetFilesExtensionSearchBox(string text) => SetSearchBoxText(text);
|
||||
|
||||
protected void SetCalculatorExtensionSearchBox(string text)
|
||||
{
|
||||
Assert.AreEqual(this.Find<TextBox>("Type an equation...").SetText(text, true).Text, text);
|
||||
}
|
||||
protected void SetCalculatorExtensionSearchBox(string text) => SetSearchBoxText(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()
|
||||
{
|
||||
var contextMenuButton = this.Find<Button>("More");
|
||||
var contextMenuButton = this.Find<Button>(By.AccessibilityId("MoreContextMenuButton"));
|
||||
Assert.IsNotNull(contextMenuButton, "Context menu button not found.");
|
||||
contextMenuButton.Click();
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ public class IndexerTests : CommandPaletteTestBase
|
||||
|
||||
searchItem.Click();
|
||||
|
||||
var openButton = this.Find<Button>("Open with");
|
||||
var openButton = this.Find<Button>(By.AccessibilityId("PrimaryCommandButton"));
|
||||
Assert.IsNotNull(openButton);
|
||||
|
||||
openButton.Click();
|
||||
@@ -144,7 +144,7 @@ public class IndexerTests : CommandPaletteTestBase
|
||||
Assert.IsNotNull(searchItem);
|
||||
searchItem.Click();
|
||||
|
||||
var openButton = this.Find<Button>("Browse");
|
||||
var openButton = this.Find<Button>(By.AccessibilityId("SecondaryCommandButton"));
|
||||
Assert.IsNotNull(openButton);
|
||||
|
||||
openButton.Click();
|
||||
|
||||
@@ -19,16 +19,23 @@ public partial class AllAppsCommandProvider : CommandProvider
|
||||
|
||||
public static readonly AllAppsPage Page = new();
|
||||
|
||||
private readonly AllAppsPage _page;
|
||||
private readonly CommandItem _listItem;
|
||||
|
||||
public AllAppsCommandProvider()
|
||||
: this(Page)
|
||||
{
|
||||
}
|
||||
|
||||
public AllAppsCommandProvider(AllAppsPage page)
|
||||
{
|
||||
_page = page ?? throw new ArgumentNullException(nameof(page));
|
||||
Id = WellKnownId;
|
||||
DisplayName = Resources.installed_apps;
|
||||
Icon = Icons.AllAppsIcon;
|
||||
Settings = AllAppsSettings.Instance.Settings;
|
||||
|
||||
_listItem = new(Page)
|
||||
_listItem = new(_page)
|
||||
{
|
||||
Subtitle = Resources.search_installed_apps,
|
||||
MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)],
|
||||
@@ -38,11 +45,11 @@ public partial class AllAppsCommandProvider : CommandProvider
|
||||
PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged;
|
||||
}
|
||||
|
||||
public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()];
|
||||
public override ICommandItem[] TopLevelCommands() => [_listItem, .._page.GetPinnedApps()];
|
||||
|
||||
public ICommandItem? LookupApp(string displayName)
|
||||
{
|
||||
var items = Page.GetItems();
|
||||
var items = _page.GetItems();
|
||||
|
||||
// We're going to do this search in two directions:
|
||||
// First, is this name a substring of any app...
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// 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.Diagnostics;
|
||||
using System.Linq;
|
||||
@@ -19,13 +20,20 @@ namespace Microsoft.CmdPal.Ext.Apps;
|
||||
public sealed partial class AllAppsPage : ListPage
|
||||
{
|
||||
private readonly Lock _listLock = new();
|
||||
private readonly IAppCache _appCache;
|
||||
|
||||
private AppItem[] allApps = [];
|
||||
private AppListItem[] unpinnedApps = [];
|
||||
private AppListItem[] pinnedApps = [];
|
||||
|
||||
public AllAppsPage()
|
||||
: this(AppCache.Instance.Value)
|
||||
{
|
||||
}
|
||||
|
||||
public AllAppsPage(IAppCache appCache)
|
||||
{
|
||||
_appCache = appCache ?? throw new ArgumentNullException(nameof(appCache));
|
||||
this.Name = Resources.all_apps;
|
||||
this.Icon = Icons.AllAppsIcon;
|
||||
this.ShowDetails = true;
|
||||
@@ -59,7 +67,7 @@ public sealed partial class AllAppsPage : ListPage
|
||||
|
||||
private void BuildListItems()
|
||||
{
|
||||
if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload())
|
||||
if (allApps.Length == 0 || _appCache.ShouldReload())
|
||||
{
|
||||
lock (_listLock)
|
||||
{
|
||||
@@ -75,7 +83,7 @@ public sealed partial class AllAppsPage : ListPage
|
||||
|
||||
this.IsLoading = false;
|
||||
|
||||
AppCache.Instance.Value.ResetReloadFlag();
|
||||
_appCache.ResetReloadFlag();
|
||||
|
||||
stopwatch.Stop();
|
||||
Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms");
|
||||
@@ -85,11 +93,11 @@ public sealed partial class AllAppsPage : ListPage
|
||||
|
||||
private AppItem[] GetAllApps()
|
||||
{
|
||||
var uwpResults = AppCache.Instance.Value.UWPs
|
||||
var uwpResults = _appCache.UWPs
|
||||
.Where((application) => application.Enabled)
|
||||
.Select(app => app.ToAppItem());
|
||||
|
||||
var win32Results = AppCache.Instance.Value.Win32s
|
||||
var win32Results = _appCache.Win32s
|
||||
.Where((application) => application.Enabled && application.Valid)
|
||||
.Select(app => app.ToAppItem());
|
||||
|
||||
|
||||
@@ -5,13 +5,14 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using Microsoft.CmdPal.Ext.Apps.Helpers;
|
||||
using Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
using Microsoft.CmdPal.Ext.Apps.Properties;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
public class AllAppsSettings : JsonSettingsManager
|
||||
public class AllAppsSettings : JsonSettingsManager, ISettingsInterface
|
||||
{
|
||||
private static readonly string _namespace = "apps";
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ using Microsoft.CmdPal.Ext.Apps.Utils;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Apps;
|
||||
|
||||
public sealed partial class AppCache : IDisposable
|
||||
public sealed partial class AppCache : IAppCache, IDisposable
|
||||
{
|
||||
private Win32ProgramFileSystemWatchers _win32ProgramRepositoryHelper;
|
||||
|
||||
@@ -24,7 +24,7 @@ public sealed partial class AppCache : IDisposable
|
||||
|
||||
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());
|
||||
|
||||
|
||||
@@ -26,6 +26,11 @@ public sealed partial class AppCommand : InvokableCommand
|
||||
|
||||
Name = Resources.run_command_action;
|
||||
Id = GenerateId();
|
||||
|
||||
if (!string.IsNullOrEmpty(app.IcoPath))
|
||||
{
|
||||
Icon = new(app.IcoPath);
|
||||
}
|
||||
}
|
||||
|
||||
internal static async Task StartApp(string aumid)
|
||||
|
||||
@@ -131,7 +131,7 @@ internal sealed partial class AppListItem : ListItem
|
||||
var newCommands = new List<IContextItem>();
|
||||
newCommands.AddRange(commands);
|
||||
|
||||
newCommands.Add(new SeparatorContextItem());
|
||||
newCommands.Add(new Separator());
|
||||
|
||||
// 0x50 = P
|
||||
// Full key chord would be Ctrl+P
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -22,7 +22,7 @@ using Theme = Microsoft.CmdPal.Ext.Apps.Utils.Theme;
|
||||
namespace Microsoft.CmdPal.Ext.Apps.Programs;
|
||||
|
||||
[Serializable]
|
||||
public class UWPApplication : IProgram
|
||||
public class UWPApplication : IUWPApplication
|
||||
{
|
||||
private static readonly IFileSystem FileSystem = new FileSystem();
|
||||
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 iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty;
|
||||
|
||||
@@ -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")]
|
||||
@@ -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.
|
||||
/// This repository will also monitor for changes to the PackageCatalog and update the repository accordingly
|
||||
/// </summary>
|
||||
internal sealed partial class PackageRepository : ListRepository<UWPApplication>, IProgramRepository
|
||||
internal sealed partial class PackageRepository : ListRepository<IUWPApplication>, IProgramRepository
|
||||
{
|
||||
private readonly IPackageCatalog _packageCatalog;
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -11,34 +11,4 @@ namespace Microsoft.CmdPal.Ext.Bookmarks;
|
||||
public sealed class Bookmarks
|
||||
{
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,20 @@ public partial class BookmarksCommandProvider : CommandProvider
|
||||
|
||||
private readonly AddBookmarkPage _addNewCommand = new(null);
|
||||
|
||||
private readonly IBookmarkDataSource _dataSource;
|
||||
private readonly BookmarkJsonParser _parser;
|
||||
private Bookmarks? _bookmarks;
|
||||
|
||||
public BookmarksCommandProvider()
|
||||
: this(new FileBookmarkDataSource(StateJsonPath()))
|
||||
{
|
||||
}
|
||||
|
||||
internal BookmarksCommandProvider(IBookmarkDataSource dataSource)
|
||||
{
|
||||
_dataSource = dataSource;
|
||||
_parser = new BookmarkJsonParser();
|
||||
|
||||
Id = "Bookmarks";
|
||||
DisplayName = Resources.bookmarks_display_name;
|
||||
Icon = Icons.PinIcon;
|
||||
@@ -49,10 +59,14 @@ public partial class BookmarksCommandProvider : CommandProvider
|
||||
|
||||
private void SaveAndUpdateCommands()
|
||||
{
|
||||
if (_bookmarks is not null)
|
||||
try
|
||||
{
|
||||
var jsonPath = BookmarksCommandProvider.StateJsonPath();
|
||||
Bookmarks.WriteToFile(jsonPath, _bookmarks);
|
||||
var jsonData = _parser.SerializeBookmarks(_bookmarks);
|
||||
_dataSource.SaveBookmarkData(jsonData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to save bookmarks: {ex.Message}");
|
||||
}
|
||||
|
||||
LoadCommands();
|
||||
@@ -82,11 +96,8 @@ public partial class BookmarksCommandProvider : CommandProvider
|
||||
{
|
||||
try
|
||||
{
|
||||
var jsonFile = StateJsonPath();
|
||||
if (File.Exists(jsonFile))
|
||||
{
|
||||
_bookmarks = Bookmarks.ReadFromFile(jsonFile);
|
||||
}
|
||||
var jsonData = _dataSource.GetBookmarkData();
|
||||
_bookmarks = _parser.ParseBookmarks(jsonData);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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")]
|
||||
@@ -14,14 +14,14 @@ namespace Microsoft.CmdPal.Ext.Shell.Commands;
|
||||
|
||||
internal sealed partial class ExecuteItem : InvokableCommand
|
||||
{
|
||||
private readonly SettingsManager _settings;
|
||||
private readonly ISettingsInterface _settings;
|
||||
private readonly RunAsType _runas;
|
||||
|
||||
public string Cmd { get; internal set; } = string.Empty;
|
||||
|
||||
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)
|
||||
{
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user