mirror of
https://github.com/microsoft/PowerToys.git
synced 2026-01-07 12:57:03 +01:00
Compare commits
29 Commits
shawn/test
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
62c83b6c69 | ||
|
|
da36d410e3 | ||
|
|
a50d548a07 | ||
|
|
8c4a3a6944 | ||
|
|
efc68bc0c9 | ||
|
|
bf74bc43d4 | ||
|
|
ca4d811fa1 | ||
|
|
e10b7bd83a | ||
|
|
12537de422 | ||
|
|
76f5fabaa3 | ||
|
|
ea5f347a1a | ||
|
|
e842621036 | ||
|
|
56aa9acfb4 | ||
|
|
da572c6c40 | ||
|
|
69dc1d5e18 | ||
|
|
1a798e03cd | ||
|
|
8cb2e4eaf7 | ||
|
|
2db1dcd10c | ||
|
|
e0a0bbffe5 | ||
|
|
a5fe4b9e2e | ||
|
|
db953bb325 | ||
|
|
75d85f80b9 | ||
|
|
1e517f2721 | ||
|
|
df08d98a81 | ||
|
|
334003c1be | ||
|
|
95dd85520b | ||
|
|
f12e41fcea | ||
|
|
c548fdbcb5 | ||
|
|
95fddb9a5e |
11
.github/actions/spell-check/expect.txt
vendored
11
.github/actions/spell-check/expect.txt
vendored
@@ -32,6 +32,7 @@ AFeature
|
||||
affordances
|
||||
AFX
|
||||
AGGREGATABLE
|
||||
AHK
|
||||
AHybrid
|
||||
akv
|
||||
ALarger
|
||||
@@ -667,6 +668,7 @@ HROW
|
||||
hsb
|
||||
HSCROLL
|
||||
hsi
|
||||
HSpeed
|
||||
HTCLIENT
|
||||
hthumbnail
|
||||
HTOUCHINPUT
|
||||
@@ -1006,6 +1008,7 @@ Mso
|
||||
msrc
|
||||
msstore
|
||||
msvcp
|
||||
MT
|
||||
MTND
|
||||
MULTIPLEUSE
|
||||
multizone
|
||||
@@ -1116,6 +1119,7 @@ NOTSRCCOPY
|
||||
NOTSRCERASE
|
||||
notwindows
|
||||
NOTXORPEN
|
||||
nowarn
|
||||
NOZORDER
|
||||
NPH
|
||||
npmjs
|
||||
@@ -1862,6 +1866,7 @@ VSINSTALLDIR
|
||||
VSM
|
||||
vso
|
||||
vsonline
|
||||
VSpeed
|
||||
vstemplate
|
||||
vstest
|
||||
VSTHRD
|
||||
@@ -1998,10 +2003,13 @@ XNamespace
|
||||
Xoshiro
|
||||
XPels
|
||||
XPixel
|
||||
XPos
|
||||
XResource
|
||||
xsi
|
||||
XSpeed
|
||||
XStr
|
||||
xstyler
|
||||
XTimer
|
||||
XUP
|
||||
XVIRTUALSCREEN
|
||||
xxxxxx
|
||||
@@ -2011,7 +2019,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" />
|
||||
@@ -113,4 +114,4 @@
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Client" Version="2.4.17140001" />
|
||||
<PackageVersion Include="Microsoft.VariantAssignment.Contract" Version="3.0.16990001" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
|
||||
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 9.0.8
|
||||
- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
|
||||
- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
|
||||
- Microsoft.Data.Sqlite 9.0.8
|
||||
- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
|
||||
- Microsoft.DotNet.ILCompiler (A)
|
||||
- Microsoft.Extensions.DependencyInjection 9.0.8
|
||||
- Microsoft.Extensions.Hosting 9.0.8
|
||||
- Microsoft.Extensions.Hosting.WindowsServices 9.0.8
|
||||
- Microsoft.Extensions.Logging 9.0.8
|
||||
- Microsoft.Extensions.Logging.Abstractions 9.0.8
|
||||
- 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 9.0.8
|
||||
- Microsoft.Windows.Compatibility 9.0.8
|
||||
- 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 9.0.8
|
||||
- System.CommandLine 2.0.0-beta4.22272.1
|
||||
- System.ComponentModel.Composition 9.0.8
|
||||
- System.Configuration.ConfigurationManager 9.0.8
|
||||
- System.Data.OleDb 9.0.8
|
||||
- System.Data.SqlClient 4.9.0
|
||||
- System.Diagnostics.EventLog 9.0.8
|
||||
- System.Diagnostics.PerformanceCounter 9.0.8
|
||||
- System.Drawing.Common 9.0.8
|
||||
- System.IO.Abstractions 22.0.13
|
||||
- System.IO.Abstractions.TestingHelpers 22.0.13
|
||||
- System.Management 9.0.8
|
||||
- System.Net.Http 4.3.4
|
||||
- System.Private.Uri 4.3.2
|
||||
- System.Reactive 6.0.1
|
||||
- System.Runtime.Caching 9.0.8
|
||||
- System.ServiceProcess.ServiceController 9.0.8
|
||||
- System.Text.Encoding.CodePages 9.0.8
|
||||
- System.Text.Json 9.0.8
|
||||
- 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
|
||||
@@ -792,6 +793,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.Apps.U
|
||||
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
|
||||
@@ -2870,6 +2875,22 @@ Global
|
||||
{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
|
||||
@@ -3183,6 +3204,8 @@ Global
|
||||
{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}
|
||||
|
||||
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,8 +27,7 @@
|
||||
</RegistryKey>
|
||||
<RemoveFolder Id="RemoveFolderNewPlusAssetsFolder" Directory="NewPlusAssetsInstallFolder" On="uninstall"/>
|
||||
</Component>
|
||||
<ComponentRef Id="NewPlus_ShellExtension_win10" />
|
||||
</ComponentGroup>
|
||||
</ComponentGroup>
|
||||
|
||||
|
||||
<!-- Example templates -->
|
||||
@@ -81,7 +67,7 @@
|
||||
</Component>
|
||||
<ComponentRef Id="NewPlusTemplateFiles_Component" />
|
||||
<ComponentRef Id="NewPlusTemplateSubFiles_Component" />
|
||||
</ComponentGroup>
|
||||
</ComponentGroup>
|
||||
|
||||
</Fragment>
|
||||
</Wix>
|
||||
|
||||
@@ -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>
|
||||
@@ -437,6 +449,35 @@
|
||||
Execute="deferred"
|
||||
BinaryKey="PTCustomActions"
|
||||
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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -7,7 +7,7 @@ import { srtDefinition } from './customLanguages/srt.js';
|
||||
export async function registerAdditionalLanguages(monaco){
|
||||
await languageDefinitions();
|
||||
registerAdditionalLanguage("cppExt", [".ino", ".pde"], "cpp", monaco);
|
||||
registerAdditionalLanguage("xmlExt", [".wsdl", ".csproj", ".vcxproj", ".vbproj", ".fsproj", ".resx", ".resw"], "xml", monaco);
|
||||
registerAdditionalLanguage("xmlExt", [".wsdl", ".projitems", ".csproj", ".fsproj", ".shproj", ".vcxproj", ".vbproj", ".resx", ".resw"], "xml", monaco);
|
||||
registerAdditionalLanguage("txtExt", [".sln", ".log", ".vsconfig", ".env", ".ahk", ".ion"], "txt", monaco);
|
||||
registerAdditionalLanguage("razorExt", [".razor"], "razor", monaco);
|
||||
registerAdditionalLanguage("vbExt", [".vbs"], "vb", monaco);
|
||||
|
||||
File diff suppressed because one or more lines are too long
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;
|
||||
}
|
||||
}
|
||||
@@ -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,9 +332,12 @@ LRESULT CALLBACK InclusiveCrosshairs::MouseHookProc(int nCode, WPARAM wParam, LP
|
||||
if (nCode >= 0)
|
||||
{
|
||||
MSLLHOOKSTRUCT* hookData = reinterpret_cast<MSLLHOOKSTRUCT*>(lParam);
|
||||
if (wParam == WM_MOUSEMOVE)
|
||||
if (instance && !instance->m_externalControl)
|
||||
{
|
||||
instance->UpdateCrosshairsPosition();
|
||||
if (wParam == WM_MOUSEMOVE)
|
||||
{
|
||||
instance->UpdateCrosshairsPosition();
|
||||
}
|
||||
}
|
||||
}
|
||||
return CallNextHookEx(0, nCode, wParam, lParam);
|
||||
@@ -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
|
||||
{
|
||||
InclusiveCrosshairsSwitch();
|
||||
if (!m_enabled)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (hotkeyId == 0)
|
||||
{
|
||||
InclusiveCrosshairsSwitch();
|
||||
return true;
|
||||
}
|
||||
if (hotkeyId == 1)
|
||||
{
|
||||
HandleGlidingHotkey();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
private:
|
||||
static void LeftClick()
|
||||
{
|
||||
INPUT inputs[2]{};
|
||||
inputs[0].type = INPUT_MOUSE;
|
||||
inputs[0].mi.dwFlags = MOUSEEVENTF_LEFTDOWN;
|
||||
inputs[1].type = INPUT_MOUSE;
|
||||
inputs[1].mi.dwFlags = MOUSEEVENTF_LEFTUP;
|
||||
SendInput(2, inputs, sizeof(INPUT));
|
||||
}
|
||||
|
||||
// Stateless helpers operating on shared State
|
||||
static void PositionCursorX(const std::shared_ptr<State>& s)
|
||||
{
|
||||
int screenW = GetSystemMetrics(SM_CXVIRTUALSCREEN);
|
||||
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
s->currentYPos = screenH / 2;
|
||||
|
||||
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
|
||||
const double perTick = (static_cast<double>(s->currentXSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->xFraction += perTick;
|
||||
int step = static_cast<int>(s->xFraction);
|
||||
if (step > 0)
|
||||
{
|
||||
s->xFraction -= step;
|
||||
s->currentXPos += step;
|
||||
}
|
||||
|
||||
s->xPosSnapshot = s->currentXPos;
|
||||
if (s->currentXPos >= screenW)
|
||||
{
|
||||
s->currentXPos = 0;
|
||||
s->currentXSpeed = s->fastHSpeed;
|
||||
s->xPosSnapshot = 0;
|
||||
s->xFraction = 0.0; // reset fractional remainder on wrap
|
||||
}
|
||||
SetCursorPos(s->currentXPos, s->currentYPos);
|
||||
// Ensure overlay crosshairs follow immediately
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
}
|
||||
|
||||
static void PositionCursorY(const std::shared_ptr<State>& s)
|
||||
{
|
||||
int screenH = GetSystemMetrics(SM_CYVIRTUALSCREEN);
|
||||
// Keep X at snapshot
|
||||
// Use s->xPosSnapshot captured during X pass
|
||||
|
||||
// Distribute movement over 10ms ticks to match pixels-per-base-window speeds
|
||||
const double perTick = (static_cast<double>(s->currentYSpeed) * kTimerTickMs) / static_cast<double>(kBaseSpeedTickMs);
|
||||
s->yFraction += perTick;
|
||||
int step = static_cast<int>(s->yFraction);
|
||||
if (step > 0)
|
||||
{
|
||||
s->yFraction -= step;
|
||||
s->currentYPos += step;
|
||||
}
|
||||
|
||||
if (s->currentYPos >= screenH)
|
||||
{
|
||||
s->currentYPos = 0;
|
||||
s->currentYSpeed = s->fastVSpeed;
|
||||
s->yFraction = 0.0; // reset fractional remainder on wrap
|
||||
}
|
||||
SetCursorPos(s->xPosSnapshot, s->currentYPos);
|
||||
// Ensure overlay crosshairs follow immediately
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
}
|
||||
|
||||
void StartXTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s->stopX = false;
|
||||
std::weak_ptr<State> wp = s;
|
||||
m_xThread = std::thread([wp]() {
|
||||
while (true)
|
||||
{
|
||||
auto sp = wp.lock();
|
||||
if (!sp || sp->stopX.load())
|
||||
{
|
||||
break;
|
||||
}
|
||||
PositionCursorX(sp);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StopXTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (s)
|
||||
{
|
||||
s->stopX = true;
|
||||
}
|
||||
if (m_xThread.joinable())
|
||||
{
|
||||
m_xThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void StartYTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
s->stopY = false;
|
||||
std::weak_ptr<State> wp = s;
|
||||
m_yThread = std::thread([wp]() {
|
||||
while (true)
|
||||
{
|
||||
auto sp = wp.lock();
|
||||
if (!sp || sp->stopY.load())
|
||||
{
|
||||
break;
|
||||
}
|
||||
PositionCursorY(sp);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(kTimerTickMs));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void StopYTimer()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (s)
|
||||
{
|
||||
s->stopY = true;
|
||||
}
|
||||
if (m_yThread.joinable())
|
||||
{
|
||||
m_yThread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void HandleGlidingHotkey()
|
||||
{
|
||||
auto s = m_state;
|
||||
if (!s)
|
||||
{
|
||||
return;
|
||||
}
|
||||
// Simulate the AHK state machine
|
||||
int state = m_glideState.load();
|
||||
switch (state)
|
||||
{
|
||||
case 0:
|
||||
{
|
||||
// Ensure crosshairs on (do not toggle off if already on)
|
||||
InclusiveCrosshairsEnsureOn();
|
||||
// Disable internal mouse hook so we control position updates explicitly
|
||||
InclusiveCrosshairsSetExternalControl(true);
|
||||
|
||||
s->currentXPos = 0;
|
||||
s->currentXSpeed = s->fastHSpeed;
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
int y = GetSystemMetrics(SM_CYVIRTUALSCREEN) / 2;
|
||||
SetCursorPos(0, y);
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
m_glideState = 1;
|
||||
StartXTimer();
|
||||
break;
|
||||
}
|
||||
case 1:
|
||||
{
|
||||
// Slow horizontal
|
||||
s->currentXSpeed = s->slowHSpeed;
|
||||
m_glideState = 2;
|
||||
break;
|
||||
}
|
||||
case 2:
|
||||
{
|
||||
// Stop horizontal, start vertical (fast)
|
||||
StopXTimer();
|
||||
s->currentYSpeed = s->fastVSpeed;
|
||||
s->currentYPos = 0;
|
||||
s->yFraction = 0.0;
|
||||
SetCursorPos(s->xPosSnapshot, s->currentYPos);
|
||||
InclusiveCrosshairsRequestUpdatePosition();
|
||||
m_glideState = 3;
|
||||
StartYTimer();
|
||||
break;
|
||||
}
|
||||
case 3:
|
||||
{
|
||||
// Slow vertical
|
||||
s->currentYSpeed = s->slowVSpeed;
|
||||
m_glideState = 4;
|
||||
break;
|
||||
}
|
||||
case 4:
|
||||
default:
|
||||
{
|
||||
// Stop vertical, click, turn crosshairs off, re-enable internal tracking, reset state
|
||||
StopYTimer();
|
||||
m_glideState = 0;
|
||||
LeftClick();
|
||||
InclusiveCrosshairsEnsureOff();
|
||||
InclusiveCrosshairsSetExternalControl(false);
|
||||
s->xFraction = 0.0;
|
||||
s->yFraction = 0.0;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Load the settings file.
|
||||
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()
|
||||
|
||||
@@ -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);
|
||||
|
||||
newplus::utilities::register_msix_package();
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -246,9 +246,17 @@ LRESULT WindowBorder::WndProc(UINT message, WPARAM wparam, LPARAM lparam) noexce
|
||||
case WM_ERASEBKGND:
|
||||
return TRUE;
|
||||
|
||||
// prevent from beeping if the border was clicked
|
||||
// Prevent from beeping if the border was clicked
|
||||
case WM_SETCURSOR:
|
||||
{
|
||||
HCURSOR hCursor = LoadCursorW(nullptr, IDC_ARROW);
|
||||
if (hCursor)
|
||||
{
|
||||
SetCursor(hCursor);
|
||||
}
|
||||
|
||||
return TRUE;
|
||||
}
|
||||
|
||||
default:
|
||||
{
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -181,15 +182,15 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
if (more is not null)
|
||||
{
|
||||
MoreCommands = more
|
||||
.Select(item =>
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||
return new CommandContextItemViewModel(contextItem, PageContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
@@ -237,8 +238,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
FastInitializeProperties();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("error fast initializing CommandItemViewModel", ex);
|
||||
Command = new(null, PageContext);
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
@@ -257,9 +259,10 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
SlowInitializeProperties();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Initialized |= InitializedState.Error;
|
||||
Logger.LogError("error slow initializing CommandItemViewModel", ex);
|
||||
}
|
||||
|
||||
return false;
|
||||
@@ -272,8 +275,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
InitializeProperties();
|
||||
return true;
|
||||
}
|
||||
catch (Exception)
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("error initializing CommandItemViewModel", ex);
|
||||
Command = new(null, PageContext);
|
||||
_itemTitle = "Error";
|
||||
Subtitle = "Item failed to load";
|
||||
@@ -342,15 +346,15 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
|
||||
if (more is not null)
|
||||
{
|
||||
var newContextMenu = more
|
||||
.Select(item =>
|
||||
.Select<IContextItem, IContextItemViewModel>(item =>
|
||||
{
|
||||
if (item is ICommandContextItem contextItem)
|
||||
{
|
||||
return new CommandContextItemViewModel(contextItem, PageContext) as IContextItemViewModel;
|
||||
return new CommandContextItemViewModel(contextItem, PageContext);
|
||||
}
|
||||
else
|
||||
{
|
||||
return new SeparatorContextItemViewModel() as IContextItemViewModel;
|
||||
return new SeparatorViewModel();
|
||||
}
|
||||
})
|
||||
.ToList();
|
||||
|
||||
@@ -29,6 +29,15 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
|
||||
public IconInfoViewModel Icon { get; private set; }
|
||||
|
||||
// UNDER NO CIRCUMSTANCES MAY SOMEONE WRITE TO THIS DICTIONARY.
|
||||
// This is our copy of the data from the extension.
|
||||
// Adding values to it does not add to the extension.
|
||||
// Modifying it will not modify the extension
|
||||
// (except it might, if the dictionary was passed by ref)
|
||||
private Dictionary<string, ExtensionObject<object>>? _properties;
|
||||
|
||||
public IReadOnlyDictionary<string, ExtensionObject<object>>? Properties => _properties?.AsReadOnly();
|
||||
|
||||
public CommandViewModel(ICommand? command, WeakReference<IPageContext> pageContext)
|
||||
: base(pageContext)
|
||||
{
|
||||
@@ -80,6 +89,11 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
UpdateProperty(nameof(Icon));
|
||||
}
|
||||
|
||||
if (model is IExtendedAttributesProvider command2)
|
||||
{
|
||||
UpdatePropertiesFromExtension(command2);
|
||||
}
|
||||
|
||||
model.PropChanged += Model_PropChanged;
|
||||
}
|
||||
|
||||
@@ -130,4 +144,26 @@ public partial class CommandViewModel : ExtensionObjectViewModel
|
||||
model.PropChanged -= Model_PropChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdatePropertiesFromExtension(IExtendedAttributesProvider? model)
|
||||
{
|
||||
var propertiesFromExtension = model?.GetProperties();
|
||||
if (propertiesFromExtension == null)
|
||||
{
|
||||
_properties = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_properties = [];
|
||||
|
||||
// COPY the properties into us.
|
||||
// The IDictionary that was passed to us may be marshalled by-ref or by-value, we _don't know_.
|
||||
//
|
||||
// If it's by-ref, the values are arbitrary objects that are out-of-proc.
|
||||
// If it's bu-value, then everything is in-proc, and we can't mutate the data.
|
||||
foreach (var property in propertiesFromExtension)
|
||||
{
|
||||
_properties.Add(property.Key, new(property.Value));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -27,6 +27,8 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
|
||||
IRandomAccessStreamReference? IIconData.Data => Data.Unsafe;
|
||||
|
||||
public string? FontFamily { get; private set; }
|
||||
|
||||
public IconDataViewModel(IIconData? icon)
|
||||
{
|
||||
_model = new(icon);
|
||||
@@ -43,5 +45,22 @@ public partial class IconDataViewModel : ObservableObject, IIconData
|
||||
|
||||
Icon = model.Icon;
|
||||
Data = new(model.Data);
|
||||
|
||||
if (model is IExtendedAttributesProvider icon2)
|
||||
{
|
||||
var props = icon2.GetProperties();
|
||||
|
||||
// From Raymond Chen:
|
||||
// Make sure you don't try do do something like
|
||||
// icon2.GetProperties().TryGetValue("awesomeKey", out var awesomeValue);
|
||||
// icon2.GetProperties().TryGetValue("slackerKey", out var slackerValue);
|
||||
// because each call to GetProperties() is a cross process hop, and if you
|
||||
// marshal-by-value the property set, then you don't want to throw it away and
|
||||
// re-marshal it for every property. MAKE SURE YOU CACHE IT.
|
||||
if (props?.TryGetValue("FontFamily", out var family) ?? false)
|
||||
{
|
||||
FontFamily = family as string;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -153,6 +153,11 @@ public sealed class CommandProviderWrapper
|
||||
// On a BG thread here
|
||||
fallbacks = model.FallbackCommands();
|
||||
|
||||
if (model is ICommandProvider2 two)
|
||||
{
|
||||
UnsafePreCacheApiAdditions(two);
|
||||
}
|
||||
|
||||
Id = model.Id;
|
||||
DisplayName = model.DisplayName;
|
||||
Icon = new(model.Icon);
|
||||
@@ -203,6 +208,19 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
private void UnsafePreCacheApiAdditions(ICommandProvider2 provider)
|
||||
{
|
||||
var apiExtensions = provider.GetApiExtensionStubs();
|
||||
Logger.LogDebug($"Provider supports {apiExtensions.Length} extensions");
|
||||
foreach (var a in apiExtensions)
|
||||
{
|
||||
if (a is IExtendedAttributesProvider command2)
|
||||
{
|
||||
Logger.LogDebug($"{ProviderId}: Found an IExtendedAttributesProvider");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
|
||||
|
||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||
|
||||
@@ -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,9 +128,13 @@ public sealed partial class TopLevelViewModel : ObservableObject, IListItem
|
||||
}
|
||||
}
|
||||
|
||||
HandleChangeAlias();
|
||||
OnPropertyChanged(nameof(AliasText));
|
||||
OnPropertyChanged(nameof(IsDirectAlias));
|
||||
// Only call HandleChangeAlias if there was an actual change.
|
||||
if (previousAlias != Alias?.Alias)
|
||||
{
|
||||
HandleChangeAlias();
|
||||
OnPropertyChanged(nameof(AliasText));
|
||||
OnPropertyChanged(nameof(IsDirectAlias));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -165,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"
|
||||
@@ -184,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}"
|
||||
@@ -207,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;
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,7 @@ using CommunityToolkit.Mvvm.Messaging;
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.Core.ViewModels;
|
||||
using Microsoft.CmdPal.Core.ViewModels.Messages;
|
||||
using Microsoft.CmdPal.UI.Helpers;
|
||||
using Microsoft.CmdPal.UI.Messages;
|
||||
using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -162,11 +163,11 @@ public sealed partial class ListPage : Page,
|
||||
if (listViewPeer is not null && li is not null)
|
||||
{
|
||||
var notificationText = li.Title;
|
||||
listViewPeer.RaiseNotificationEvent(
|
||||
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationKind.Other,
|
||||
Microsoft.UI.Xaml.Automation.Peers.AutomationNotificationProcessing.MostRecent,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ItemsList,
|
||||
notificationText,
|
||||
"CommandPaletteSelectedItemChanged");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(icon.Icon))
|
||||
{
|
||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false);
|
||||
var source = IconPathConverter.IconSourceMUX(icon.Icon, false, icon.FontFamily);
|
||||
return source;
|
||||
}
|
||||
else if (icon.Data is not null)
|
||||
|
||||
@@ -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
|
||||
|
||||
32
src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/UIHelper.cs
Normal file
32
src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/UIHelper.cs
Normal file
@@ -0,0 +1,32 @@
|
||||
// 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.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.Helpers;
|
||||
|
||||
public static partial class UIHelper
|
||||
{
|
||||
static UIHelper()
|
||||
{
|
||||
}
|
||||
|
||||
public static void AnnounceActionForAccessibility(UIElement ue, string announcement, string activityID)
|
||||
{
|
||||
if (FrameworkElementAutomationPeer.FromElement(ue) is AutomationPeer peer)
|
||||
{
|
||||
peer.RaiseNotificationEvent(
|
||||
AutomationNotificationKind.ActionCompleted,
|
||||
AutomationNotificationProcessing.ImportantMostRecent,
|
||||
announcement,
|
||||
activityID);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,12 @@
|
||||
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
|
||||
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- For debugging purposes, uncomment this block to enable AOT builds -->
|
||||
<!-- <PropertyGroup>
|
||||
<EnableCmdPalAOT>true</EnableCmdPalAOT>
|
||||
<CIBuild>true</CIBuild>
|
||||
</PropertyGroup> -->
|
||||
|
||||
<PropertyGroup Condition="'$(EnableCmdPalAOT)' == 'true'">
|
||||
<SelfContained>true</SelfContained>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -25,17 +25,11 @@
|
||||
<RowDefinition Height="*" />
|
||||
</Grid.RowDefinitions>
|
||||
<!-- TO DO: Replace this with WinUI TitleBar once that ships. -->
|
||||
<Button
|
||||
x:Name="PaneToggleBtn"
|
||||
Width="48"
|
||||
HorizontalAlignment="Left"
|
||||
VerticalAlignment="Center"
|
||||
Click="PaneToggleBtn_Click"
|
||||
Style="{StaticResource PaneToggleButtonStyle}" />
|
||||
<StackPanel
|
||||
x:Name="AppTitleBar"
|
||||
Grid.Row="0"
|
||||
Height="48"
|
||||
Margin="16,0,0,0"
|
||||
Orientation="Horizontal">
|
||||
<Image
|
||||
Width="16"
|
||||
|
||||
@@ -10,6 +10,7 @@ using Microsoft.CmdPal.UI.ViewModels;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Messages;
|
||||
using Microsoft.UI.Windowing;
|
||||
using Microsoft.UI.Xaml;
|
||||
using Microsoft.UI.Xaml.Automation.Peers;
|
||||
using Microsoft.UI.Xaml.Controls;
|
||||
using WinUIEx;
|
||||
using RS_ = Microsoft.CmdPal.UI.Helpers.ResourceLoaderInstance;
|
||||
@@ -22,6 +23,9 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
{
|
||||
public ObservableCollection<Crumb> BreadCrumbs { get; } = [];
|
||||
|
||||
// Gets or sets optional action invoked after NavigationView is loaded.
|
||||
public Action NavigationViewLoaded { get; set; } = () => { };
|
||||
|
||||
public SettingsWindow()
|
||||
{
|
||||
this.InitializeComponent();
|
||||
@@ -35,10 +39,33 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.Register<QuitMessage>(this);
|
||||
}
|
||||
|
||||
// Handles NavigationView loaded event.
|
||||
// Sets up initial navigation and accessibility notifications.
|
||||
private void NavView_Loaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// Delay necessary to ensure NavigationView visual state can match navigation
|
||||
Task.Delay(500).ContinueWith(_ => this.NavigationViewLoaded?.Invoke(), TaskScheduler.FromCurrentSynchronizationContext());
|
||||
|
||||
NavView.SelectedItem = NavView.MenuItems[0];
|
||||
Navigate("General");
|
||||
|
||||
if (sender is NavigationView navigationView)
|
||||
{
|
||||
// Register for pane open/close changes to announce to screen readers
|
||||
navigationView.RegisterPropertyChangedCallback(NavigationView.IsPaneOpenProperty, AnnounceNavigationPaneStateChanged);
|
||||
}
|
||||
}
|
||||
|
||||
// Announces navigation pane open/close state to screen readers for accessibility.
|
||||
private void AnnounceNavigationPaneStateChanged(DependencyObject sender, DependencyProperty dp)
|
||||
{
|
||||
if (sender is NavigationView navigationView)
|
||||
{
|
||||
UIHelper.AnnounceActionForAccessibility(
|
||||
ue: (UIElement)sender,
|
||||
(sender as NavigationView)?.IsPaneOpen == true ? RS_.GetString("NavigationPaneOpened") : RS_.GetString("NavigationPaneClosed"),
|
||||
"NavigationViewPaneIsOpenChangeNotificationId");
|
||||
}
|
||||
}
|
||||
|
||||
private void NavView_ItemInvoked(NavigationView sender, NavigationViewItemInvokedEventArgs args)
|
||||
@@ -109,24 +136,15 @@ public sealed partial class SettingsWindow : WindowEx,
|
||||
WeakReferenceMessenger.Default.UnregisterAll(this);
|
||||
}
|
||||
|
||||
private void PaneToggleBtn_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
NavView.IsPaneOpen = !NavView.IsPaneOpen;
|
||||
}
|
||||
|
||||
private void NavView_DisplayModeChanged(NavigationView sender, NavigationViewDisplayModeChangedEventArgs args)
|
||||
{
|
||||
if (args.DisplayMode == NavigationViewDisplayMode.Compact || args.DisplayMode == NavigationViewDisplayMode.Minimal)
|
||||
{
|
||||
PaneToggleBtn.Visibility = Visibility.Visible;
|
||||
NavView.IsPaneToggleButtonVisible = false;
|
||||
AppTitleBar.Margin = new Thickness(48, 0, 0, 0);
|
||||
}
|
||||
else
|
||||
{
|
||||
PaneToggleBtn.Visibility = Visibility.Collapsed;
|
||||
NavView.IsPaneToggleButtonVisible = true;
|
||||
AppTitleBar.Margin = new Thickness(16, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -434,4 +435,10 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
|
||||
<data name="StatusMessagesButton.[using:Microsoft.UI.Xaml.Controls]ToolTipService.ToolTip" xml:space="preserve">
|
||||
<value>Show status messages</value>
|
||||
</data>
|
||||
<data name="NavigationPaneClosed" xml:space="preserve">
|
||||
<value>Navigation pane closed</value>
|
||||
</data>
|
||||
<data name="NavigationPageOpened" xml:space="preserve">
|
||||
<value>Navigation page opened</value>
|
||||
</data>
|
||||
</root>
|
||||
@@ -158,7 +158,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
// Return Value:
|
||||
// - An IconElement with its IconSource set, if possible.
|
||||
template<typename TIconSource>
|
||||
TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const int targetSize)
|
||||
TIconSource _getIconSource(const winrt::hstring& iconPath, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
TIconSource iconSource{ nullptr };
|
||||
|
||||
@@ -187,6 +187,11 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
{
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ L"Segoe Fluent Icons, Segoe MDL2 Assets" });
|
||||
}
|
||||
else if (!fontFamily.empty())
|
||||
{
|
||||
icon.FontFamily(winrt::Microsoft::UI::Xaml::Media::FontFamily{ fontFamily });
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
// Note: you _do_ need to manually set the font here.
|
||||
@@ -225,9 +230,9 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
// return _getIconSource<Windows::UI::Xaml::Controls::IconSource>(path, false);
|
||||
// }
|
||||
|
||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const int targetSize)
|
||||
static Microsoft::UI::Xaml::Controls::IconSource _IconSourceMUX(const hstring& path, bool monochrome, const winrt::hstring& fontFamily, const int targetSize)
|
||||
{
|
||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, targetSize);
|
||||
return _getIconSource<Microsoft::UI::Xaml::Controls::IconSource>(path, monochrome, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
static SoftwareBitmap _convertToSoftwareBitmap(HICON hicon,
|
||||
@@ -343,13 +348,14 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
|
||||
MUX::Controls::IconSource IconPathConverter::IconSourceMUX(const winrt::hstring& iconPath,
|
||||
const bool monochrome,
|
||||
const winrt::hstring& fontFamily,
|
||||
const int targetSize)
|
||||
{
|
||||
std::wstring_view iconPathWithoutIndex;
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
return _IconSourceMUX(iconPath, monochrome, targetSize);
|
||||
return _IconSourceMUX(iconPath, monochrome, fontFamily, targetSize);
|
||||
}
|
||||
|
||||
const auto bitmapSource = _getImageIconSourceForBinary(iconPathWithoutIndex, indexOpt.value(), targetSize);
|
||||
@@ -369,7 +375,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
const auto indexOpt = _getIconIndex(iconPath, iconPathWithoutIndex);
|
||||
if (!indexOpt.has_value())
|
||||
{
|
||||
auto source = IconSourceMUX(iconPath, false, targetSize);
|
||||
auto source = IconSourceMUX(iconPath, false, L"", targetSize);
|
||||
Microsoft::UI::Xaml::Controls::IconSourceElement icon;
|
||||
icon.IconSource(source);
|
||||
return icon;
|
||||
|
||||
@@ -10,7 +10,7 @@ namespace winrt::Microsoft::Terminal::UI::implementation
|
||||
|
||||
//static Windows::UI::Xaml::Controls::IconElement IconWUX(const winrt::hstring& iconPath);
|
||||
//static Windows::UI::Xaml::Controls::IconSource IconSourceWUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const int targetSize=24);
|
||||
static Microsoft::UI::Xaml::Controls::IconSource IconSourceMUX(const winrt::hstring& iconPath, bool convertToGrayscale, const winrt::hstring& fontFamily, const int targetSize=24);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath);
|
||||
static Microsoft::UI::Xaml::Controls::IconElement IconMUX(const winrt::hstring& iconPath, const int targetSize);
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ namespace Microsoft.Terminal.UI
|
||||
{
|
||||
// static Windows.UI.Xaml.Controls.IconElement IconWUX(String path);
|
||||
// static Windows.UI.Xaml.Controls.IconSource IconSourceWUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale);
|
||||
static Microsoft.UI.Xaml.Controls.IconSource IconSourceMUX(String path, Boolean convertToGrayscale, String fontFamily);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path);
|
||||
static Microsoft.UI.Xaml.Controls.IconElement IconMUX(String path, Int32 targetSize);
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
author: Mike Griese
|
||||
created on: 2024-07-19
|
||||
last updated: 2025-03-10
|
||||
last updated: 2025-08-08
|
||||
issue id: n/a
|
||||
---
|
||||
|
||||
@@ -1410,8 +1410,8 @@ interface IDetailsLink requires IDetailsData {
|
||||
Windows.Foundation.Uri Link { get; };
|
||||
String Text { get; };
|
||||
}
|
||||
interface IDetailsCommand requires IDetailsData {
|
||||
ICommand Command { get; };
|
||||
interface IDetailsCommands requires IDetailsData {
|
||||
ICommand[] Commands { get; };
|
||||
}
|
||||
[uuid("58070392-02bb-4e89-9beb-47ceb8c3d741")]
|
||||
interface IDetailsSeparator requires IDetailsData {}
|
||||
@@ -1936,6 +1936,115 @@ When displaying a page:
|
||||
* The title will be `IPage.Title ?? ICommand.Name`
|
||||
* The icon will be `ICommand.Icon`
|
||||
|
||||
## Addenda I: API additions (ICommandProvider2)
|
||||
|
||||
In experiments with extending our API, we've found some quirks with the way
|
||||
that we use WinRT's metadata-based marshalling (MBM). Typically, you'd add
|
||||
another contract version, add the new runtimeclass under the new contract
|
||||
version, and then have the client app just check if that contract is available.
|
||||
|
||||
However, we're not using `runtimeclass`es that are exposed from the extensions.
|
||||
Everything is being transferred over MBM, based on the
|
||||
`Microsoft.CommandPalette.Extensions.winmd`. And out-of-proc MBM has some
|
||||
limitations. You can essentially only have a linear chain of requires for
|
||||
extension interfaces.
|
||||
|
||||
> E.g. if it implements `IWidget2` and `IWidget2 requires IWidget`, and the object's `GetRuntimeClassName` gives `IWidget2`, we know to look at `IWidget2` directly and `IWidget` due to requires.
|
||||
>
|
||||
> The unfortunate thing for the developer experience when authoring an extension with cppwinrt/CsWinRT implementations of interfaces, is they implement each interface separately. So the `IInspectable::GetRuntimeClassName` method inherited by `Interface1` gives `"Interface1"` and the method inherited by `Interface2` gives `"Interface2"`.
|
||||
>
|
||||
> Only one of these interfaces can be what the object responds to with a QI for `IInspectable`, and that's the implementation that MBM calls.
|
||||
|
||||
That means we can't just add another interface easily. But what we can do:
|
||||
|
||||
> It might be possible to prefill the cache with the interfaces in question by
|
||||
> marshaling objects that implement each of the interfaces in a way that
|
||||
> registration-free MBM can work with.
|
||||
>
|
||||
> E.g. to keep it simple, marshal an
|
||||
> instance of a separate implementation class per interface that "implements"
|
||||
> each interface
|
||||
|
||||
So that's exactly what we're going to do, because it works. As an example,
|
||||
we're going to add the following interface to our API:
|
||||
|
||||
```csharp
|
||||
interface IExtendedAttributesProvider
|
||||
{
|
||||
Windows.Foundation.Collections.IMap<String, Object> GetProperties();
|
||||
};
|
||||
|
||||
interface ICommandProvider2 requires ICommandProvider
|
||||
{
|
||||
Object[] GetApiExtensionStubs();
|
||||
};
|
||||
```
|
||||
|
||||
`IExtendedAttributesProvider` is just a simple interface, indicating that there's some
|
||||
property bag of additional values that the host could read. We're starting with
|
||||
this, because it's a helpful tool for us to add arbitrary properties to object
|
||||
in an experimental fashion. We can continue to add more things we read from
|
||||
this property set, without breaking the ABI.
|
||||
|
||||
As an example, `ICommand` proves uniquely challenging to extend, because it has
|
||||
both the `IInvokableCommand` and `IPage` family trees of interfaces which
|
||||
extend from it. Typically, it would be impossible for a class to be defined as
|
||||
|
||||
```cs
|
||||
class MyCommandWithProperties : IInvokableCommand, IExtendedAttributesProvider { ... }
|
||||
```
|
||||
|
||||
because Command Palette would only ever see the _first_ interface
|
||||
(`IInvokableCommand`) via MBM, and would never be able to check if an extension
|
||||
object was an `IExtendedAttributesProvider`. But a class defined like
|
||||
|
||||
```cs
|
||||
class CommandWithOnlyProperties : IExtendedAttributesProvider { ... }
|
||||
```
|
||||
|
||||
will populate the WinRT type cache in Command Palette with the type information
|
||||
for `ICommandWithProperties`. In fact, if Command Palette has the
|
||||
`IExtendedAttributesProvider` type info in it's cache, and then later receives a new
|
||||
`MyCommandWithProperties` object, it'll actually be able to know that
|
||||
`MyCommandWithProperties` is an `IExtendedAttributesProvider`. WinRT is just weird
|
||||
like that some times.
|
||||
|
||||
`ICommandProvider2` is where the magic happens. This is a _linear_ addition to
|
||||
`ICommandProvider`, which merely adds a method to return a set of objects.
|
||||
Extensions can implement that method, by returning out stub implementations of
|
||||
all the future additions to the API that we may add. In so doing, CmdPal will
|
||||
be able to ask each extension for these stubs, pre-load the type cache for each
|
||||
extension, and then never have to worry in the future.
|
||||
|
||||
As an example:
|
||||
|
||||
```cs
|
||||
public partial class SamplePagesCommandsProvider : CommandProvider, ICommandProvider2 {
|
||||
public SamplePagesCommandsProvider() {
|
||||
DisplayName = "Sample Pages Commands";
|
||||
Icon = new IconInfo("\uE82D");
|
||||
}
|
||||
public override ICommandItem[] TopLevelCommands() {
|
||||
return [
|
||||
new CommandItem(new SamplesListPage()) { Title = "Sample Pages", Subtitle = "View example commands" },
|
||||
];
|
||||
}
|
||||
|
||||
// Here is where we enable support for future additions to the API
|
||||
public object[] GetApiExtensionStubs() {
|
||||
return [new SupportCommandsWithProperties()];
|
||||
}
|
||||
private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider {
|
||||
public IDictionary<string, object>? GetProperties() => null;
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
Fortunately, we can put all of that (`GetApiExtensionStubs`,
|
||||
`SupportCommandsWithProperties`) directly in `Toolkit.CommandProvider`, so
|
||||
developers won't have to do anything. The toolkit will just do the right thing
|
||||
for them.
|
||||
|
||||
## Class diagram
|
||||
|
||||
@@ -2210,6 +2319,8 @@ this prevents us from being able to use `[contract]` attributes to add to the
|
||||
interfaces. We'll instead need to rely on the tried-and-true method of adding a
|
||||
`IFoo2` when we want to add methods to `IFoo`.
|
||||
|
||||
[Addenda I](#addenda-i-api-additions-icommandprovider2) talks a little more on some of the challenges with adding more APIs.
|
||||
|
||||
[^1]: In this example, as in other places, I've referenced a
|
||||
`Microsoft.DevPal.Extensions.InvokableCommand` class, as the base for that action.
|
||||
Our SDK will include partial class implementations for interfaces like
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -9,7 +9,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
|
||||
public class SettingsManager : JsonSettingsManager
|
||||
public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
{
|
||||
private static readonly string _namespace = "shell";
|
||||
|
||||
|
||||
@@ -17,9 +17,9 @@ namespace Microsoft.CmdPal.Ext.Shell.Helpers;
|
||||
public class ShellListPageHelpers
|
||||
{
|
||||
private static readonly CompositeFormat CmdHasBeenExecutedTimes = System.Text.CompositeFormat.Parse(Properties.Resources.cmd_has_been_executed_times);
|
||||
private readonly SettingsManager _settings;
|
||||
private readonly ISettingsInterface _settings;
|
||||
|
||||
public ShellListPageHelpers(SettingsManager settings)
|
||||
public ShellListPageHelpers(ISettingsInterface settings)
|
||||
{
|
||||
_settings = settings;
|
||||
}
|
||||
|
||||
@@ -35,7 +35,7 @@ internal sealed partial class ShellListPage : DynamicListPage, IDisposable
|
||||
|
||||
private bool _loadedInitialHistory;
|
||||
|
||||
public ShellListPage(SettingsManager settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
|
||||
public ShellListPage(ISettingsInterface settingsManager, IRunHistoryService runHistoryService, bool addBuiltins = false)
|
||||
{
|
||||
Icon = Icons.RunV2Icon;
|
||||
Id = "com.microsoft.cmdpal.shell";
|
||||
|
||||
@@ -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.Shell.UnitTests")]
|
||||
@@ -13,11 +13,11 @@ namespace Microsoft.CmdPal.Ext.WebSearch.Commands;
|
||||
|
||||
internal sealed partial class SearchWebCommand : InvokableCommand
|
||||
{
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
|
||||
public string Arguments { get; internal set; } = string.Empty;
|
||||
|
||||
internal SearchWebCommand(string arguments, SettingsManager settingsManager)
|
||||
internal SearchWebCommand(string arguments, ISettingsInterface settingsManager)
|
||||
{
|
||||
Arguments = arguments;
|
||||
BrowserInfo.UpdateIfTimePassed();
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
// 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.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
public interface ISettingsInterface
|
||||
{
|
||||
public bool GlobalIfURI { get; }
|
||||
|
||||
public string ShowHistory { get; }
|
||||
|
||||
public List<ListItem> LoadHistory();
|
||||
|
||||
public void SaveHistory(HistoryItem historyItem);
|
||||
}
|
||||
@@ -14,7 +14,7 @@ using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
namespace Microsoft.CmdPal.Ext.WebSearch.Helpers;
|
||||
|
||||
public class SettingsManager : JsonSettingsManager
|
||||
public class SettingsManager : JsonSettingsManager, ISettingsInterface
|
||||
{
|
||||
private readonly string _historyPath;
|
||||
|
||||
|
||||
@@ -20,12 +20,12 @@ internal sealed partial class WebSearchListPage : DynamicListPage
|
||||
{
|
||||
private readonly string _iconPath = string.Empty;
|
||||
private readonly List<ListItem>? _historyItems;
|
||||
private readonly SettingsManager _settingsManager;
|
||||
private readonly ISettingsInterface _settingsManager;
|
||||
private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name);
|
||||
private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open);
|
||||
private List<ListItem> _allItems;
|
||||
|
||||
public WebSearchListPage(SettingsManager settingsManager)
|
||||
public WebSearchListPage(ISettingsInterface settingsManager)
|
||||
{
|
||||
Name = Resources.command_item_title;
|
||||
Title = Resources.command_item_title;
|
||||
|
||||
@@ -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.WebSearch.UnitTests")]
|
||||
@@ -32,7 +32,16 @@ public partial class InstallPackageListItem : ListItem
|
||||
{
|
||||
_package = package;
|
||||
|
||||
var version = _package.DefaultInstallVersion ?? _package.InstalledVersion;
|
||||
PackageVersionInfo? version = null;
|
||||
try
|
||||
{
|
||||
version = _package.DefaultInstallVersion ?? _package.InstalledVersion;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Logger.LogError("Could not get package version", e);
|
||||
}
|
||||
|
||||
var versionTagText = "Unknown";
|
||||
if (version is not null)
|
||||
{
|
||||
@@ -57,20 +66,27 @@ public partial class InstallPackageListItem : ListItem
|
||||
}
|
||||
catch (COMException ex)
|
||||
{
|
||||
Logger.LogWarning($"{ex.ErrorCode}");
|
||||
Logger.LogWarning($"GetCatalogPackageMetadata error {ex.ErrorCode}");
|
||||
}
|
||||
|
||||
if (metadata is not null)
|
||||
{
|
||||
if (metadata.Tags.Where(t => t.Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase)).Any())
|
||||
for (var i = 0; i < metadata.Tags.Count; i++)
|
||||
{
|
||||
if (_installCommand is not null)
|
||||
if (metadata.Tags[i].Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_installCommand.SkipDependencies = true;
|
||||
if (_installCommand is not null)
|
||||
{
|
||||
_installCommand.SkipDependencies = true;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
var description = string.IsNullOrEmpty(metadata.Description) ? metadata.ShortDescription : metadata.Description;
|
||||
var description = string.IsNullOrEmpty(metadata.Description) ?
|
||||
metadata.ShortDescription :
|
||||
metadata.Description;
|
||||
var detailsBody = $"""
|
||||
|
||||
{description}
|
||||
@@ -116,9 +132,11 @@ public partial class InstallPackageListItem : ListItem
|
||||
// These can be l o n g
|
||||
{ Properties.Resources.winget_release_notes, (metadata.ReleaseNotes, string.Empty) },
|
||||
};
|
||||
var docs = metadata.Documentations.ToArray();
|
||||
foreach (var item in docs)
|
||||
var docs = metadata.Documentations;
|
||||
var count = docs.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var item = docs[i];
|
||||
simpleData.Add(item.DocumentLabel, (string.Empty, item.DocumentUrl));
|
||||
}
|
||||
|
||||
@@ -141,7 +159,7 @@ public partial class InstallPackageListItem : ListItem
|
||||
}
|
||||
}
|
||||
|
||||
if (metadata.Tags.Any())
|
||||
if (metadata.Tags.Count > 0)
|
||||
{
|
||||
DetailsElement pair = new()
|
||||
{
|
||||
@@ -169,6 +187,7 @@ public partial class InstallPackageListItem : ListItem
|
||||
{
|
||||
// Handle other exceptions
|
||||
ExtensionHost.LogMessage($"[WinGet] UpdatedInstalledStatus throw exception: {ex.Message}");
|
||||
Logger.LogError($"[WinGet] UpdatedInstalledStatus throw exception", ex);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -233,10 +252,10 @@ public partial class InstallPackageListItem : ListItem
|
||||
Stopwatch s = new();
|
||||
Logger.LogDebug($"Starting RefreshPackageCatalogAsync");
|
||||
s.Start();
|
||||
var refs = WinGetStatics.AvailableCatalogs.ToArray();
|
||||
|
||||
foreach (var catalog in refs)
|
||||
var refs = WinGetStatics.AvailableCatalogs;
|
||||
for (var i = 0; i < refs.Count; i++)
|
||||
{
|
||||
var catalog = refs[i];
|
||||
var operation = catalog.RefreshPackageCatalogAsync();
|
||||
operation.Wait();
|
||||
}
|
||||
|
||||
@@ -32,7 +32,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
private CancellationTokenSource? _cancellationTokenSource;
|
||||
private Task<IEnumerable<CatalogPackage>>? _currentSearchTask;
|
||||
|
||||
private IEnumerable<CatalogPackage>? _results;
|
||||
private List<CatalogPackage>? _results;
|
||||
|
||||
public static string ExtensionsTag => "windows-commandpalette-extension";
|
||||
|
||||
@@ -48,7 +48,6 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
IListItem[] items = [];
|
||||
lock (_resultsLock)
|
||||
{
|
||||
// emptySearchForTag ===
|
||||
@@ -61,12 +60,28 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
{
|
||||
IsLoading = true;
|
||||
DoUpdateSearchText(string.Empty);
|
||||
return items;
|
||||
return [];
|
||||
}
|
||||
|
||||
if (_results is not null && _results.Any())
|
||||
if (_results is not null && _results.Count != 0)
|
||||
{
|
||||
ListItem[] results = _results.Select(PackageToListItem).ToArray();
|
||||
var count = _results.Count;
|
||||
var results = new ListItem[count];
|
||||
var next = 0;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
var li = PackageToListItem(_results[i]);
|
||||
results[next] = li;
|
||||
next++;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError("error converting result to listitem", ex);
|
||||
}
|
||||
}
|
||||
|
||||
IsLoading = false;
|
||||
return results;
|
||||
}
|
||||
@@ -82,7 +97,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
|
||||
IsLoading = false;
|
||||
|
||||
return items;
|
||||
return [];
|
||||
}
|
||||
|
||||
private static ListItem PackageToListItem(CatalogPackage p) => new InstallPackageListItem(p);
|
||||
@@ -108,7 +123,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
|
||||
_cancellationTokenSource = new CancellationTokenSource();
|
||||
|
||||
CancellationToken cancellationToken = _cancellationTokenSource.Token;
|
||||
var cancellationToken = _cancellationTokenSource.Token;
|
||||
|
||||
IsLoading = true;
|
||||
|
||||
@@ -139,7 +154,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
{
|
||||
try
|
||||
{
|
||||
IEnumerable<CatalogPackage> results = await searchTask;
|
||||
var results = await searchTask;
|
||||
|
||||
// Ensure this is still the latest task
|
||||
if (_currentSearchTask == searchTask)
|
||||
@@ -156,7 +171,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Handle other exceptions
|
||||
Logger.LogError(ex.Message);
|
||||
Logger.LogError("Unexpected error while processing results", ex);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -165,10 +180,10 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
Logger.LogDebug($"Completed search for '{query}'");
|
||||
lock (_resultsLock)
|
||||
{
|
||||
this._results = results;
|
||||
this._results = results.ToList();
|
||||
}
|
||||
|
||||
RaiseItemsChanged(this._results.Count());
|
||||
RaiseItemsChanged();
|
||||
}
|
||||
|
||||
private async Task<IEnumerable<CatalogPackage>> DoSearchAsync(string query, CancellationToken ct)
|
||||
@@ -190,12 +205,12 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
HashSet<CatalogPackage> results = new(new PackageIdCompare());
|
||||
|
||||
// Default selector: this is the way to do a `winget search <query>`
|
||||
PackageMatchFilter selector = WinGetStatics.WinGetFactory.CreatePackageMatchFilter();
|
||||
var selector = WinGetStatics.WinGetFactory.CreatePackageMatchFilter();
|
||||
selector.Field = Microsoft.Management.Deployment.PackageMatchField.CatalogDefault;
|
||||
selector.Value = query;
|
||||
selector.Option = PackageFieldMatchOption.ContainsCaseInsensitive;
|
||||
|
||||
FindPackagesOptions opts = WinGetStatics.WinGetFactory.CreateFindPackagesOptions();
|
||||
var opts = WinGetStatics.WinGetFactory.CreateFindPackagesOptions();
|
||||
opts.Selectors.Add(selector);
|
||||
|
||||
// testing
|
||||
@@ -204,7 +219,7 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
// Selectors is "OR", Filters is "AND"
|
||||
if (HasTag)
|
||||
{
|
||||
PackageMatchFilter tagFilter = WinGetStatics.WinGetFactory.CreatePackageMatchFilter();
|
||||
var tagFilter = WinGetStatics.WinGetFactory.CreatePackageMatchFilter();
|
||||
tagFilter.Field = Microsoft.Management.Deployment.PackageMatchField.Tag;
|
||||
tagFilter.Value = _tag;
|
||||
tagFilter.Option = PackageFieldMatchOption.ContainsCaseInsensitive;
|
||||
@@ -215,11 +230,11 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
// Clean up here, then...
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
Lazy<Task<PackageCatalog>> catalogTask = HasTag ? WinGetStatics.CompositeWingetCatalog : WinGetStatics.CompositeAllCatalog;
|
||||
var catalogTask = HasTag ? WinGetStatics.CompositeWingetCatalog : WinGetStatics.CompositeAllCatalog;
|
||||
|
||||
// Both these catalogs should have been instantiated by the
|
||||
// WinGetStatics static ctor when we were created.
|
||||
PackageCatalog catalog = await catalogTask.Value;
|
||||
var catalog = await catalogTask.Value;
|
||||
|
||||
if (catalog is null)
|
||||
{
|
||||
@@ -235,8 +250,8 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
|
||||
// BODGY, re: microsoft/winget-cli#5151
|
||||
// FindPackagesAsync isn't actually async.
|
||||
Task<FindPackagesResult> internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
|
||||
FindPackagesResult searchResults = await internalSearchTask;
|
||||
var internalSearchTask = Task.Run(() => catalog.FindPackages(opts), ct);
|
||||
var searchResults = await internalSearchTask;
|
||||
|
||||
// TODO more error handling like this:
|
||||
if (searchResults.Status != FindPackagesResultStatus.Ok)
|
||||
@@ -247,13 +262,17 @@ internal sealed partial class WinGetExtensionPage : DynamicListPage, IDisposable
|
||||
}
|
||||
|
||||
Logger.LogDebug($" got results for ({query})", memberName: nameof(DoSearchAsync));
|
||||
foreach (Management.Deployment.MatchResult? match in searchResults.Matches.ToArray())
|
||||
|
||||
// FYI Using .ToArray or any other kind of enumerable loop
|
||||
// on arrays returned by the winget API are NOT trim safe
|
||||
var count = searchResults.Matches.Count;
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var match = searchResults.Matches[i];
|
||||
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
// Print the packages
|
||||
CatalogPackage package = match.CatalogPackage;
|
||||
|
||||
var package = match.CatalogPackage;
|
||||
results.Add(package);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ namespace Microsoft.CmdPal.Ext.WindowsServices.Helpers;
|
||||
|
||||
public static class ServiceHelper
|
||||
{
|
||||
public static IEnumerable<ListItem> Search(string search)
|
||||
public static IEnumerable<ListItem> Search(string search, string filterId)
|
||||
{
|
||||
var services = ServiceController.GetServices().OrderBy(s => s.DisplayName);
|
||||
IEnumerable<ServiceController> serviceList = [];
|
||||
@@ -44,6 +44,21 @@ public static class ServiceHelper
|
||||
serviceList = servicesStartsWith.Concat(servicesContains);
|
||||
}
|
||||
|
||||
switch (filterId)
|
||||
{
|
||||
case "running":
|
||||
serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Running);
|
||||
break;
|
||||
case "stopped":
|
||||
serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Stopped);
|
||||
break;
|
||||
case "paused":
|
||||
serviceList = serviceList.Where(w => w.Status == ServiceControllerStatus.Paused);
|
||||
break;
|
||||
case "all":
|
||||
break;
|
||||
}
|
||||
|
||||
var result = serviceList.Select(s =>
|
||||
{
|
||||
var serviceResult = ServiceResult.CreateServiceController(s);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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.WindowsServices;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public partial class ServiceFilters : Filters
|
||||
{
|
||||
public ServiceFilters()
|
||||
{
|
||||
CurrentFilterId = "all";
|
||||
}
|
||||
|
||||
public override IFilterItem[] GetFilters()
|
||||
{
|
||||
return [
|
||||
new Filter() { Id = "all", Name = "All Services" },
|
||||
new Separator(),
|
||||
new Filter() { Id = "running", Name = "Running", Icon = Icons.GreenCircleIcon },
|
||||
new Filter() { Id = "stopped", Name = "Stopped", Icon = Icons.RedCircleIcon },
|
||||
new Filter() { Id = "paused", Name = "Paused", Icon = Icons.PauseIcon },
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -16,13 +16,19 @@ internal sealed partial class ServicesListPage : DynamicListPage
|
||||
{
|
||||
Icon = Icons.ServicesIcon;
|
||||
Name = "Windows Services";
|
||||
|
||||
var filters = new ServiceFilters();
|
||||
filters.PropChanged += Filters_PropChanged;
|
||||
Filters = filters;
|
||||
}
|
||||
|
||||
private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged();
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(0);
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
var items = ServiceHelper.Search(SearchText).ToArray();
|
||||
var items = ServiceHelper.Search(SearchText, Filters.CurrentFilterId).ToArray();
|
||||
|
||||
return items;
|
||||
}
|
||||
|
||||
@@ -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.Linq;
|
||||
using System.Runtime.InteropServices.WindowsRuntime;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
@@ -16,9 +17,14 @@ internal sealed partial class SampleDynamicListPage : DynamicListPage
|
||||
Icon = new IconInfo(string.Empty);
|
||||
Name = "Dynamic List";
|
||||
IsLoading = true;
|
||||
var filters = new SampleFilters();
|
||||
filters.PropChanged += Filters_PropChanged;
|
||||
Filters = filters;
|
||||
}
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged(newSearch.Length);
|
||||
private void Filters_PropChanged(object sender, IPropChangedEventArgs args) => RaiseItemsChanged();
|
||||
|
||||
public override void UpdateSearchText(string oldSearch, string newSearch) => RaiseItemsChanged();
|
||||
|
||||
public override IListItem[] GetItems()
|
||||
{
|
||||
@@ -28,6 +34,23 @@ internal sealed partial class SampleDynamicListPage : DynamicListPage
|
||||
items = [new ListItem(new NoOpCommand()) { Title = "Start typing in the search box" }];
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(Filters.CurrentFilterId))
|
||||
{
|
||||
switch (Filters.CurrentFilterId)
|
||||
{
|
||||
case "mod2":
|
||||
items = items.Where((item, index) => (index + 1) % 2 == 0).ToArray();
|
||||
break;
|
||||
case "mod3":
|
||||
items = items.Where((item, index) => (index + 1) % 3 == 0).ToArray();
|
||||
break;
|
||||
case "all":
|
||||
default:
|
||||
// No filtering
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (items.Length > 0)
|
||||
{
|
||||
items[0].Subtitle = "Notice how the number of items changes for this page when you type in the filter box";
|
||||
@@ -36,3 +59,18 @@ internal sealed partial class SampleDynamicListPage : DynamicListPage
|
||||
return items;
|
||||
}
|
||||
}
|
||||
|
||||
#pragma warning disable SA1402 // File may only contain a single type
|
||||
public partial class SampleFilters : Filters
|
||||
#pragma warning restore SA1402 // File may only contain a single type
|
||||
{
|
||||
public override IFilterItem[] GetFilters()
|
||||
{
|
||||
return
|
||||
[
|
||||
new Filter() { Id = "all", Name = "All" },
|
||||
new Filter() { Id = "mod2", Name = "Every 2nd", Icon = new IconInfo("2") },
|
||||
new Filter() { Id = "mod3", Name = "Every 3rd", Icon = new IconInfo("3") },
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,11 @@
|
||||
// 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.Diagnostics.CodeAnalysis;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
using Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
using Windows.Foundation;
|
||||
using Windows.System;
|
||||
using Windows.Win32;
|
||||
|
||||
@@ -81,7 +84,7 @@ internal sealed partial class SampleListPage : ListPage
|
||||
Title = "I'm a second command",
|
||||
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
|
||||
},
|
||||
new SeparatorContextItem(),
|
||||
new Separator(),
|
||||
new CommandContextItem(
|
||||
new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
|
||||
{
|
||||
@@ -169,6 +172,61 @@ internal sealed partial class SampleListPage : ListPage
|
||||
{
|
||||
Title = "Get the name of the Foreground window",
|
||||
},
|
||||
|
||||
new ListItem(new CommandWithProperties())
|
||||
{
|
||||
Title = "I have properties",
|
||||
},
|
||||
new ListItem(new OtherCommandWithProperties())
|
||||
{
|
||||
Title = "I also have properties",
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
internal sealed partial class CommandWithProperties : InvokableCommand, IExtendedAttributesProvider
|
||||
{
|
||||
private FontIconData _icon = new("\u0026", "Wingdings");
|
||||
|
||||
public override IconInfo Icon => new(_icon, _icon);
|
||||
|
||||
public override string Name => "Whatever";
|
||||
|
||||
// LOAD-BEARING: Use a Windows.Foundation.Collections.ValueSet as the
|
||||
// backing store for Properties. A regular `Dictionary<string, object>`
|
||||
// will not work across the ABI
|
||||
public IDictionary<string, object> GetProperties() => new Windows.Foundation.Collections.ValueSet()
|
||||
{
|
||||
{ "Foo", "bar" },
|
||||
{ "Secret", 42 },
|
||||
{ "hmm?", null },
|
||||
};
|
||||
}
|
||||
|
||||
internal sealed partial class OtherCommandWithProperties : IExtendedAttributesProvider, IInvokableCommand
|
||||
{
|
||||
public string Name => "Whatever 2";
|
||||
|
||||
public IIconInfo Icon => new IconInfo("\uF146");
|
||||
|
||||
public string Id => string.Empty;
|
||||
|
||||
public event TypedEventHandler<object, IPropChangedEventArgs> PropChanged;
|
||||
|
||||
public ICommandResult Invoke(object sender)
|
||||
{
|
||||
PropChanged?.Invoke(this, new PropChangedEventArgs(nameof(Name)));
|
||||
return CommandResult.ShowToast("whoop");
|
||||
}
|
||||
|
||||
// LOAD-BEARING: Use a Windows.Foundation.Collections.ValueSet as the
|
||||
// backing store for Properties. A regular `Dictionary<string, object>`
|
||||
// will not work across the ABI
|
||||
public IDictionary<string, object> GetProperties() => new Windows.Foundation.Collections.ValueSet()
|
||||
{
|
||||
{ "yo", "dog" },
|
||||
{ "Secret", 12345 },
|
||||
{ "hmm?", null },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"SamplePagesExtension (Package)": {
|
||||
"commandName": "MsixPackage",
|
||||
"doNotLaunchApp": true,
|
||||
"nativeDebugging": true
|
||||
"nativeDebugging": false
|
||||
},
|
||||
"SamplePagesExtension (Unpackaged)": {
|
||||
"commandName": "Project"
|
||||
|
||||
@@ -6,7 +6,7 @@ using Windows.Foundation;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class CommandProvider : ICommandProvider
|
||||
public abstract partial class CommandProvider : ICommandProvider, ICommandProvider2
|
||||
{
|
||||
public virtual string Id { get; protected set; } = string.Empty;
|
||||
|
||||
@@ -47,4 +47,26 @@ public abstract partial class CommandProvider : ICommandProvider
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// This is used to manually populate the WinRT type cache in CmdPal with
|
||||
/// any interfaces that might not follow a straight linear path of requires.
|
||||
///
|
||||
/// You don't need to call this as an extension author.
|
||||
/// </summary>
|
||||
/// <returns>an array of objects that implement all the leaf interfaces we support</returns>
|
||||
public object[] GetApiExtensionStubs()
|
||||
{
|
||||
return [new SupportCommandsWithProperties()];
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A stub class which implements IExtendedAttributesProvider. Just marshalling this
|
||||
/// across the ABI will be enough for CmdPal to store IExtendedAttributesProvider in
|
||||
/// its type cache.
|
||||
/// </summary>
|
||||
private sealed partial class SupportCommandsWithProperties : IExtendedAttributesProvider
|
||||
{
|
||||
public IDictionary<string, object>? GetProperties() => null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ public abstract class DynamicListPage : ListPage, IDynamicListPage
|
||||
set
|
||||
{
|
||||
var oldSearch = base.SearchText;
|
||||
base.SearchText = value;
|
||||
SetSearchNoUpdate(value);
|
||||
UpdateSearchText(oldSearch, value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
public abstract partial class Filters : BaseObservable, IFilters
|
||||
{
|
||||
public string CurrentFilterId
|
||||
{
|
||||
get => field;
|
||||
set
|
||||
{
|
||||
field = value;
|
||||
OnPropertyChanged(nameof(CurrentFilterId));
|
||||
}
|
||||
}
|
||||
|
||||
= string.Empty;
|
||||
|
||||
// This method should be overridden in derived classes to provide the actual filters.
|
||||
public abstract IFilterItem[] GetFilters();
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
// 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 Windows.Foundation.Collections;
|
||||
|
||||
namespace Microsoft.CommandPalette.Extensions.Toolkit;
|
||||
|
||||
/// <summary>
|
||||
/// Represents an icon that is a font glyph.
|
||||
/// This is used for icons that are defined by a specific font face,
|
||||
/// such as Wingdings.
|
||||
///
|
||||
/// Note that Command Palette will default to using the Segoe Fluent Icons,
|
||||
/// Segoe MDL2 Assets font for glyphs in the Segoe UI Symbol range, or Segoe
|
||||
/// UI for any other glyphs. This class is only needed if you want a non-Segoe
|
||||
/// font icon.
|
||||
/// </summary>
|
||||
public partial class FontIconData : IconData, IExtendedAttributesProvider
|
||||
{
|
||||
public string FontFamily { get; set; }
|
||||
|
||||
public FontIconData(string glyph, string fontFamily)
|
||||
: base(glyph)
|
||||
{
|
||||
FontFamily = fontFamily;
|
||||
}
|
||||
|
||||
public IDictionary<string, object>? GetProperties() => new ValueSet()
|
||||
{
|
||||
{ "FontFamily", FontFamily },
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user