Merge branch 'dev/vanzue/search' of github.com:microsoft/PowerToys into dev/vanzue/search

This commit is contained in:
vanzue 2025-08-21 23:05:41 +08:00
commit 0940fec34a
94 changed files with 2121 additions and 347 deletions

View File

@ -1120,6 +1120,7 @@ NOTSRCCOPY
NOTSRCERASE
notwindows
NOTXORPEN
nowarn
NOZORDER
NPH
npmjs

View File

@ -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;
}

View File

@ -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

View File

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

137
NOTICE.md
View File

@ -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

View File

@ -262,6 +262,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "utils", "utils", "{B39DC643
src\common\utils\EventLocker.h = src\common\utils\EventLocker.h
src\common\utils\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
@ -796,6 +797,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
@ -2898,6 +2903,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
@ -3214,6 +3235,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}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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"

View File

@ -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;

View File

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

View File

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

View File

@ -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" />

View File

@ -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">

View File

@ -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;
}

View File

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

View File

@ -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" />

View File

@ -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" />

View File

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

View File

@ -16,6 +16,7 @@
#include "trace.h"
#include "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;
}

View File

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

View File

@ -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();

View File

@ -0,0 +1,57 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class FilterItemViewModel : ExtensionObjectViewModel, IFilterItemViewModel
{
private ExtensionObject<IFilter> _model;
public string Id { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty;
public IconInfoViewModel Icon { get; set; } = new(null);
internal InitializedState Initialized { get; private set; } = InitializedState.Uninitialized;
protected bool IsInitialized => IsInErrorState || Initialized.HasFlag(InitializedState.Initialized);
public bool IsInErrorState => Initialized.HasFlag(InitializedState.Error);
public FilterItemViewModel(IFilter filter, WeakReference<IPageContext> context)
: base(context)
{
_model = new(filter);
}
public override void InitializeProperties()
{
if (IsInitialized)
{
return;
}
var filter = _model.Unsafe;
if (filter == null)
{
return; // throw?
}
Id = filter.Id;
Name = filter.Name;
Icon = new(filter.Icon);
if (Icon is not null)
{
Icon.InitializeProperties();
}
UpdateProperty(nameof(Id));
UpdateProperty(nameof(Name));
UpdateProperty(nameof(Icon));
}
}

View File

@ -0,0 +1,81 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.CmdPal.Core.ViewModels.Models;
using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.Core.ViewModels;
public partial class FiltersViewModel : ExtensionObjectViewModel
{
private readonly ExtensionObject<IFilters> _filtersModel = new(null);
[ObservableProperty]
public partial string CurrentFilterId { get; set; } = string.Empty;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ShouldShowFilters))]
public partial IFilterItemViewModel[] Filters { get; set; } = [];
public bool ShouldShowFilters => Filters.Length > 0;
public FiltersViewModel(ExtensionObject<IFilters> filters, WeakReference<IPageContext> context)
: base(context)
{
_filtersModel = filters;
}
public override void InitializeProperties()
{
try
{
if (_filtersModel.Unsafe is not null)
{
var filters = _filtersModel.Unsafe.GetFilters();
Filters = filters.Select<IFilterItem, IFilterItemViewModel>(filter =>
{
var filterItem = filter as IFilter;
if (filterItem != null)
{
var filterVM = new FilterItemViewModel(filterItem!, PageContext);
filterVM.InitializeProperties();
return filterVM;
}
else
{
return new SeparatorViewModel();
}
}).ToArray();
CurrentFilterId = _filtersModel.Unsafe.CurrentFilterId;
return;
}
}
catch (Exception ex)
{
ShowException(ex, _filtersModel.Unsafe?.GetType().Name);
}
Filters = [];
CurrentFilterId = string.Empty;
}
public override void SafeCleanup()
{
base.SafeCleanup();
foreach (var filter in Filters)
{
if (filter is FilterItemViewModel filterVM)
{
filterVM.SafeCleanup();
}
}
Filters = [];
}
}

View File

@ -2,12 +2,7 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// 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;

View File

@ -0,0 +1,9 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
namespace Microsoft.CmdPal.Core.ViewModels;
public interface IFilterItemViewModel
{
}

View File

@ -26,6 +26,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
[ObservableProperty]
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)
{

View File

@ -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.

View File

@ -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
{
}

View File

@ -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"

View File

@ -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"

View File

@ -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()

View File

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

View File

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

View File

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

View File

@ -62,7 +62,7 @@ public sealed partial class SearchBar : UserControl,
{
// TODO: In some cases we probably want commands to clear a filter
// 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;
}
}

View File

@ -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;

View File

@ -0,0 +1,36 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Core.ViewModels;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Microsoft.CmdPal.UI;
internal sealed partial class FilterTemplateSelector : DataTemplateSelector
{
public DataTemplate? Default { get; set; }
public DataTemplate? Separator { get; set; }
protected override DataTemplate? SelectTemplateCore(object item, DependencyObject dependencyObject)
{
DataTemplate? dataTemplate = Default;
if (dependencyObject is ComboBoxItem comboBoxItem)
{
comboBoxItem.IsEnabled = true;
if (item is SeparatorViewModel)
{
comboBoxItem.IsEnabled = false;
comboBoxItem.AllowFocusWhenDisabled = false;
comboBoxItem.AllowFocusOnInteraction = false;
dataTemplate = Separator;
}
}
return dataTemplate;
}
}

View File

@ -92,7 +92,7 @@ internal sealed partial class TrayIconService
{
_popupMenu = PInvoke.CreatePopupMenu_SafeHandle();
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

View File

@ -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

View File

@ -419,8 +419,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.</v
<data name="TrayMenu_Settings" xml:space="preserve">
<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>

View File

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

View File

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

View File

@ -0,0 +1,49 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
using Microsoft.CmdPal.Ext.Shell.Helpers;
namespace Microsoft.CmdPal.Ext.Shell.UnitTests;
public class Settings : ISettingsInterface
{
private readonly bool leaveShellOpen;
private readonly string shellCommandExecution;
private readonly bool runAsAdministrator;
private readonly Dictionary<string, int> count;
public Settings(
bool leaveShellOpen = false,
string shellCommandExecution = "0",
bool runAsAdministrator = false,
Dictionary<string, int> count = null)
{
this.leaveShellOpen = leaveShellOpen;
this.shellCommandExecution = shellCommandExecution;
this.runAsAdministrator = runAsAdministrator;
this.count = count ?? new Dictionary<string, int>();
}
public bool LeaveShellOpen => leaveShellOpen;
public string ShellCommandExecution => shellCommandExecution;
public bool RunAsAdministrator => runAsAdministrator;
public Dictionary<string, int> Count => count;
public void AddCmdHistory(string cmdName)
{
count[cmdName] = count.TryGetValue(cmdName, out var currentCount) ? currentCount + 1 : 1;
}
public static Settings CreateDefaultSettings() => new Settings();
public static Settings CreateLeaveShellOpenSettings() => new Settings(leaveShellOpen: true);
public static Settings CreatePowerShellSettings() => new Settings(shellCommandExecution: "1");
public static Settings CreateAdministratorSettings() => new Settings(runAsAdministrator: true);
}

View File

@ -0,0 +1,51 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.CmdPal.Common.Services;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
namespace Microsoft.CmdPal.Ext.Shell.UnitTests;
[TestClass]
public class ShellCommandProviderTests
{
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var mockHistoryService = new Mock<IRunHistoryService>();
var provider = new ShellCommandsProvider(mockHistoryService.Object);
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace Microsoft.CmdPal.Ext.WebSearch.UnitTests;
[TestClass]
public class WebSearchCommandProviderTests
{
[TestMethod]
public void ProviderHasCorrectId()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Assert
Assert.AreEqual("WebSearch", provider.Id);
}
[TestMethod]
public void ProviderHasDisplayName()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Assert
Assert.IsNotNull(provider.DisplayName);
Assert.IsTrue(provider.DisplayName.Length > 0);
}
[TestMethod]
public void ProviderHasIcon()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Assert
Assert.IsNotNull(provider.Icon);
}
[TestMethod]
public void TopLevelCommandsNotEmpty()
{
// Setup
var provider = new WebSearchCommandsProvider();
// Act
var commands = provider.TopLevelCommands();
// Assert
Assert.IsNotNull(commands);
Assert.IsTrue(commands.Length > 0);
}
}

View File

@ -19,29 +19,22 @@ public class CommandPaletteTestBase : UITestBase
{
}
protected void SetSearchBox(string text)
{
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();
}

View File

@ -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();

View File

@ -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

View File

@ -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)
{

View File

@ -0,0 +1,20 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Collections.Generic;
namespace Microsoft.CmdPal.Ext.Shell.Helpers;
public interface ISettingsInterface
{
public bool LeaveShellOpen { get; }
public string ShellCommandExecution { get; }
public bool RunAsAdministrator { get; }
public Dictionary<string, int> Count { get; }
public void AddCmdHistory(string cmdName);
}

View File

@ -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";

View File

@ -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;
}

View File

@ -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";

View File

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.Shell.UnitTests")]

View File

@ -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();

View File

@ -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);
}

View File

@ -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;

View File

@ -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;

View File

@ -0,0 +1,7 @@
// Copyright (c) Microsoft Corporation
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Microsoft.CmdPal.Ext.WebSearch.UnitTests")]

View File

@ -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);

View File

@ -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 },
];
}
}

View File

@ -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;
}

View File

@ -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") },
];
}
}

View File

@ -81,7 +81,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
{

View File

@ -12,7 +12,7 @@ public abstract class DynamicListPage : ListPage, IDynamicListPage
set
{
var oldSearch = base.SearchText;
base.SearchText = value;
SetSearchNoUpdate(value);
UpdateSearchText(oldSearch, value);
}
}

View File

@ -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();
}

View File

@ -105,4 +105,9 @@ public partial class ListPage : Page, IListPage
{
}
}
protected void SetSearchNoUpdate(string newSearchText)
{
_searchText = newSearchText;
}
}

View File

@ -4,6 +4,6 @@
namespace Microsoft.CommandPalette.Extensions.Toolkit;
public partial class SeparatorContextItem : ISeparatorContextItem
public partial class Separator : ISeparatorContextItem, ISeparatorFilterItem
{
}

View File

@ -122,7 +122,7 @@ namespace Microsoft.CommandPalette.Extensions
interface ISeparatorFilterItem requires IFilterItem {}
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IFilter requires IFilterItem {
interface IFilter requires INotifyPropChanged, IFilterItem {
String Id { get; };
String Name { get; };
IIconInfo Icon { get; };
@ -131,7 +131,7 @@ namespace Microsoft.CommandPalette.Extensions
[contract(Microsoft.CommandPalette.Extensions.ExtensionsContract, 1)]
interface IFilters {
String CurrentFilterId { get; set; };
IFilterItem[] Filters();
IFilterItem[] GetFilters();
}
struct Color

View File

@ -100,6 +100,7 @@
<None Include="resource.base.h" />
<ClInclude Include="Generated Files/resource.h" />
<ClInclude Include="ImageResizerExt_i.h" />
<ClInclude Include="RuntimeRegistration.h" />
<ClInclude Include="pch.h" />
<ClInclude Include="targetver.h" />
</ItemGroup>

View File

@ -54,6 +54,9 @@
<ClInclude Include="Generated Files/resource.h">
<Filter>Generated Files</Filter>
</ClInclude>
<ClInclude Include="RuntimeRegistration.h">
<Filter>Header Files</Filter>
</ClInclude>
</ItemGroup>
<ItemGroup>
<None Include="ImageResizerExt.rgs">

View File

@ -0,0 +1,38 @@
// Header-only runtime registration for ImageResizer shell extension.
#pragma once
#include <common/utils/shell_ext_registration.h>
extern "C" IMAGE_DOS_HEADER __ImageBase; // provided by linker
namespace ImageResizerRuntimeRegistration
{
namespace
{
inline runtime_shell_ext::Spec BuildSpec()
{
runtime_shell_ext::Spec spec;
spec.clsid = L"{51B4D7E5-7568-4234-B4BB-47FB3C016A69}";
spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\ImageResizer";
spec.sentinelValue = L"ContextMenuRegistered";
spec.dllFileCandidates = { L"PowerToys.ImageResizerExt.dll" };
spec.contextMenuHandlerKeyPaths = { };
spec.systemFileAssocHandlerName = L"ImageResizer";
spec.systemFileAssocExtensions = { 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" };
spec.representativeSystemExt = L".png"; // probe for repair
spec.extraAssociationPaths = { L"Software\\Classes\\Directory\\ShellEx\\DragDropHandlers\\ImageResizer" };
spec.friendlyName = L"ImageResizer Shell Extension";
return spec;
}
}
inline bool EnsureRegistered()
{
return runtime_shell_ext::EnsureRegistered(BuildSpec(), reinterpret_cast<HMODULE>(&__ImageBase));
}
inline void Unregister()
{
runtime_shell_ext::Unregister(BuildSpec());
}
}

View File

@ -14,6 +14,7 @@
#include <common/utils/resources.h>
#include <common/utils/logger_helper.h>
#include <interface/powertoy_module_interface.h>
#include "RuntimeRegistration.h"
CImageResizerExtModule _AtlModule;
HINSTANCE g_hInst_imageResizer = 0;
@ -106,12 +107,17 @@ public:
{
std::wstring path = get_module_folderpath(g_hInst_imageResizer);
std::wstring packageUri = path + L"\\ImageResizerContextMenuPackage.msix";
if (!package::IsPackageRegisteredWithPowerToysVersion(ImageResizerConstants::ModulePackageDisplayName))
{
package::RegisterSparsePackage(path, packageUri);
}
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::EnsureRegistered();
#endif
}
Trace::EnableImageResizer(m_enabled);
}
@ -121,6 +127,13 @@ public:
{
m_enabled = false;
Trace::EnableImageResizer(m_enabled);
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
ImageResizerRuntimeRegistration::Unregister();
Logger::info(L"ImageResizer context menu unregistered (Win10)");
#endif
}
}
// Returns if the powertoys is enabled

View File

@ -30,6 +30,7 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="NLog" />
<PackageReference Include="NLog.Extensions.Logging" />
<!-- HACK: Microsoft.Extensions.Hosting is referenced, even if it is not used, to force dll versions to be the same as in other projects. Really only needed since OneNote uses the LazyCache and NLog dependencies, which references older assemblies. -->
<PackageReference Include="Microsoft.Extensions.Hosting" />

View File

@ -34,6 +34,7 @@
<ClInclude Include="Generated Files/resource.h" />
<ClInclude Include="PowerRenameConstants.h" />
<ClInclude Include="PowerRenameExt.h" />
<ClInclude Include="RuntimeRegistration.h" />
<ClInclude Include="pch.h" />
<None Include="resource.base.h" />
<ClInclude Include="targetver.h" />

View File

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

View File

@ -0,0 +1,37 @@
// Header-only runtime registration for PowerRename context menu extension.
#pragma once
#include <common/utils/shell_ext_registration.h>
// Provided by dllmain.cpp
extern HINSTANCE g_hInst;
namespace PowerRenameRuntimeRegistration
{
namespace
{
inline runtime_shell_ext::Spec BuildSpec()
{
runtime_shell_ext::Spec spec;
spec.clsid = L"{0440049F-D1DC-4E46-B27B-98393D79486B}";
spec.sentinelKey = L"Software\\Microsoft\\PowerToys\\PowerRename";
spec.sentinelValue = L"ContextMenuRegistered";
spec.dllFileCandidates = { L"PowerToys.PowerRenameExt.dll" };
spec.contextMenuHandlerKeyPaths = {
L"Software\\Classes\\AllFileSystemObjects\\ShellEx\\ContextMenuHandlers\\PowerRenameExt",
L"Software\\Classes\\Directory\\background\\ShellEx\\ContextMenuHandlers\\PowerRenameExt" };
spec.friendlyName = L"PowerRename Shell Extension";
return spec;
}
}
inline bool EnsureRegistered()
{
return runtime_shell_ext::EnsureRegistered(BuildSpec(), g_hInst);
}
inline void Unregister()
{
runtime_shell_ext::Unregister(BuildSpec());
}
}

View File

@ -15,6 +15,7 @@
#include <common/utils/package.h>
#include <common/utils/process_path.h>
#include <common/utils/resources.h>
#include "RuntimeRegistration.h"
#include <atomic>
@ -196,12 +197,17 @@ public:
{
std::wstring path = get_module_folderpath(g_hInst);
std::wstring packageUri = path + L"\\PowerRenameContextMenuPackage.msix";
if (!package::IsPackageRegisteredWithPowerToysVersion(PowerRenameConstants::ModulePackageDisplayName))
{
package::RegisterSparsePackage(path, packageUri);
}
}
else
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::EnsureRegistered();
#endif
}
}
// Disable the powertoy
@ -209,6 +215,13 @@ public:
{
m_enabled = false;
Logger::info(L"PowerRename disabled");
if (!package::IsWin11OrGreater())
{
#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
PowerRenameRuntimeRegistration::Unregister();
Logger::info(L"PowerRename context menu unregistered (Win10)");
#endif
}
}
// Returns if the powertoy is enabled

View File

@ -176,10 +176,6 @@
<data name="DOCUMENTATION_MENU_TEXT" xml:space="preserve">
<value>Documentation</value>
</data>
<data name="EXIT_MENU_TEXT" xml:space="preserve">
<value>Exit</value>
<comment>Exit as a verb, as in Exit the application</comment>
</data>
<data name="SUBMIT_BUG_TEXT" xml:space="preserve">
<value>Report bug</value>
</data>
@ -193,4 +189,8 @@
<data name="TRAY_ICON_ADMIN_TOOLTIP" xml:space="preserve">
<value>Administrator</value>
</data>
<data name="CLOSE_MENU_TEXT" xml:space="preserve">
<value>Close</value>
<comment>Close as a verb, as in Close the application</comment>
</data>
</root>

View File

@ -15,7 +15,7 @@
#define APPICON 101
#define ID_TRAY_MENU 102
#define ID_EXIT_MENU_COMMAND 40001
#define ID_CLOSE_MENU_COMMAND 40001
#define ID_SETTINGS_MENU_COMMAND 40002
#define ID_ABOUT_MENU_COMMAND 40003
#define ID_REPORT_BUG_COMMAND 40004

Binary file not shown.

View File

@ -84,7 +84,7 @@ void handle_tray_command(HWND window, const WPARAM command_id, LPARAM lparam)
open_settings_window(settings_window, false);
}
break;
case ID_EXIT_MENU_COMMAND:
case ID_CLOSE_MENU_COMMAND:
if (h_menu)
{
DestroyMenu(h_menu);
@ -191,12 +191,12 @@ LRESULT __stdcall tray_icon_window_proc(HWND window, UINT message, WPARAM wparam
if (h_menu)
{
static std::wstring settings_menuitem_label = GET_RESOURCE_STRING(IDS_SETTINGS_MENU_TEXT);
static std::wstring exit_menuitem_label = GET_RESOURCE_STRING(IDS_EXIT_MENU_TEXT);
static std::wstring close_menuitem_label = GET_RESOURCE_STRING(IDS_CLOSE_MENU_TEXT);
static std::wstring submit_bug_menuitem_label = GET_RESOURCE_STRING(IDS_SUBMIT_BUG_TEXT);
static std::wstring documentation_menuitem_label = GET_RESOURCE_STRING(IDS_DOCUMENTATION_MENU_TEXT);
static std::wstring quick_access_menuitem_label = GET_RESOURCE_STRING(IDS_QUICK_ACCESS_MENU_TEXT);
change_menu_item_text(ID_SETTINGS_MENU_COMMAND, settings_menuitem_label.data());
change_menu_item_text(ID_EXIT_MENU_COMMAND, exit_menuitem_label.data());
change_menu_item_text(ID_CLOSE_MENU_COMMAND, close_menuitem_label.data());
change_menu_item_text(ID_REPORT_BUG_COMMAND, submit_bug_menuitem_label.data());
bool bug_report_disabled = is_bug_report_running();
EnableMenuItem(h_sub_menu, ID_REPORT_BUG_COMMAND, MF_BYCOMMAND | (bug_report_disabled ? MF_GRAYED : MF_ENABLED));

View File

@ -277,9 +277,11 @@
IsEnabled="{x:Bind ViewModel.IsRunAtStartupGPOManaged, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
<ToggleSwitch x:Uid="ToggleSwitch" IsOn="{x:Bind ViewModel.Startup, Mode=TwoWay}" />
</tkcontrols:SettingsCard>
<tkcontrols:SettingsCard Name="ShowSystemTrayIcon" x:Uid="ShowSystemTrayIcon">
<ToggleSwitch x:Uid="ShowSystemTrayIcon_ToggleSwitch" IsOn="{x:Bind ViewModel.ShowSysTrayIcon, Mode=TwoWay}" />
<tkcontrols:SettingsCard x:Uid="ShowSystemTrayIcon">
<ToggleSwitch
x:Uid="ShowSystemTrayIcon_ToggleSwitch"
IsOn="{x:Bind ViewModel.ShowSysTrayIcon, Mode=TwoWay}"
Toggled="ShowSystemTrayIcon_Toggled" />
</tkcontrols:SettingsCard>
<InfoBar
x:Uid="GPO_SettingIsManaged"

View File

@ -10,7 +10,6 @@ using Microsoft.PowerToys.Settings.UI.Flyout;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Windows.Data.Json;
@ -174,19 +173,6 @@ namespace Microsoft.PowerToys.Settings.UI.Views
await Task.Run(ViewModel.ViewDiagnosticData);
}
private void ExitPTItem_Tapped(object sender, RoutedEventArgs e)
{
const string ptTrayIconWindowClass = "PToyTrayIconWindow"; // Defined in runner/tray_icon.h
const nuint ID_EXIT_MENU_COMMAND = 40001; // Generated resource from runner/runner.base.rc
// Exit the XAML application
Application.Current.Exit();
// Invoke the exit command from the tray icon
IntPtr hWnd = NativeMethods.FindWindow(ptTrayIconWindowClass, ptTrayIconWindowClass);
NativeMethods.SendMessage(hWnd, NativeMethods.WM_COMMAND, ID_EXIT_MENU_COMMAND, 0);
}
private void BugReportToolClicked(object sender, RoutedEventArgs e)
{
// Start bug report
@ -232,5 +218,17 @@ namespace Microsoft.PowerToys.Settings.UI.Views
ShellPage.ShellHandler.IPCResponseHandleList.Remove(HandleBugReportStatusResponse);
}
}
private void ShowSystemTrayIcon_Toggled(object sender, RoutedEventArgs e)
{
if (sender is ToggleSwitch toggleSwitch)
{
var shellViewModel = ShellPage.ShellHandler?.ViewModel;
if (shellViewModel != null)
{
shellViewModel.ShowCloseMenu = !toggleSwitch.IsOn;
}
}
}
}
}

View File

@ -1,4 +1,4 @@
<UserControl
<UserControl
x:Class="Microsoft.PowerToys.Settings.UI.Views.ShellPage"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@ -94,17 +94,10 @@
Margin="48,0,0,0"
VerticalAlignment="Top"
IsHitTestVisible="True">
<Grid.ColumnDefinitions>
<ColumnDefinition x:Name="LeftTitleBarColumn" Width="*" />
<ColumnDefinition x:Name="RightTitleBarColumn" Width="Auto" />
</Grid.ColumnDefinitions>
<animations:Implicit.Animations>
<animations:OffsetAnimation Duration="0:0:0.3" />
</animations:Implicit.Animations>
<StackPanel
Grid.Column="0"
HorizontalAlignment="Left"
Orientation="Horizontal">
<StackPanel Orientation="Horizontal">
<Image
Width="16"
Height="16"
@ -126,24 +119,6 @@
TextWrapping="NoWrap"
Visibility="Collapsed" />
</StackPanel>
<StackPanel
Grid.Column="1"
Margin="0,0,148,0"
HorizontalAlignment="Right"
Orientation="Horizontal">
<Button
x:Name="ShutDownBtn"
Height="48"
Click="ExitPTItem_Tapped"
Style="{StaticResource SubtleButtonStyle}">
<FontIcon FontSize="16" Glyph="&#xE7E8;" />
<ToolTipService.ToolTip>
<ToolTip>
<TextBlock x:Uid="AppTitleBarShutDown_Tooltip" />
</ToolTip>
</ToolTipService.ToolTip>
</Button>
</StackPanel>
</Grid>
<NavigationView
x:Name="navigationView"
@ -380,6 +355,11 @@
x:Uid="Feedback_NavViewItem"
Icon="{ui:FontIcon Glyph=&#xED15;}"
Tapped="FeedbackItem_Tapped" />
<NavigationViewItem
x:Uid="Close_NavViewItem"
Icon="{ui:FontIcon Glyph=&#xE7E8;}"
Tapped="Close_Tapped"
Visibility="{x:Bind ViewModel.ShowCloseMenu, Mode=OneWay}" />
</StackPanel>
</NavigationView.PaneFooter>
<i:Interaction.Behaviors>
@ -389,5 +369,12 @@
</i:Interaction.Behaviors>
<Frame x:Name="shellFrame" />
</NavigationView>
<ContentDialog
x:Name="CloseDialog"
x:Uid="CloseDialog"
IsPrimaryButtonEnabled="True"
IsSecondaryButtonEnabled="True"
PrimaryButtonClick="CloseDialog_Click"
PrimaryButtonStyle="{StaticResource AccentButtonStyle}" />
</Grid>
</UserControl>

View File

@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation
// 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.
@ -12,6 +12,7 @@ using Common.Search;
using Common.Search.FuzzSearch;
using ManagedCommon;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.PowerToys.Settings.UI.ViewModels;
using Microsoft.UI.Windowing;
@ -120,7 +121,7 @@ namespace Microsoft.PowerToys.Settings.UI.Views
/// <summary>
/// Gets view model.
/// </summary>
public ShellViewModel ViewModel { get; } = new ShellViewModel();
public ShellViewModel ViewModel { get; }
/// <summary>
/// Gets a collection of functions that handle IPC responses.
@ -149,6 +150,8 @@ namespace Microsoft.PowerToys.Settings.UI.Views
{
InitializeComponent();
var settingsUtils = new SettingsUtils();
ViewModel = new ShellViewModel(SettingsRepository<GeneralSettings>.GetInstance(settingsUtils));
DataContext = ViewModel;
ShellHandler = this;
ViewModel.Initialize(shellFrame, navigationView, KeyboardAccelerators);
@ -491,17 +494,22 @@ namespace Microsoft.PowerToys.Settings.UI.Views
navigationView.IsPaneOpen = !navigationView.IsPaneOpen;
}
private void ExitPTItem_Tapped(object sender, RoutedEventArgs e)
private async void Close_Tapped(object sender, Microsoft.UI.Xaml.Input.TappedRoutedEventArgs e)
{
await CloseDialog.ShowAsync();
}
private void CloseDialog_Click(ContentDialog sender, ContentDialogButtonClickEventArgs args)
{
const string ptTrayIconWindowClass = "PToyTrayIconWindow"; // Defined in runner/tray_icon.h
const nuint ID_EXIT_MENU_COMMAND = 40001; // Generated resource from runner/runner.base.rc
const nuint ID_CLOSE_MENU_COMMAND = 40001; // Generated resource from runner/runner.base.rc
// Exit the XAML application
Application.Current.Exit();
// Invoke the exit command from the tray icon
IntPtr hWnd = NativeMethods.FindWindow(ptTrayIconWindowClass, ptTrayIconWindowClass);
NativeMethods.SendMessage(hWnd, NativeMethods.WM_COMMAND, ID_EXIT_MENU_COMMAND, 0);
NativeMethods.SendMessage(hWnd, NativeMethods.WM_COMMAND, ID_CLOSE_MENU_COMMAND, 0);
}
private List<SettingEntry> _lastSearchResults = new();

View File

@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
@ -5071,9 +5071,6 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="GeneralPageReportBugPackage.Content" xml:space="preserve">
<value>Generate package</value>
</data>
<data name="AppTitleBarShutDown_Tooltip.Text" xml:space="preserve">
<value>Shut down</value>
</data>
<data name="BugReportUnderConstruction" xml:space="preserve">
<value>Bug report package is being created</value>
</data>
@ -5168,6 +5165,23 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
<data name="GeneralPage_EnableViewDiagnosticDataText.Text" xml:space="preserve">
<value>Stores diagnostic data locally in .xml format; folder may include .etl files as well. May use up 1GB or more of disk space.</value>
</data>
<data name="Close_NavViewItem.Content" xml:space="preserve">
<value>Close PowerToys</value>
<comment>Don't loc "PowerToys"</comment>
</data>
<data name="CloseDialog.Content" xml:space="preserve">
<value>Closing PowerToys will stop all active utilities.</value>
<comment>Don't loc "PowerToys"</comment>
</data>
<data name="CloseDialog.Title" xml:space="preserve">
<value>Are you sure?</value>
</data>
<data name="CloseDialog.PrimaryButtonText" xml:space="preserve">
<value>Yes</value>
</data>
<data name="CloseDialog.SecondaryButtonText" xml:space="preserve">
<value>No</value>
</data>
<data name="KeyBack" xml:space="preserve">
<value>Back key</value>
</data>

View File

@ -10,7 +10,9 @@ using System.Threading.Tasks;
using System.Windows.Input;
using Microsoft.PowerToys.Settings.UI.Helpers;
using Microsoft.PowerToys.Settings.UI.Library;
using Microsoft.PowerToys.Settings.UI.Library.Helpers;
using Microsoft.PowerToys.Settings.UI.Library.Interfaces;
using Microsoft.PowerToys.Settings.UI.Services;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Input;
@ -26,6 +28,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private readonly KeyboardAccelerator backKeyboardAccelerator = BuildKeyboardAccelerator(VirtualKey.GoBack);
private bool isBackEnabled;
private bool showCloseMenu;
private IList<KeyboardAccelerator> keyboardAccelerators;
private NavigationView navigationView;
private NavigationViewItem selected;
@ -34,17 +37,24 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
private ICommand itemInvokedCommand;
private NavigationViewItem[] _fullListOfNavViewItems;
private NavigationViewItem[] _moduleNavViewItems;
private GeneralSettings _generalSettingsConfig;
public bool IsBackEnabled
{
get { return isBackEnabled; }
set { Set(ref isBackEnabled, value); }
get => isBackEnabled;
set => Set(ref isBackEnabled, value);
}
public bool ShowCloseMenu
{
get => showCloseMenu;
set => Set(ref showCloseMenu, value);
}
public NavigationViewItem Selected
{
get { return selected; }
set { Set(ref selected, value); }
get => selected;
set => Set(ref selected, value);
}
public NavigationViewItem Expanding
@ -62,8 +72,10 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels
public ICommand ItemInvokedCommand => itemInvokedCommand ?? (itemInvokedCommand = new RelayCommand<NavigationViewItemInvokedEventArgs>(OnItemInvoked));
public ShellViewModel()
public ShellViewModel(ISettingsRepository<GeneralSettings> settingsRepository)
{
_generalSettingsConfig = settingsRepository.SettingsConfig;
ShowCloseMenu = !_generalSettingsConfig.ShowSysTrayIcon;
}
public void Initialize(Frame frame, NavigationView navigationView, IList<KeyboardAccelerator> keyboardAccelerators)