diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index db96384da6..e539d0ae1c 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -1120,6 +1120,7 @@ NOTSRCCOPY
NOTSRCERASE
notwindows
NOTXORPEN
+nowarn
NOZORDER
NPH
npmjs
diff --git a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1 b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
index e3120836c8..af9ab8ff6f 100644
--- a/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
+++ b/.pipelines/verifyNoticeMdAgainstNugetPackages.ps1
@@ -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;
}
diff --git a/.pipelines/verifyNugetPackages.ps1 b/.pipelines/verifyNugetPackages.ps1
index 54d0137121..c7cebbd383 100644
--- a/.pipelines/verifyNugetPackages.ps1
+++ b/.pipelines/verifyNugetPackages.ps1
@@ -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
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 71bbda5042..a5270e7d75 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -65,7 +65,8 @@
-
+
+
@@ -103,7 +104,7 @@
-
+
@@ -113,4 +114,4 @@
-
\ No newline at end of file
+
diff --git a/NOTICE.md b/NOTICE.md
index d75fe99522..bedc11379d 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1491,93 +1491,50 @@ SOFTWARE.
## NuGet Packages used by PowerToys
-- AdaptiveCards.ObjectModel.WinUI3 2.0.0-beta
-- AdaptiveCards.Rendering.WinUI3 2.1.0-beta
-- AdaptiveCards.Templating 2.0.5
-- Appium.WebDriver 4.4.5
-- Azure.AI.OpenAI 1.0.0-beta.17
-- CoenM.ImageSharp.ImageHash 1.3.6
-- CommunityToolkit.Common 8.4.0
-- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock 0.1.250703-build.2173
-- CommunityToolkit.Mvvm 8.4.0
-- CommunityToolkit.WinUI.Animations 8.2.250402
-- CommunityToolkit.WinUI.Collections 8.2.250402
-- CommunityToolkit.WinUI.Controls.Primitives 8.2.250402
-- CommunityToolkit.WinUI.Controls.Segmented 8.2.250402
-- CommunityToolkit.WinUI.Controls.SettingsControls 8.2.250402
-- CommunityToolkit.WinUI.Controls.Sizers 8.2.250402
-- CommunityToolkit.WinUI.Converters 8.2.250402
-- CommunityToolkit.WinUI.Extensions 8.2.250402
-- CommunityToolkit.WinUI.UI.Controls.DataGrid 7.1.2
-- CommunityToolkit.WinUI.UI.Controls.Markdown 7.1.2
-- ControlzEx 6.0.0
-- HelixToolkit 2.24.0
-- HelixToolkit.Core.Wpf 2.24.0
-- hyjiacan.pinyin4net 4.1.1
-- Interop.Microsoft.Office.Interop.OneNote 1.1.0.2
-- LazyCache 2.4.0
-- Mages 3.0.0
-- Markdig.Signed 0.34.0
-- MessagePack 3.1.3
-- Microsoft.Bcl.AsyncInterfaces 9.0.8
-- Microsoft.Bot.AdaptiveExpressions.Core 4.23.0
-- Microsoft.CodeAnalysis.NetAnalyzers 9.0.0
-- Microsoft.Data.Sqlite 9.0.8
-- Microsoft.Diagnostics.Tracing.TraceEvent 3.1.16
-- Microsoft.DotNet.ILCompiler (A)
-- Microsoft.Extensions.DependencyInjection 9.0.8
-- Microsoft.Extensions.Hosting 9.0.8
-- Microsoft.Extensions.Hosting.WindowsServices 9.0.8
-- Microsoft.Extensions.Logging 9.0.8
-- Microsoft.Extensions.Logging.Abstractions 9.0.8
-- Microsoft.NET.ILLink.Tasks (A)
-- Microsoft.SemanticKernel 1.15.0
-- Microsoft.Toolkit.Uwp.Notifications 7.1.2
-- Microsoft.Web.WebView2 1.0.2903.40
-- Microsoft.Win32.SystemEvents 9.0.8
-- Microsoft.Windows.Compatibility 9.0.8
-- Microsoft.Windows.CsWin32 0.3.183
-- Microsoft.Windows.CsWinRT 2.2.0
-- Microsoft.Windows.SDK.BuildTools 10.0.26100.4188
-- Microsoft.WindowsAppSDK 1.7.250513003
-- Microsoft.WindowsPackageManager.ComInterop 1.10.340
-- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
-- Microsoft.Xaml.Behaviors.Wpf 1.1.39
-- ModernWpfUI 0.9.4
-- Moq 4.18.4
-- MSTest 3.8.3
-- NLog.Extensions.Logging 5.3.8
-- NLog.Schema 5.2.8
-- OpenAI 2.0.0
-- ReverseMarkdown 4.1.0
-- ScipBe.Common.Office.OneNote 3.0.1
-- SharpCompress 0.37.2
-- SkiaSharp.Views.WinUI 2.88.9
-- StreamJsonRpc 2.21.69
-- StyleCop.Analyzers 1.2.0-beta.556
-- System.CodeDom 9.0.8
-- System.CommandLine 2.0.0-beta4.22272.1
-- System.ComponentModel.Composition 9.0.8
-- System.Configuration.ConfigurationManager 9.0.8
-- System.Data.OleDb 9.0.8
-- System.Data.SqlClient 4.9.0
-- System.Diagnostics.EventLog 9.0.8
-- System.Diagnostics.PerformanceCounter 9.0.8
-- System.Drawing.Common 9.0.8
-- System.IO.Abstractions 22.0.13
-- System.IO.Abstractions.TestingHelpers 22.0.13
-- System.Management 9.0.8
-- System.Net.Http 4.3.4
-- System.Private.Uri 4.3.2
-- System.Reactive 6.0.1
-- System.Runtime.Caching 9.0.8
-- System.ServiceProcess.ServiceController 9.0.8
-- System.Text.Encoding.CodePages 9.0.8
-- System.Text.Json 9.0.8
-- System.Text.RegularExpressions 4.3.1
-- UnicodeInformation 2.6.0
-- UnitsNet 5.56.0
-- UTF.Unknown 2.5.1
-- WinUIEx 2.2.0
-- WPF-UI 3.0.5
-- WyHash 1.0.5
+- AdaptiveCards.ObjectModel.WinUI3
+- AdaptiveCards.Rendering.WinUI3
+- AdaptiveCards.Templating
+- Appium.WebDriver
+- Azure.AI.OpenAI
+- CoenM.ImageSharp.ImageHash
+- CommunityToolkit.Common
+- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock
+- CommunityToolkit.Mvvm
+- CommunityToolkit.WinUI.Animations
+- CommunityToolkit.WinUI.Collections
+- CommunityToolkit.WinUI.Controls.Primitives
+- CommunityToolkit.WinUI.Controls.Segmented
+- CommunityToolkit.WinUI.Controls.SettingsControls
+- CommunityToolkit.WinUI.Controls.Sizers
+- CommunityToolkit.WinUI.Converters
+- CommunityToolkit.WinUI.Extensions
+- CommunityToolkit.WinUI.UI.Controls.DataGrid
+- CommunityToolkit.WinUI.UI.Controls.Markdown
+- ControlzEx
+- HelixToolkit
+- HelixToolkit.Core.Wpf
+- hyjiacan.pinyin4net
+- Interop.Microsoft.Office.Interop.OneNote
+- LazyCache
+- Mages
+- Markdig.Signed
+- MessagePack
+- ModernWpfUI
+- Moq
+- MSTest
+- NLog
+- NLog.Extensions.Logging
+- NLog.Schema
+- OpenAI
+- ReverseMarkdown
+- ScipBe.Common.Office.OneNote
+- SharpCompress
+- SkiaSharp.Views.WinUI
+- StreamJsonRpc
+- StyleCop.Analyzers
+- UnicodeInformation
+- UnitsNet
+- UTF.Unknown
+- WinUIEx
+- WPF-UI
+- WyHash
\ No newline at end of file
diff --git a/PowerToys.sln b/PowerToys.sln
index bedee18991..cedc7f05b4 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -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}
diff --git a/installer/PowerToysSetup/FileLocksmith.wxs b/installer/PowerToysSetup/FileLocksmith.wxs
index 085e60eaa7..5943ab4147 100644
--- a/installer/PowerToysSetup/FileLocksmith.wxs
+++ b/installer/PowerToysSetup/FileLocksmith.wxs
@@ -14,21 +14,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -38,7 +23,6 @@
-
diff --git a/installer/PowerToysSetup/ImageResizer.wxs b/installer/PowerToysSetup/ImageResizer.wxs
index 17da272014..67b5acf198 100644
--- a/installer/PowerToysSetup/ImageResizer.wxs
+++ b/installer/PowerToysSetup/ImageResizer.wxs
@@ -16,71 +16,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -90,7 +25,6 @@
-
diff --git a/installer/PowerToysSetup/NewPlus.wxs b/installer/PowerToysSetup/NewPlus.wxs
index 4dd1c67701..624c01fca2 100644
--- a/installer/PowerToysSetup/NewPlus.wxs
+++ b/installer/PowerToysSetup/NewPlus.wxs
@@ -18,19 +18,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -40,8 +27,7 @@
-
-
+
@@ -81,7 +67,7 @@
-
+
diff --git a/installer/PowerToysSetup/PowerRename.wxs b/installer/PowerToysSetup/PowerRename.wxs
index 1e722d9334..7aa357e207 100644
--- a/installer/PowerToysSetup/PowerRename.wxs
+++ b/installer/PowerToysSetup/PowerRename.wxs
@@ -14,22 +14,6 @@
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
@@ -39,7 +23,6 @@
-
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index f15b8a4714..77ffad8483 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -176,6 +176,18 @@
Installed AND (REMOVE="ALL")
+
+ Installed AND (REMOVE="ALL")
+
+
+ Installed AND (REMOVE="ALL")
+
+
+ Installed AND (REMOVE="ALL")
+
+
+ Installed AND (REMOVE="ALL")
+
Installed AND (NOT UPGRADINGPRODUCTCODE) AND (REMOVE="ALL")
@@ -437,6 +449,35 @@
Execute="deferred"
BinaryKey="PTCustomActions"
DllEntry="UnRegisterContextMenuPackagesCA"
+ />
+
+
+
+
+
+#include
+#include
+#include
+
+#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 dllFileCandidates; // relative filenames (pick first existing)
+ std::vector 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 extraAssociationPaths; // additional key paths (DragDropHandlers etc.) default=CLSID
+ std::vector systemFileAssocExtensions; // e.g. .png -> Software\\Classes\\SystemFileAssociations\\.png\\ShellEx\\ContextMenuHandlers\\
+ 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& 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(&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(&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(spec.friendlyName.c_str()), static_cast((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(&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(dllPath.c_str()), static_cast((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(tm), static_cast((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(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(value.c_str()), static_cast((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;
+ }
+}
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj
index 0c285a8bfa..c67119808f 100644
--- a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj
+++ b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj
@@ -73,6 +73,7 @@
+
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters
index c3b4f47ebc..49bf0e21a9 100644
--- a/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters
+++ b/src/modules/FileLocksmith/FileLocksmithExt/FileLocksmithExt.vcxproj.filters
@@ -27,6 +27,9 @@
Header Files
+
+ Header Files
+
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp
index ec755d99a3..7d188a2010 100644
--- a/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp
+++ b/src/modules/FileLocksmith/FileLocksmithExt/PowerToysModule.cpp
@@ -12,6 +12,7 @@
#include "FileLocksmithLib/Constants.h"
#include "FileLocksmithLib/Settings.h"
#include "FileLocksmithLib/Trace.h"
+#include "RuntimeRegistration.h"
#include "dllmain.h"
#include "Generated Files/resource.h"
@@ -82,12 +83,17 @@ public:
{
std::wstring path = get_module_folderpath(globals::instance);
std::wstring packageUri = path + L"\\FileLocksmithContextMenuPackage.msix";
-
if (!package::IsPackageRegisteredWithPowerToysVersion(constants::nonlocalizable::ContextMenuPackageName))
{
package::RegisterSparsePackage(path, packageUri);
}
}
+ else
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ FileLocksmithRuntimeRegistration::EnsureRegistered();
+#endif
+ }
m_enabled = true;
}
@@ -95,6 +101,13 @@ public:
virtual void disable() override
{
Logger::info(L"File Locksmith disabled");
+ if (!package::IsWin11OrGreater())
+ {
+#if defined(ENABLE_REGISTRATION) || defined(NDEBUG)
+ FileLocksmithRuntimeRegistration::Unregister();
+ Logger::info(L"File Locksmith context menu unregistered (Win10)");
+#endif
+ }
m_enabled = false;
}
diff --git a/src/modules/FileLocksmith/FileLocksmithExt/RuntimeRegistration.h b/src/modules/FileLocksmith/FileLocksmithExt/RuntimeRegistration.h
new file mode 100644
index 0000000000..4dd0d34bea
--- /dev/null
+++ b/src/modules/FileLocksmith/FileLocksmithExt/RuntimeRegistration.h
@@ -0,0 +1,36 @@
+// Header-only runtime registration for FileLocksmith context menu extension.
+#pragma once
+
+#include
+
+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());
+ }
+}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
index 9ac251c8ec..90058a503e 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj
@@ -123,6 +123,7 @@ MakeAppx.exe pack /d . /p $(OutDir)NewPlusPackage.msix /nv
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters
index 7d014eb00f..d8b2eaf9ea 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/NewShellExtensionContextMenu.vcxproj.filters
@@ -84,6 +84,9 @@
Header Files
+
+ Header Files
+
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/RuntimeRegistration.h b/src/modules/NewPlus/NewShellExtensionContextMenu/RuntimeRegistration.h
new file mode 100644
index 0000000000..91596d9e3d
--- /dev/null
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/RuntimeRegistration.h
@@ -0,0 +1,36 @@
+// Header-only runtime registration for New+ Win10 context menu.
+#pragma once
+
+#include
+#include
+#include
+
+// 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());
+ }
+}
diff --git a/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp b/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp
index 303f072e3b..ad94431953 100644
--- a/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp
+++ b/src/modules/NewPlus/NewShellExtensionContextMenu/powertoys_module.cpp
@@ -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;
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs
index 4f589a4e2f..22dad16504 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/CommandItemViewModel.cs
@@ -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();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs
index 0c0f7c7c12..9665908474 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ContentPageViewModel.cs
@@ -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();
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs
new file mode 100644
index 0000000000..78fdb26286
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FilterItemViewModel.cs
@@ -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 _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 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));
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs
new file mode 100644
index 0000000000..511581558c
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/FiltersViewModel.cs
@@ -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 _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 filters, WeakReference context)
+ : base(context)
+ {
+ _filtersModel = filters;
+ }
+
+ public override void InitializeProperties()
+ {
+ try
+ {
+ if (_filtersModel.Unsafe is not null)
+ {
+ var filters = _filtersModel.Unsafe.GetFilters();
+ Filters = filters.Select(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 = [];
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs
index cad1af9d4d..a8f65b2634 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IContextItemViewModel.cs
@@ -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;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs
new file mode 100644
index 0000000000..fb324bb42f
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/IFilterItemViewModel.cs
@@ -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
+{
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs
index 10fc9445ad..f89b2a5906 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/ListViewModel.cs
@@ -26,6 +26,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
[ObservableProperty]
public partial ObservableCollection FilteredItems { get; set; } = [];
+ public FiltersViewModel? Filters { get; set; }
+
private ObservableCollection Items { get; set; } = [];
private readonly ExtensionObject _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.
///
- private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, Filter));
+ private void ApplyFilterUnderLock() => ListHelpers.InPlaceUpdateList(FilteredItems, FilterList(Items, SearchTextBox));
///
/// 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)
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
index 046c9fae93..5c445615be 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/PageViewModel.cs
@@ -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.
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs
similarity index 73%
rename from src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs
rename to src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs
index 8d896bd341..a1c4696b35 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorContextItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Core.ViewModels/SeparatorViewModel.cs
@@ -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
{
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
index aa14b0878a..aba9ed477d 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
@@ -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"
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
index f3c4e5413e..e23fb815b0 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ContextMenu.xaml
@@ -108,7 +108,7 @@
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs
new file mode 100644
index 0000000000..b51376dfa3
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/FiltersDropDown.xaml.cs
@@ -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;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
index 379ea6b03d..80eb1a3ad6 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml
@@ -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"
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
index 7b34594b46..a5f02d76cb 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
@@ -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;
}
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs
index 3ab4dd1c0b..09fa902a4b 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/ContextItemTemplateSelector.cs
@@ -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;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs
new file mode 100644
index 0000000000..2d12c82b28
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Converters/FilterTemplateSelector.cs
@@ -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;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs
index 224c851ff1..442341cc5e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/TrayIconService.cs
@@ -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
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
index 8f7c6ac7bd..dbb3818518 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
@@ -176,6 +176,7 @@
+
@@ -320,6 +321,18 @@
+
+
+
+
+
+
+
+
+
Settings
-
- Exit
+
+ Close
+ Close as a verb, as in Close the application
Direct
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/Microsoft.CmdPal.Ext.Shell.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/Microsoft.CmdPal.Ext.Shell.UnitTests.csproj
new file mode 100644
index 0000000000..74aeb04b39
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/Microsoft.CmdPal.Ext.Shell.UnitTests.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+ false
+ true
+ Microsoft.CmdPal.Ext.Shell.UnitTests
+ $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/QueryTests.cs
new file mode 100644
index 0000000000..bf9fcac406
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/QueryTests.cs
@@ -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 CreateMockHistoryService(IList historyItems = null)
+ {
+ var mockHistoryService = new Mock();
+ var history = historyItems ?? new List();
+
+ mockHistoryService.Setup(x => x.GetRunHistory())
+ .Returns(() => history.ToList().AsReadOnly());
+
+ mockHistoryService.Setup(x => x.AddRunHistoryItem(It.IsAny()))
+ .Callback(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 CreateMockHistoryServiceWithCommonCommands()
+ {
+ var commonCommands = new List
+ {
+ "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);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/Settings.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/Settings.cs
new file mode 100644
index 0000000000..953f252be8
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/Settings.cs
@@ -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 count;
+
+ public Settings(
+ bool leaveShellOpen = false,
+ string shellCommandExecution = "0",
+ bool runAsAdministrator = false,
+ Dictionary count = null)
+ {
+ this.leaveShellOpen = leaveShellOpen;
+ this.shellCommandExecution = shellCommandExecution;
+ this.runAsAdministrator = runAsAdministrator;
+ this.count = count ?? new Dictionary();
+ }
+
+ public bool LeaveShellOpen => leaveShellOpen;
+
+ public string ShellCommandExecution => shellCommandExecution;
+
+ public bool RunAsAdministrator => runAsAdministrator;
+
+ public Dictionary 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);
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/ShellCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/ShellCommandProviderTests.cs
new file mode 100644
index 0000000000..42fb0900a4
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.Shell.UnitTests/ShellCommandProviderTests.cs
@@ -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();
+ 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();
+ var provider = new ShellCommandsProvider(mockHistoryService.Object);
+
+ // Assert
+ Assert.IsNotNull(provider.Icon);
+ }
+
+ [TestMethod]
+ public void TopLevelCommandsNotEmpty()
+ {
+ // Setup
+ var mockHistoryService = new Mock();
+ var provider = new ShellCommandsProvider(mockHistoryService.Object);
+
+ // Act
+ var commands = provider.TopLevelCommands();
+
+ // Assert
+ Assert.IsNotNull(commands);
+ Assert.IsTrue(commands.Length > 0);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj
new file mode 100644
index 0000000000..d819beb7c7
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/Microsoft.CmdPal.Ext.WebSearch.UnitTests.csproj
@@ -0,0 +1,23 @@
+
+
+
+
+
+ false
+ true
+ Microsoft.CmdPal.Ext.WebSearch.UnitTests
+ $(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal\tests\
+ false
+ false
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockSettingsInterface.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockSettingsInterface.cs
new file mode 100644
index 0000000000..853360674f
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/MockSettingsInterface.cs
@@ -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 _historyItems;
+
+ public bool GlobalIfURI { get; set; }
+
+ public string ShowHistory { get; set; }
+
+ public MockSettingsInterface(string showHistory = "none", bool globalIfUri = true, List mockHistory = null)
+ {
+ _historyItems = mockHistory ?? new List();
+ GlobalIfURI = globalIfUri;
+ ShowHistory = showHistory;
+ }
+
+ public List LoadHistory()
+ {
+ var listItems = new List();
+ 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;
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs
new file mode 100644
index 0000000000..64a5366a61
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/QueryTests.cs
@@ -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
+ {
+ 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
+ {
+ 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
+ {
+ 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);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/WebSearchCommandProviderTests.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/WebSearchCommandProviderTests.cs
new file mode 100644
index 0000000000..c141d28d6e
--- /dev/null
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.Ext.WebSearch.UnitTests/WebSearchCommandProviderTests.cs
@@ -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);
+ }
+}
diff --git a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/CommandPaletteTestBase.cs b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/CommandPaletteTestBase.cs
index ab7dac1a3a..a839c3e999 100644
--- a/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/CommandPaletteTestBase.cs
+++ b/src/modules/cmdpal/Tests/Microsoft.CmdPal.UITests/CommandPaletteTestBase.cs
@@ -19,29 +19,22 @@ public class CommandPaletteTestBase : UITestBase
{
}
- protected void SetSearchBox(string text)
- {
- Assert.AreEqual(this.Find("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("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("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("Search values or type a custom time stamp...").SetText(text, true).Text, text);
+ Assert.AreEqual(this.Find(By.AccessibilityId("MainSearchBox")).SetText(text, true).Text, text);
}
protected void OpenContextMenu()
{
- var contextMenuButton = this.Find
diff --git a/src/modules/imageresizer/dll/ImageResizerExt.vcxproj.filters b/src/modules/imageresizer/dll/ImageResizerExt.vcxproj.filters
index 8ec4d09d40..5d58153793 100644
--- a/src/modules/imageresizer/dll/ImageResizerExt.vcxproj.filters
+++ b/src/modules/imageresizer/dll/ImageResizerExt.vcxproj.filters
@@ -54,6 +54,9 @@
Generated Files
+
+ Header Files
+
diff --git a/src/modules/imageresizer/dll/RuntimeRegistration.h b/src/modules/imageresizer/dll/RuntimeRegistration.h
new file mode 100644
index 0000000000..f9369da1c4
--- /dev/null
+++ b/src/modules/imageresizer/dll/RuntimeRegistration.h
@@ -0,0 +1,38 @@
+// Header-only runtime registration for ImageResizer shell extension.
+#pragma once
+
+#include
+
+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(&__ImageBase));
+ }
+
+ inline void Unregister()
+ {
+ runtime_shell_ext::Unregister(BuildSpec());
+ }
+}
diff --git a/src/modules/imageresizer/dll/dllmain.cpp b/src/modules/imageresizer/dll/dllmain.cpp
index 4786388f06..ef07566b7e 100644
--- a/src/modules/imageresizer/dll/dllmain.cpp
+++ b/src/modules/imageresizer/dll/dllmain.cpp
@@ -14,6 +14,7 @@
#include
#include
#include
+#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
diff --git a/src/modules/launcher/Wox.Plugin/Wox.Plugin.csproj b/src/modules/launcher/Wox.Plugin/Wox.Plugin.csproj
index e9179e6ddf..67f7ead02c 100644
--- a/src/modules/launcher/Wox.Plugin/Wox.Plugin.csproj
+++ b/src/modules/launcher/Wox.Plugin/Wox.Plugin.csproj
@@ -30,6 +30,7 @@
+
diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj b/src/modules/powerrename/dll/PowerRenameExt.vcxproj
index 32484c75b6..9c612a08ff 100644
--- a/src/modules/powerrename/dll/PowerRenameExt.vcxproj
+++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj
@@ -34,6 +34,7 @@
+
diff --git a/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters b/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters
index ffd2177829..f4f0ffcbe5 100644
--- a/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters
+++ b/src/modules/powerrename/dll/PowerRenameExt.vcxproj.filters
@@ -36,6 +36,9 @@
Header Files
+
+ Header Files
+
diff --git a/src/modules/powerrename/dll/RuntimeRegistration.h b/src/modules/powerrename/dll/RuntimeRegistration.h
new file mode 100644
index 0000000000..3cb06d5876
--- /dev/null
+++ b/src/modules/powerrename/dll/RuntimeRegistration.h
@@ -0,0 +1,37 @@
+// Header-only runtime registration for PowerRename context menu extension.
+#pragma once
+
+#include
+
+// 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());
+ }
+}
diff --git a/src/modules/powerrename/dll/dllmain.cpp b/src/modules/powerrename/dll/dllmain.cpp
index 4f9c918fb6..18c612a304 100644
--- a/src/modules/powerrename/dll/dllmain.cpp
+++ b/src/modules/powerrename/dll/dllmain.cpp
@@ -15,6 +15,7 @@
#include
#include
#include
+#include "RuntimeRegistration.h"
#include
@@ -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
diff --git a/src/runner/Resources.resx b/src/runner/Resources.resx
index 3cc2f1ad36..c8eb5f25cc 100644
--- a/src/runner/Resources.resx
+++ b/src/runner/Resources.resx
@@ -176,10 +176,6 @@
Documentation
-
- Exit
- Exit as a verb, as in Exit the application
-
Report bug
@@ -193,4 +189,8 @@
Administrator
+
+ Close
+ Close as a verb, as in Close the application
+
\ No newline at end of file
diff --git a/src/runner/resource.base.h b/src/runner/resource.base.h
index 027f5b4281..7037f4342d 100644
--- a/src/runner/resource.base.h
+++ b/src/runner/resource.base.h
@@ -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
diff --git a/src/runner/runner.base.rc b/src/runner/runner.base.rc
index 10a4555db8..367735ade4 100644
Binary files a/src/runner/runner.base.rc and b/src/runner/runner.base.rc differ
diff --git a/src/runner/tray_icon.cpp b/src/runner/tray_icon.cpp
index 53da747539..749c921659 100644
--- a/src/runner/tray_icon.cpp
+++ b/src/runner/tray_icon.cpp
@@ -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));
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
index 139c396ed1..7658d11237 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/GeneralPage.xaml
@@ -277,9 +277,11 @@
IsEnabled="{x:Bind ViewModel.IsRunAtStartupGPOManaged, Mode=OneWay, Converter={StaticResource BoolNegationConverter}}">
-
-
-
+
+
-
-
-
-
-
+
-
-
-
-
-
-
-
-
-
-
+
@@ -389,5 +369,12 @@
+
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
index 69a7bec01f..e5759678c4 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Views/ShellPage.xaml.cs
@@ -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
///
/// Gets view model.
///
- public ShellViewModel ViewModel { get; } = new ShellViewModel();
+ public ShellViewModel ViewModel { get; }
///
/// 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.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 _lastSearchResults = new();
diff --git a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
index 75e4a50aae..95f4834ea8 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -1,4 +1,4 @@
-
+