diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 2148c78663..43447793a0 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -1279,6 +1279,7 @@ pstm
PStr
pstream
pstrm
+pswd
PSYSTEM
psz
ptb
@@ -1425,6 +1426,7 @@ searchterm
SEARCHUI
SECONDARYDISPLAY
secpol
+securestring
SEEMASKINVOKEIDLIST
SELCHANGE
SENDCHANGE
@@ -1980,4 +1982,12 @@ zoomit
ZOOMITX
ZXk
ZXNs
-zzz
\ No newline at end of file
+zzz
+ACIE
+AOklab
+BCIE
+BOklab
+culori
+Evercoder
+LCh
+CIELCh
diff --git a/.pipelines/tsa.json b/.pipelines/tsa.json
index 351545613f..2f1e84c7f1 100644
--- a/.pipelines/tsa.json
+++ b/.pipelines/tsa.json
@@ -3,5 +3,5 @@
"notificationAliases": ["powertoys@microsoft.com"],
"instanceUrl": "https://microsoft.visualstudio.com",
"projectName": "OS",
- "areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\SHINE\\PowerToys"
+ "areaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\PowerToys"
}
diff --git a/src/common/ManagedCommon/ColorFormatHelper.cs b/src/common/ManagedCommon/ColorFormatHelper.cs
index 08e62b921d..471104f215 100644
--- a/src/common/ManagedCommon/ColorFormatHelper.cs
+++ b/src/common/ManagedCommon/ColorFormatHelper.cs
@@ -141,6 +141,40 @@ namespace ManagedCommon
return lab;
}
+ ///
+ /// Convert a given to a Oklab color
+ ///
+ /// The to convert
+ /// The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]
+ public static (double Lightness, double ChromaticityA, double ChromaticityB) ConvertToOklabColor(Color color)
+ {
+ var linear = ConvertSRGBToLinearRGB(color.R / 255d, color.G / 255d, color.B / 255d);
+ var oklab = GetOklabColorFromLinearRGB(linear.R, linear.G, linear.B);
+ return oklab;
+ }
+
+ ///
+ /// Convert a given to a Oklch color
+ ///
+ /// The to convert
+ /// The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]
+ public static (double Lightness, double Chroma, double Hue) ConvertToOklchColor(Color color)
+ {
+ var oklab = ConvertToOklabColor(color);
+ var oklch = GetOklchColorFromOklab(oklab.Lightness, oklab.ChromaticityA, oklab.ChromaticityB);
+
+ return oklch;
+ }
+
+ public static (double R, double G, double B) ConvertSRGBToLinearRGB(double r, double g, double b)
+ {
+ // inverse companding, gamma correction must be undone
+ double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
+ double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
+ double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
+ return (rLinear, gLinear, bLinear);
+ }
+
///
/// Convert a given to a CIE XYZ color (XYZ)
/// The constants of the formula matches this Wikipedia page, but at a higher precision:
@@ -156,10 +190,7 @@ namespace ManagedCommon
double g = color.G / 255d;
double b = color.B / 255d;
- // inverse companding, gamma correction must be undone
- double rLinear = (r > 0.04045) ? Math.Pow((r + 0.055) / 1.055, 2.4) : (r / 12.92);
- double gLinear = (g > 0.04045) ? Math.Pow((g + 0.055) / 1.055, 2.4) : (g / 12.92);
- double bLinear = (b > 0.04045) ? Math.Pow((b + 0.055) / 1.055, 2.4) : (b / 12.92);
+ (double rLinear, double gLinear, double bLinear) = ConvertSRGBToLinearRGB(r, g, b);
return (
(rLinear * 0.41239079926595948) + (gLinear * 0.35758433938387796) + (bLinear * 0.18048078840183429),
@@ -210,6 +241,63 @@ namespace ManagedCommon
return (l, a, b);
}
+ ///
+ /// Convert a linear RGB color to an Oklab color.
+ /// The constants of this formula come from https://github.com/Evercoder/culori/blob/2bedb8f0507116e75f844a705d0b45cf279b15d0/src/oklab/convertLrgbToOklab.js
+ /// and the implementation is based on https://bottosson.github.io/posts/oklab/
+ ///
+ /// Linear R value
+ /// Linear G value
+ /// Linear B value
+ /// The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]
+ private static (double Lightness, double ChromaticityA, double ChromaticityB)
+ GetOklabColorFromLinearRGB(double r, double g, double b)
+ {
+ double l = (0.41222147079999993 * r) + (0.5363325363 * g) + (0.0514459929 * b);
+ double m = (0.2119034981999999 * r) + (0.6806995450999999 * g) + (0.1073969566 * b);
+ double s = (0.08830246189999998 * r) + (0.2817188376 * g) + (0.6299787005000002 * b);
+
+ double l_ = Math.Cbrt(l);
+ double m_ = Math.Cbrt(m);
+ double s_ = Math.Cbrt(s);
+
+ return (
+ (0.2104542553 * l_) + (0.793617785 * m_) - (0.0040720468 * s_),
+ (1.9779984951 * l_) - (2.428592205 * m_) + (0.4505937099 * s_),
+ (0.0259040371 * l_) + (0.7827717662 * m_) - (0.808675766 * s_)
+ );
+ }
+
+ ///
+ /// Convert an Oklab color from Cartesian form to its polar form Oklch
+ /// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
+ ///
+ /// The
+ /// The
+ /// The
+ /// The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]
+ private static (double Lightness, double Chroma, double Hue)
+ GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
+ {
+ return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
+ }
+
+ ///
+ /// Convert a color in Cartesian form (Lab) to its polar form (LCh)
+ ///
+ /// The
+ /// The
+ /// The
+ /// The lightness, chroma, and hue angle
+ private static (double Lightness, double Chroma, double Hue)
+ GetLCHColorFromLAB(double lightness, double chromaticity_a, double chromaticity_b)
+ {
+ // Lab to LCh transformation
+ double chroma = Math.Sqrt(Math.Pow(chromaticity_a, 2) + Math.Pow(chromaticity_b, 2));
+ double hue = Math.Round(chroma, 3) == 0 ? 0.0 : ((Math.Atan2(chromaticity_b, chromaticity_a) * 180d / Math.PI) + 360d) % 360d;
+ return (lightness, chroma, hue);
+ }
+
///
/// Convert a given to a natural color (hue, whiteness, blackness)
///
@@ -276,12 +364,17 @@ namespace ManagedCommon
{ "Br", 'p' }, // brightness percent
{ "In", 'p' }, // intensity percent
{ "Ll", 'p' }, // lightness (HSL) percent
- { "Lc", 'p' }, // lightness(CIELAB)percent
{ "Va", 'p' }, // value percent
{ "Wh", 'p' }, // whiteness percent
{ "Bn", 'p' }, // blackness percent
- { "Ca", 'p' }, // chromaticityA percent
- { "Cb", 'p' }, // chromaticityB percent
+ { "Lc", 'p' }, // lightness (CIE) percent
+ { "Ca", 'p' }, // chromaticityA (CIELAB) percent
+ { "Cb", 'p' }, // chromaticityB (CIELAB) percent
+ { "Lo", 'p' }, // lightness (Oklab/Oklch) percent
+ { "Oa", 'p' }, // chromaticityA (Oklab) percent
+ { "Ob", 'p' }, // chromaticityB (Oklab) percent
+ { "Oc", 'p' }, // chroma (Oklch) percent
+ { "Oh", 'p' }, // hue angle (Oklch) percent
{ "Xv", 'i' }, // X value int
{ "Yv", 'i' }, // Y value int
{ "Zv", 'i' }, // Z value int
@@ -424,6 +517,10 @@ namespace ManagedCommon
var (lightnessC, _, _) = ConvertToCIELABColor(color);
lightnessC = Math.Round(lightnessC, 2);
return lightnessC.ToString(CultureInfo.InvariantCulture);
+ case "Lo":
+ var (lightnessO, _, _) = ConvertToOklabColor(color);
+ lightnessO = Math.Round(lightnessO, 2);
+ return lightnessO.ToString(CultureInfo.InvariantCulture);
case "Wh":
var (_, whiteness, _) = ConvertToHWBColor(color);
whiteness = Math.Round(whiteness * 100);
@@ -440,6 +537,22 @@ namespace ManagedCommon
var (_, _, chromaticityB) = ConvertToCIELABColor(color);
chromaticityB = Math.Round(chromaticityB, 2);
return chromaticityB.ToString(CultureInfo.InvariantCulture);
+ case "Oa":
+ var (_, chromaticityAOklab, _) = ConvertToOklabColor(color);
+ chromaticityAOklab = Math.Round(chromaticityAOklab, 2);
+ return chromaticityAOklab.ToString(CultureInfo.InvariantCulture);
+ case "Ob":
+ var (_, _, chromaticityBOklab) = ConvertToOklabColor(color);
+ chromaticityBOklab = Math.Round(chromaticityBOklab, 2);
+ return chromaticityBOklab.ToString(CultureInfo.InvariantCulture);
+ case "Oc":
+ var (_, chromaOklch, _) = ConvertToOklchColor(color);
+ chromaOklch = Math.Round(chromaOklch, 2);
+ return chromaOklch.ToString(CultureInfo.InvariantCulture);
+ case "Oh":
+ var (_, _, hueOklch) = ConvertToOklchColor(color);
+ hueOklch = Math.Round(hueOklch, 2);
+ return hueOklch.ToString(CultureInfo.InvariantCulture);
case "Xv":
var (x, _, _) = ConvertToCIEXYZColor(color);
x = Math.Round(x * 100, 4);
@@ -495,8 +608,10 @@ namespace ManagedCommon
case "HSI": return "hsi(%Hu, %Si%, %In%)";
case "HWB": return "hwb(%Hu, %Wh%, %Bn%)";
case "NCol": return "%Hn, %Wh%, %Bn%";
- case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
case "CIEXYZ": return "XYZ(%Xv, %Yv, %Zv)";
+ case "CIELAB": return "CIELab(%Lc, %Ca, %Cb)";
+ case "Oklab": return "oklab(%Lo, %Oa, %Ob)";
+ case "Oklch": return "oklch(%Lo, %Oc, %Oh)";
case "VEC4": return "(%Reff, %Grff, %Blff, 1f)";
case "Decimal": return "%Dv";
case "HEX Int": return "0xFF%ReX%GrX%BlX";
diff --git a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs
index b56868ece8..218349b32b 100644
--- a/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs
+++ b/src/modules/AdvancedPaste/AdvancedPaste/Helpers/OcrHelpers.cs
@@ -18,10 +18,19 @@ public static class OcrHelpers
{
public static async Task ExtractTextAsync(SoftwareBitmap bitmap, CancellationToken cancellationToken)
{
- var ocrLanguage = GetOCRLanguage() ?? throw new InvalidOperationException("Unable to determine OCR language");
+ var ocrLanguage = GetOCRLanguage();
cancellationToken.ThrowIfCancellationRequested();
- var ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine");
+ OcrEngine ocrEngine;
+ if (ocrLanguage is not null)
+ {
+ ocrEngine = OcrEngine.TryCreateFromLanguage(ocrLanguage) ?? throw new InvalidOperationException("Unable to create OCR engine from specified language");
+ }
+ else
+ {
+ ocrEngine = OcrEngine.TryCreateFromUserProfileLanguages() ?? throw new InvalidOperationException("Unable to create OCR engine from user profile language");
+ }
+
cancellationToken.ThrowIfCancellationRequested();
var ocrResult = await ocrEngine.RecognizeAsync(bitmap);
diff --git a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
index 5bd12f316e..2d70013009 100644
--- a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
+++ b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
@@ -2,7 +2,7 @@
-
+
17.0
Win32Proj
@@ -53,7 +53,6 @@
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
%(PreprocessorDefinitions);
- $(CommandPaletteBranding)
IS_DEV_BRANDING;%(PreprocessorDefinitions)
diff --git a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
index be3eb6a3b7..bff7279b68 100644
--- a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
+++ b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
@@ -3,6 +3,7 @@
#include
+#include
#include
#include
#include
@@ -10,10 +11,11 @@
#include
#include
#include
+#include
#include
#include
#include
-#include
+#include
HINSTANCE g_hInst_cmdPal = 0;
@@ -37,8 +39,6 @@ BOOL APIENTRY DllMain(HMODULE hInstance,
class CmdPal : public PowertoyModuleIface
{
private:
- bool m_enabled = false;
-
std::wstring app_name;
//contains the non localized key of the powertoy
@@ -46,7 +46,10 @@ private:
HANDLE m_hTerminateEvent;
- void LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated)
+ // Track if this is the first call to enable
+ bool firstEnableCall = true;
+
+ static bool LaunchApp(const std::wstring& appPath, const std::wstring& commandLineArgs, bool elevated, bool silentFail)
{
std::wstring dir = std::filesystem::path(appPath).parent_path();
@@ -54,6 +57,10 @@ private:
sei.cbSize = sizeof(SHELLEXECUTEINFO);
sei.hwnd = nullptr;
sei.fMask = SEE_MASK_NOCLOSEPROCESS | SEE_MASK_NO_CONSOLE;
+ if (silentFail)
+ {
+ sei.fMask = sei.fMask | SEE_MASK_FLAG_NO_UI;
+ }
sei.lpVerb = elevated ? L"runas" : L"open";
sei.lpFile = appPath.c_str();
sei.lpParameters = commandLineArgs.c_str();
@@ -64,7 +71,11 @@ private:
{
std::wstring error = get_last_error_or_default(GetLastError());
Logger::error(L"Failed to launch process. {}", error);
+ return false;
}
+
+ m_launched.store(true);
+ return true;
}
std::vector GetProcessesIdByName(const std::wstring& processName)
@@ -122,6 +133,9 @@ private:
}
public:
+ static std::atomic m_enabled;
+ static std::atomic m_launched;
+
CmdPal()
{
app_name = L"CmdPal";
@@ -133,10 +147,7 @@ public:
~CmdPal()
{
- if (m_enabled)
- {
- }
- m_enabled = false;
+ CmdPal::m_enabled.store(false);
}
// Destroy the powertoy and free memory
@@ -203,15 +214,18 @@ public:
{
Logger::trace("CmdPal::enable()");
- m_enabled = true;
+ CmdPal::m_enabled.store(true);
- try
- {
- std::wstring packageName = L"Microsoft.CommandPalette";
+ std::wstring packageName = L"Microsoft.CommandPalette";
+ std::wstring launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette_8wekyb3d8bbwe!App";
#ifdef IS_DEV_BRANDING
- packageName = L"Microsoft.CommandPalette.Dev";
+ packageName = L"Microsoft.CommandPalette.Dev";
+ launchPath = L"shell:AppsFolder\\Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App";
#endif
- if (!package::GetRegisteredPackage(packageName, false).has_value())
+
+ if (!package::GetRegisteredPackage(packageName, false).has_value())
+ {
+ try
{
Logger::info(L"CmdPal not installed. Installing...");
@@ -238,28 +252,34 @@ public:
}
}
}
+ catch (std::exception& e)
+ {
+ std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
+ errorMessage += e.what();
+ Logger::error(errorMessage);
+ }
}
- catch (std::exception& e)
+
+ if (!package::GetRegisteredPackage(packageName, false).has_value())
{
- std::string errorMessage{ "Exception thrown while trying to install CmdPal package: " };
- errorMessage += e.what();
- Logger::error(errorMessage);
+ Logger::error("Cmdpal is not registered, quit..");
+ return;
}
- try
+
+ if (!firstEnableCall)
{
-#ifdef IS_DEV_BRANDING
- LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
-#else
- LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
-#endif
+ Logger::trace("Not first attempt, try to launch");
+ LaunchApp(launchPath, L"RunFromPT", false /*no elevated*/, false /*error pop up*/);
}
- catch (std::exception& e)
+ else
{
- std::string errorMessage{ "Exception thrown while trying to launch CmdPal: " };
- errorMessage += e.what();
- Logger::error(errorMessage);
- throw;
+ // If first time enable, do retry launch.
+ Logger::trace("First attempt, try to launch");
+ std::thread launchThread(&CmdPal::RetryLaunch, launchPath);
+ launchThread.detach();
}
+
+ firstEnableCall = false;
}
virtual void disable()
@@ -267,7 +287,44 @@ public:
Logger::trace("CmdPal::disable()");
TerminateCmdPal();
- m_enabled = false;
+ CmdPal::m_enabled.store(false);
+ }
+
+ static void RetryLaunch(std::wstring path)
+ {
+ const int base_delay_milliseconds = 1000;
+ int max_retry = 9; // 2**9 - 1 seconds. Control total wait time within 10 min.
+ int retry = 0;
+ do
+ {
+ auto launch_result = LaunchApp(path, L"RunFromPT", false, retry < max_retry);
+ if (launch_result)
+ {
+ Logger::info(L"CmdPal launched successfully after {} retries.", retry);
+ return;
+ }
+ else
+ {
+ Logger::error(L"Retry {} launch CmdPal launch failed.", retry);
+ }
+
+ // When we got max retry, we don't need to wait for the next retry.
+ if (retry < max_retry)
+ {
+ int delay = base_delay_milliseconds * (1 << (retry));
+ std::this_thread::sleep_for(std::chrono::milliseconds(delay));
+ }
+ ++retry;
+ } while (retry <= max_retry && m_enabled.load() && !m_launched.load());
+
+ if (!m_enabled.load() || m_launched.load())
+ {
+ Logger::error(L"Retry cancelled. CmdPal is disabled or already launched.");
+ }
+ else
+ {
+ Logger::error(L"CmdPal launch failed after {} attempts.", retry);
+ }
}
virtual bool on_hotkey(size_t) override
@@ -282,11 +339,14 @@ public:
virtual bool is_enabled() override
{
- return m_enabled;
+ return CmdPal::m_enabled.load();
}
};
+std::atomic CmdPal::m_enabled{ false };
+std::atomic CmdPal::m_launched{ false };
+
extern "C" __declspec(dllexport) PowertoyModuleIface* __cdecl powertoy_create()
{
return new CmdPal();
-}
+}
\ No newline at end of file
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs
deleted file mode 100644
index b9348eb520..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs
+++ /dev/null
@@ -1,14 +0,0 @@
-// 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.Common.Contracts;
-
-public interface IFileService
-{
- T Read(string folderPath, string fileName);
-
- void Save(string folderPath, string fileName, T content);
-
- void Delete(string folderPath, string fileName);
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs
deleted file mode 100644
index 2350050e3e..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs
+++ /dev/null
@@ -1,16 +0,0 @@
-// Copyright (c) Microsoft Corporation
-// The Microsoft Corporation licenses this file to you under the MIT license.
-// See the LICENSE file in the project root for more information.
-
-using System.Threading.Tasks;
-
-namespace Microsoft.CmdPal.Common.Contracts;
-
-public interface ILocalSettingsService
-{
- Task HasSettingAsync(string key);
-
- Task ReadSettingAsync(string key);
-
- Task SaveSettingAsync(string key, T value);
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs
deleted file mode 100644
index a975083c7c..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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.UI.Xaml;
-
-namespace Microsoft.CmdPal.Common.Extensions;
-
-///
-/// Extension class implementing extension methods for .
-///
-public static class ApplicationExtensions
-{
- ///
- /// Get registered services at the application level from anywhere in the
- /// application.
- ///
- /// Note:
- /// https://learn.microsoft.com/uwp/api/windows.ui.xaml.application.current?view=winrt-22621#windows-ui-xaml-application-current
- /// "Application is a singleton that implements the static Current property
- /// to provide shared access to the Application instance for the current
- /// application. The singleton pattern ensures that state managed by
- /// Application, including shared resources and properties, is available
- /// from a single, shared location."
- ///
- /// Example of usage:
- ///
- /// Application.Current.GetService()
- ///
- ///
- /// Service type.
- /// Current application.
- /// Service reference.
- public static T GetService(this Application application)
- where T : class
- {
- return (application as IApp)!.GetService();
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs
deleted file mode 100644
index 660dcd2931..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs
+++ /dev/null
@@ -1,40 +0,0 @@
-// 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 Microsoft.Extensions.DependencyInjection;
-using Microsoft.Extensions.Hosting;
-
-namespace Microsoft.CmdPal.Common.Extensions;
-
-public static class IHostExtensions
-{
- ///
- ///
- ///
- public static T CreateInstance(this IHost host, params object[] parameters)
- {
- return ActivatorUtilities.CreateInstance(host.Services, parameters);
- }
-
- ///
- /// Gets the service object for the specified type, or throws an exception
- /// if type was not registered.
- ///
- /// Service type
- /// Host object
- /// Service object
- /// Throw an exception if the specified
- /// type is not registered
- public static T GetService(this IHost host)
- where T : class
- {
- if (host.Services.GetService(typeof(T)) is not T service)
- {
- throw new ArgumentException($"{typeof(T)} needs to be registered in ConfigureServices.");
- }
-
- return service;
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs
deleted file mode 100644
index d865e10bdb..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs
+++ /dev/null
@@ -1,36 +0,0 @@
-// 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.IO;
-using System.Text;
-using System.Text.Json;
-using System.Threading.Tasks;
-
-namespace Microsoft.CmdPal.Common.Helpers;
-
-public static class Json
-{
- public static async Task ToObjectAsync(string value)
- {
- if (typeof(T) == typeof(bool))
- {
- return (T)(object)bool.Parse(value);
- }
-
- await using var stream = new MemoryStream(Encoding.UTF8.GetBytes(value));
- return (await JsonSerializer.DeserializeAsync(stream))!;
- }
-
- public static async Task StringifyAsync(T value)
- {
- if (typeof(T) == typeof(bool))
- {
- return value!.ToString()!.ToLowerInvariant();
- }
-
- await using var stream = new MemoryStream();
- await JsonSerializer.SerializeAsync(stream, value);
- return Encoding.UTF8.GetString(stream.ToArray());
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs
index 6ec1885a4c..2344fbb917 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/NativeEventWaiter.cs
@@ -9,7 +9,7 @@ using Microsoft.UI.Dispatching;
namespace Microsoft.CmdPal.Common.Helpers;
-public static class NativeEventWaiter
+public static partial class NativeEventWaiter
{
public static void WaitForEventLoop(string eventName, Action callback)
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs
index 46dce07e5e..342667cf83 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/RuntimeHelper.cs
@@ -9,7 +9,7 @@ using Windows.Win32.Foundation;
namespace Microsoft.CmdPal.Common.Helpers;
-public static class RuntimeHelper
+public static partial class RuntimeHelper
{
public static bool IsMSIX
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs
index ed698d1024..097aefdee9 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Messages/HideWindowMessage.cs
@@ -4,6 +4,6 @@
namespace Microsoft.CmdPal.Common.Messages;
-public record HideWindowMessage()
+public partial record HideWindowMessage()
{
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj
index 970df0df58..5f83ca54e1 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/Microsoft.CmdPal.Common.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Common
enable
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs
deleted file mode 100644
index bae7422878..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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.Common.Models;
-
-public class LocalSettingsOptions
-{
- public string? ApplicationDataFolder
- {
- get; set;
- }
-
- public string? LocalSettingsFile
- {
- get; set;
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt
index 0d456bde31..996bbd7153 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt
+++ b/src/modules/cmdpal/Microsoft.CmdPal.Common/NativeMethods.txt
@@ -1,16 +1,7 @@
-EnableWindow
-CoCreateInstance
-FileOpenDialog
-FileSaveDialog
-IFileOpenDialog
-IFileSaveDialog
-SHCreateItemFromParsingName
GetCurrentPackageFullName
SetWindowLong
GetWindowLong
WINDOW_EX_STYLE
-SHLoadIndirectString
-StrFormatByteSizeEx
SFBS_FLAGS
MAX_PATH
GetDpiForWindow
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs
deleted file mode 100644
index cc6ef96098..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs
+++ /dev/null
@@ -1,48 +0,0 @@
-// 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.IO;
-using System.Text;
-using System.Text.Json;
-using Microsoft.CmdPal.Common.Contracts;
-
-namespace Microsoft.CmdPal.Common.Services;
-
-public class FileService : IFileService
-{
- private static readonly Encoding _encoding = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false);
-
-#pragma warning disable CS8603 // Possible null reference return.
- public T Read(string folderPath, string fileName)
- {
- var path = Path.Combine(folderPath, fileName);
- if (File.Exists(path))
- {
- using var fileStream = File.OpenText(path);
- return JsonSerializer.Deserialize(fileStream.BaseStream);
- }
-
- return default;
- }
-#pragma warning restore CS8603 // Possible null reference return.
-
- public void Save(string folderPath, string fileName, T content)
- {
- if (!Directory.Exists(folderPath))
- {
- Directory.CreateDirectory(folderPath);
- }
-
- var fileContent = JsonSerializer.Serialize(content);
- File.WriteAllText(Path.Combine(folderPath, fileName), fileContent, _encoding);
- }
-
- public void Delete(string folderPath, string fileName)
- {
- if (fileName != null && File.Exists(Path.Combine(folderPath, fileName)))
- {
- File.Delete(Path.Combine(folderPath, fileName));
- }
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs
deleted file mode 100644
index 92980dfaff..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs
+++ /dev/null
@@ -1,18 +0,0 @@
-// 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.Common.Services;
-
-///
-/// Interface for the current application singleton object exposing the API
-/// that can be accessed from anywhere in the application.
-///
-public interface IApp
-{
- ///
- /// Gets services registered at the application level.
- ///
- public T GetService()
- where T : class;
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs b/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs
deleted file mode 100644
index e4cd2a174b..0000000000
--- a/src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs
+++ /dev/null
@@ -1,120 +0,0 @@
-// 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.IO;
-using System.Threading.Tasks;
-using Microsoft.CmdPal.Common.Contracts;
-using Microsoft.CmdPal.Common.Helpers;
-using Microsoft.CmdPal.Common.Models;
-using Microsoft.Extensions.Options;
-using Windows.Storage;
-
-namespace Microsoft.CmdPal.Common.Services;
-
-public class LocalSettingsService : ILocalSettingsService
-{
- // TODO! for now, we're hardcoding the path as effectively:
- // %localappdata%\CmdPal\LocalSettings.json
- private const string DefaultApplicationDataFolder = "CmdPal";
- private const string DefaultLocalSettingsFile = "LocalSettings.json";
-
- private readonly IFileService _fileService;
- private readonly LocalSettingsOptions _options;
-
- private readonly string _localApplicationData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
- private readonly string _applicationDataFolder;
- private readonly string _localSettingsFile;
-
- private readonly bool _isMsix;
-
- private Dictionary _settings;
- private bool _isInitialized;
-
- public LocalSettingsService(IFileService fileService, IOptions options)
- {
- _isMsix = false; // RuntimeHelper.IsMSIX;
-
- _fileService = fileService;
- _options = options.Value;
-
- _applicationDataFolder = Path.Combine(_localApplicationData, _options.ApplicationDataFolder ?? DefaultApplicationDataFolder);
- _localSettingsFile = _options.LocalSettingsFile ?? DefaultLocalSettingsFile;
-
- _settings = new Dictionary();
- }
-
- private async Task InitializeAsync()
- {
- if (!_isInitialized)
- {
- _settings = await Task.Run(() => _fileService.Read>(_applicationDataFolder, _localSettingsFile)) ?? new Dictionary();
-
- _isInitialized = true;
- }
- }
-
- public async Task HasSettingAsync(string key)
- {
- if (_isMsix)
- {
- return ApplicationData.Current.LocalSettings.Values.ContainsKey(key);
- }
- else
- {
- await InitializeAsync();
-
- if (_settings != null)
- {
- return _settings.ContainsKey(key);
- }
- }
-
- return false;
- }
-
- public async Task ReadSettingAsync(string key)
- {
- if (_isMsix)
- {
- if (ApplicationData.Current.LocalSettings.Values.TryGetValue(key, out var obj))
- {
- return await Json.ToObjectAsync((string)obj);
- }
- }
- else
- {
- await InitializeAsync();
-
- if (_settings != null && _settings.TryGetValue(key, out var obj))
- {
- var s = obj.ToString();
-
- if (s != null)
- {
- return await Json.ToObjectAsync(s);
- }
- }
- }
-
- return default;
- }
-
- public async Task SaveSettingAsync(string key, T value)
- {
- if (_isMsix)
- {
- ApplicationData.Current.LocalSettings.Values[key] = await Json.StringifyAsync(value!);
- }
- else
- {
- await InitializeAsync();
-
- _settings[key] = await Json.StringifyAsync(value!);
-
- await Task.Run(() => _fileService.Save(_applicationDataFolder, _localSettingsFile, _settings));
- }
- }
-}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs
index 69d38a8655..649e49fbc7 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/AppStateModel.cs
@@ -49,7 +49,7 @@ public partial class AppStateModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
- var loaded = JsonSerializer.Deserialize(jsonContent, _deserializerOptions);
+ var loaded = JsonSerializer.Deserialize(jsonContent, JsonSerializationContext.Default.AppStateModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -73,7 +73,7 @@ public partial class AppStateModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
- var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
+ var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.AppStateModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -89,7 +89,7 @@ public partial class AppStateModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
- var serialized = savedSettings.ToJsonString(_serializerOptions);
+ var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.AppStateModel.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -122,18 +122,19 @@ public partial class AppStateModel : ObservableObject
return Path.Combine(directory, "state.json");
}
- private static readonly JsonSerializerOptions _serializerOptions = new()
- {
- WriteIndented = true,
- Converters = { new JsonStringEnumConverter() },
- };
+ // [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
+ // private static readonly JsonSerializerOptions _serializerOptions = new()
+ // {
+ // WriteIndented = true,
+ // Converters = { new JsonStringEnumConverter() },
+ // };
- private static readonly JsonSerializerOptions _deserializerOptions = new()
- {
- PropertyNameCaseInsensitive = true,
- IncludeFields = true,
- AllowTrailingCommas = true,
- PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
- ReadCommentHandling = JsonCommentHandling.Skip,
- };
+ // private static readonly JsonSerializerOptions _deserializerOptions = new()
+ // {
+ // PropertyNameCaseInsensitive = true,
+ // IncludeFields = true,
+ // AllowTrailingCommas = true,
+ // PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate,
+ // ReadCommentHandling = JsonCommentHandling.Skip,
+ // };
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
index a476fba179..06f55ccf02 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
@@ -4,18 +4,14 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
-using CommunityToolkit.Mvvm.Input;
using CommunityToolkit.Mvvm.Messaging;
using Microsoft.CmdPal.UI.ViewModels.Messages;
-using Microsoft.CommandPalette.Extensions;
-using Microsoft.CommandPalette.Extensions.Toolkit;
using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels;
public partial class CommandBarViewModel : ObservableObject,
- IRecipient,
- IRecipient
+ IRecipient
{
public ICommandBarContext? SelectedItem
{
@@ -53,20 +49,17 @@ public partial class CommandBarViewModel : ObservableObject,
public partial PageViewModel? CurrentPage { get; set; }
[ObservableProperty]
- public partial ObservableCollection ContextCommands { get; set; } = [];
+ public partial ObservableCollection ContextMenuStack { get; set; } = [];
- private Dictionary? _contextKeybindings;
+ public ContextMenuStackViewModel? ContextMenu => ContextMenuStack.LastOrDefault();
public CommandBarViewModel()
{
WeakReferenceMessenger.Default.Register(this);
- WeakReferenceMessenger.Default.Register(this);
}
public void Receive(UpdateCommandBarMessage message) => SelectedItem = message.ViewModel;
- public void Receive(UpdateItemKeybindingsMessage message) => _contextKeybindings = message.Keys;
-
private void SetSelectedItem(ICommandBarContext? value)
{
if (value != null)
@@ -111,7 +104,10 @@ public partial class CommandBarViewModel : ObservableObject,
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
- ContextCommands = [.. SelectedItem.AllCommands.Where(c => c.ShouldBeVisible)];
+
+ ContextMenuStack.Clear();
+ ContextMenuStack.Add(new ContextMenuStackViewModel(SelectedItem));
+ OnPropertyChanged(nameof(ContextMenu));
}
else
{
@@ -125,43 +121,80 @@ public partial class CommandBarViewModel : ObservableObject,
// InvokeItemCommand is what this will be in Xaml due to source generator
// this comes in when an item in the list is tapped
- [RelayCommand]
- private void InvokeItem(CommandContextItemViewModel item) =>
- WeakReferenceMessenger.Default.Send(new(item.Command.Model, item.Model));
+ // [RelayCommand]
+ public ContextKeybindingResult InvokeItem(CommandContextItemViewModel item) =>
+ PerformCommand(item);
// this comes in when the primary button is tapped
public void InvokePrimaryCommand()
{
- if (PrimaryCommand != null)
- {
- WeakReferenceMessenger.Default.Send(new(PrimaryCommand.Command.Model, PrimaryCommand.Model));
- }
+ PerformCommand(SecondaryCommand);
}
// this comes in when the secondary button is tapped
public void InvokeSecondaryCommand()
{
- if (SecondaryCommand != null)
+ PerformCommand(SecondaryCommand);
+ }
+
+ public ContextKeybindingResult CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
+ {
+ var matchedItem = ContextMenu?.CheckKeybinding(ctrl, alt, shift, win, key);
+ return matchedItem != null ? PerformCommand(matchedItem) : ContextKeybindingResult.Unhandled;
+ }
+
+ private ContextKeybindingResult PerformCommand(CommandItemViewModel? command)
+ {
+ if (command == null)
{
- WeakReferenceMessenger.Default.Send(new(SecondaryCommand.Command.Model, SecondaryCommand.Model));
+ return ContextKeybindingResult.Unhandled;
+ }
+
+ if (command.HasMoreCommands)
+ {
+ ContextMenuStack.Add(new ContextMenuStackViewModel(command));
+ OnPropertyChanging(nameof(ContextMenu));
+ OnPropertyChanged(nameof(ContextMenu));
+ return ContextKeybindingResult.KeepOpen;
+ }
+ else
+ {
+ WeakReferenceMessenger.Default.Send(new(command.Command.Model, command.Model));
+ return ContextKeybindingResult.Hide;
}
}
- public bool CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
+ public bool CanPopContextStack()
{
- if (_contextKeybindings != null)
+ return ContextMenuStack.Count > 1;
+ }
+
+ public void PopContextStack()
+ {
+ if (ContextMenuStack.Count > 1)
{
- // Does the pressed key match any of the keybindings?
- var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
- if (_contextKeybindings.TryGetValue(pressedKeyChord, out var item))
- {
- // TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
- // so that the correct item is activated.
- WeakReferenceMessenger.Default.Send(new(item));
- return true;
- }
+ ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
}
- return false;
+ OnPropertyChanging(nameof(ContextMenu));
+ OnPropertyChanged(nameof(ContextMenu));
+ }
+
+ public void ClearContextStack()
+ {
+ while (ContextMenuStack.Count > 1)
+ {
+ ContextMenuStack.RemoveAt(ContextMenuStack.Count - 1);
+ }
+
+ OnPropertyChanging(nameof(ContextMenu));
+ OnPropertyChanged(nameof(ContextMenu));
}
}
+
+public enum ContextKeybindingResult
+{
+ Unhandled,
+ Hide,
+ KeepOpen,
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
index 8634b63278..24dd9e1788 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs
@@ -48,7 +48,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
public List MoreCommands { get; private set; } = [];
- IEnumerable ICommandBarContext.MoreCommands => MoreCommands;
+ IEnumerable IContextMenuContext.MoreCommands => MoreCommands;
public bool HasMoreCommands => MoreCommands.Count > 0;
@@ -187,23 +187,26 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
// use Initialize straight up
MoreCommands.ForEach(contextItem =>
{
- contextItem.InitializeProperties();
+ contextItem.SlowInitializeProperties();
});
- _defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
+ if (!string.IsNullOrEmpty(model.Command.Name))
{
- _itemTitle = Name,
- Subtitle = Subtitle,
- Command = Command,
+ _defaultCommandContextItem = new(new CommandContextItem(model.Command!), PageContext)
+ {
+ _itemTitle = Name,
+ Subtitle = Subtitle,
+ Command = Command,
- // TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
- };
+ // TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever
+ };
- // Only set the icon on the context item for us if our command didn't
- // have its own icon
- if (!Command.HasIcon)
- {
- _defaultCommandContextItem._listItemIcon = _listItemIcon;
+ // Only set the icon on the context item for us if our command didn't
+ // have its own icon
+ if (!Command.HasIcon)
+ {
+ _defaultCommandContextItem._listItemIcon = _listItemIcon;
+ }
}
Initialized |= InitializedState.SelectionInitialized;
@@ -398,23 +401,6 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel, ICommandBa
base.SafeCleanup();
Initialized |= InitializedState.CleanedUp;
}
-
- ///
- /// Generates a mapping of key -> command item for this particular item's
- /// MoreCommands. (This won't include the primary Command, but it will
- /// include the secondary one). This map can be used to quickly check if a
- /// shortcut key was pressed
- ///
- /// a dictionary of KeyChord -> Context commands, for all commands
- /// that have a shortcut key set.
- internal Dictionary Keybindings()
- {
- return MoreCommands
- .Where(c => c.HasRequestedShortcut)
- .ToDictionary(
- c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
- c => c);
- }
}
[Flags]
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs
index 1939162662..44bcb49cb3 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/CreatedExtensionForm.cs
@@ -13,12 +13,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
{
public CreatedExtensionForm(string name, string displayName, string path)
{
+ var serializeString = (string? s) => JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
TemplateJson = CardTemplate;
DataJson = $$"""
{
- "name": {{JsonSerializer.Serialize(name)}},
- "directory": {{JsonSerializer.Serialize(path)}},
- "displayName": {{JsonSerializer.Serialize(displayName)}}
+ "name": {{serializeString(name)}},
+ "directory": {{serializeString(path)}},
+ "displayName": {{serializeString(displayName)}}
}
""";
_name = name;
@@ -28,13 +29,13 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
public override ICommandResult SubmitForm(string inputs, string data)
{
- JsonObject? dataInput = JsonNode.Parse(data)?.AsObject();
+ var dataInput = JsonNode.Parse(data)?.AsObject();
if (dataInput == null)
{
return CommandResult.KeepOpen();
}
- string verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
+ var verb = dataInput["x"]?.AsValue()?.ToString() ?? string.Empty;
return verb switch
{
"sln" => OpenSolution(),
@@ -47,7 +48,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenSolution()
{
string[] parts = [_path, _name, $"{_name}.sln"];
- string pathToSolution = Path.Combine(parts);
+ var pathToSolution = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToSolution);
return CommandResult.Hide();
}
@@ -55,7 +56,7 @@ internal sealed partial class CreatedExtensionForm : NewExtensionFormBase
private ICommandResult OpenDirectory()
{
string[] parts = [_path, _name];
- string pathToDir = Path.Combine(parts);
+ var pathToDir = Path.Combine(parts);
ShellHelpers.OpenInShell(pathToDir);
return CommandResult.Hide();
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs
index 698faf0335..aca45f3494 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Commands/NewExtensionForm.cs
@@ -194,9 +194,8 @@ internal sealed partial class NewExtensionForm : NewExtensionFormBase
Directory.Delete(tempDir, true);
}
- private string FormatJsonString(string str)
- {
+ private string FormatJsonString(string str) =>
+
// Escape the string for JSON
- return JsonSerializer.Serialize(str);
- }
+ JsonSerializer.Serialize(str, JsonSerializationContext.Default.String);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
index e1bbe0b604..5af1959cd6 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentFormViewModel.cs
@@ -51,15 +51,16 @@ public partial class ContentFormViewModel(IFormContent _form, WeakReference JsonSerializer.Serialize(s, JsonSerializationContext.Default.String);
// todo: we could probably stick Card.Errors in there too
var dataJson = $$"""
{
- "error_message": {{JsonSerializer.Serialize(e.Message)}},
- "error_stack": {{JsonSerializer.Serialize(e.StackTrace)}},
- "inner_exception": {{JsonSerializer.Serialize(e.InnerException?.Message)}},
- "template_json": {{JsonSerializer.Serialize(TemplateJson)}},
- "data_json": {{JsonSerializer.Serialize(DataJson)}}
+ "error_message": {{serializeString(e.Message)}},
+ "error_stack": {{serializeString(e.StackTrace)}},
+ "inner_exception": {{serializeString(e.InnerException?.Message)}},
+ "template_json": {{serializeString(TemplateJson)}},
+ "data_json": {{serializeString(DataJson)}}
}
""";
var cardJson = template.Expand(dataJson);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuStackViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuStackViewModel.cs
new file mode 100644
index 0000000000..2b16bd8f47
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuStackViewModel.cs
@@ -0,0 +1,82 @@
+// 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.ObjectModel;
+using CommunityToolkit.Mvvm.ComponentModel;
+using Microsoft.CmdPal.UI.ViewModels.Messages;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.System;
+
+namespace Microsoft.CmdPal.UI.ViewModels;
+
+public partial class ContextMenuStackViewModel : ObservableObject
+{
+ [ObservableProperty]
+ public partial ObservableCollection FilteredItems { get; set; }
+
+ private readonly IContextMenuContext _context;
+ private string _lastSearchText = string.Empty;
+
+ // private Dictionary? _contextKeybindings;
+ public ContextMenuStackViewModel(IContextMenuContext context)
+ {
+ _context = context;
+ FilteredItems = [.. context.AllCommands];
+ }
+
+ public void SetSearchText(string searchText)
+ {
+ if (searchText == _lastSearchText)
+ {
+ return;
+ }
+
+ _lastSearchText = searchText;
+
+ var commands = _context.AllCommands.Where(c => c.ShouldBeVisible);
+ if (string.IsNullOrEmpty(searchText))
+ {
+ ListHelpers.InPlaceUpdateList(FilteredItems, commands);
+ return;
+ }
+
+ var newResults = ListHelpers.FilterList(commands, searchText, ScoreContextCommand);
+ ListHelpers.InPlaceUpdateList(FilteredItems, newResults);
+ }
+
+ private static int ScoreContextCommand(string query, CommandContextItemViewModel item)
+ {
+ if (string.IsNullOrEmpty(query) || string.IsNullOrWhiteSpace(query))
+ {
+ return 1;
+ }
+
+ if (string.IsNullOrEmpty(item.Title))
+ {
+ return 0;
+ }
+
+ var nameMatch = StringMatcher.FuzzySearch(query, item.Title);
+
+ var descriptionMatch = StringMatcher.FuzzySearch(query, item.Subtitle);
+
+ return new[] { nameMatch.Score, (descriptionMatch.Score - 4) / 2, 0 }.Max();
+ }
+
+ public CommandContextItemViewModel? CheckKeybinding(bool ctrl, bool alt, bool shift, bool win, VirtualKey key)
+ {
+ var keybindings = _context.Keybindings();
+ if (keybindings != null)
+ {
+ // Does the pressed key match any of the keybindings?
+ var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrl, alt, shift, win, key, 0);
+ if (keybindings.TryGetValue(pressedKeyChord, out var item))
+ {
+ return item;
+ }
+ }
+
+ return null;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
index bcafb0235e..83dc4018f9 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
@@ -344,8 +344,6 @@ public partial class ListViewModel : PageViewModel, IDisposable
{
WeakReferenceMessenger.Default.Send(new(item));
- WeakReferenceMessenger.Default.Send(new(item.Keybindings()));
-
if (ShowDetails && item.HasDetails)
{
WeakReferenceMessenger.Default.Send(new(item.Details));
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/TryCommandKeybindingMessage.cs
similarity index 59%
rename from src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs
rename to src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/TryCommandKeybindingMessage.cs
index 2054d3d8fd..3df48ec3a0 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateItemKeybindingsMessage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/TryCommandKeybindingMessage.cs
@@ -2,8 +2,11 @@
// The Microsoft Corporation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
-using Microsoft.CommandPalette.Extensions;
+using Windows.System;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
-public record UpdateItemKeybindingsMessage(Dictionary? Keys);
+public record TryCommandKeybindingMessage(bool Ctrl, bool Alt, bool Shift, bool Win, VirtualKey Key)
+{
+ public bool Handled { get; set; }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs
index 0a540c7408..929b5995c5 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/UpdateCommandBarMessage.cs
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.
using System.ComponentModel;
+using Microsoft.CommandPalette.Extensions;
namespace Microsoft.CmdPal.UI.ViewModels.Messages;
@@ -13,22 +14,42 @@ public record UpdateCommandBarMessage(ICommandBarContext? ViewModel)
{
}
-// Represents everything the command bar needs to know about to show command
-// buttons at the bottom.
-//
-// This is implemented by both ListItemViewModel and ContentPageViewModel,
-// the two things with sub-commands.
-public interface ICommandBarContext : INotifyPropertyChanged
+public interface IContextMenuContext : INotifyPropertyChanged
{
public IEnumerable MoreCommands { get; }
public bool HasMoreCommands { get; }
+ public List AllCommands { get; }
+
+ ///
+ /// Generates a mapping of key -> command item for this particular item's
+ /// MoreCommands. (This won't include the primary Command, but it will
+ /// include the secondary one). This map can be used to quickly check if a
+ /// shortcut key was pressed
+ ///
+ /// a dictionary of KeyChord -> Context commands, for all commands
+ /// that have a shortcut key set.
+ public Dictionary Keybindings()
+ {
+ return MoreCommands
+ .Where(c => c.HasRequestedShortcut)
+ .ToDictionary(
+ c => c.RequestedShortcut ?? new KeyChord(0, 0, 0),
+ c => c);
+ }
+}
+
+// Represents everything the command bar needs to know about to show command
+// buttons at the bottom.
+//
+// This is implemented by both ListItemViewModel and ContentPageViewModel,
+// the two things with sub-commands.
+public interface ICommandBarContext : IContextMenuContext
+{
public string SecondaryCommandName { get; }
public CommandItemViewModel? PrimaryCommand { get; }
public CommandItemViewModel? SecondaryCommand { get; }
-
- public List AllCommands { get; }
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
index 342dbe251c..8057bb09be 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Microsoft.CmdPal.UI.ViewModels.csproj
@@ -1,5 +1,7 @@
+
+
enable
enable
@@ -67,4 +69,15 @@
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
index 576ea08140..6d59aa66b4 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionService.cs
@@ -11,7 +11,7 @@ using Windows.Foundation.Collections;
namespace Microsoft.CmdPal.UI.ViewModels.Models;
-public class ExtensionService : IExtensionService, IDisposable
+public partial class ExtensionService : IExtensionService, IDisposable
{
public event TypedEventHandler>? OnExtensionAdded;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs
index ed15268507..83644c8d44 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Models/ExtensionWrapper.cs
@@ -12,6 +12,7 @@ using Windows.Win32;
using Windows.Win32.System.Com;
using WinRT;
+// [assembly: System.Runtime.CompilerServices.DisableRuntimeMarshalling]
namespace Microsoft.CmdPal.UI.ViewModels.Models;
public class ExtensionWrapper : IExtensionWrapper
@@ -113,25 +114,36 @@ public class ExtensionWrapper : IExtensionWrapper
// -2147467262: E_NOINTERFACE
// -2147024893: E_PATH_NOT_FOUND
var guid = typeof(IExtension).GUID;
- var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
- if (hr.Value == -2147024893)
+ unsafe
{
- Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
+ var hr = PInvoke.CoCreateInstance(Guid.Parse(ExtensionClassId), null, CLSCTX.CLSCTX_LOCAL_SERVER, guid, out var extensionObj);
- // We don't really need to throw this exception.
- // We'll just return out nothing.
- return;
+ if (hr.Value == -2147024893)
+ {
+ Logger.LogDebug($"Failed to find {ExtensionDisplayName}: {hr}. It may have been uninstalled or deleted.");
+
+ // We don't really need to throw this exception.
+ // We'll just return out nothing.
+ return;
+ }
+
+ extensionPtr = Marshal.GetIUnknownForObject((nint)extensionObj);
+ if (hr < 0)
+ {
+ Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
+ Marshal.ThrowExceptionForHR(hr);
+ }
+
+ // extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
+ extensionPtr = (nint)extensionObj;
+ if (hr < 0)
+ {
+ Marshal.ThrowExceptionForHR(hr);
+ }
+
+ _extensionObject = MarshalInterface.FromAbi(extensionPtr);
}
-
- extensionPtr = Marshal.GetIUnknownForObject(extensionObj);
- if (hr < 0)
- {
- Logger.LogDebug($"Failed to instantiate {ExtensionDisplayName}: {hr}");
- Marshal.ThrowExceptionForHR(hr);
- }
-
- _extensionObject = MarshalInterface.FromAbi(extensionPtr);
}
finally
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.json b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.json
new file mode 100644
index 0000000000..59fa7259c4
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.json
@@ -0,0 +1,4 @@
+{
+ "$schema": "https://aka.ms/CsWin32.schema.json",
+ "allowMarshaling": false
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
index a082f0acd2..126efa83ca 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
@@ -99,7 +99,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
//// Run on background thread from ListPage.xaml.cs
[RelayCommand]
- private Task InitializeAsync()
+ internal Task InitializeAsync()
{
// TODO: We may want a SemaphoreSlim lock here.
@@ -182,6 +182,7 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
return; // throw?
}
+ var updateProperty = true;
switch (propertyName)
{
case nameof(Name):
@@ -198,9 +199,21 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext
case nameof(Icon):
this.Icon = new(model.Icon);
break;
+ default:
+ updateProperty = false;
+ break;
}
- UpdateProperty(propertyName);
+ // GH #38829: If we always UpdateProperty here, then there's a possible
+ // race condition, where we raise the PropertyChanged(SearchText)
+ // before the subclass actually retrieves the new SearchText from the
+ // model. In that race situation, if the UI thread handles the
+ // PropertyChanged before ListViewModel fetches the SearchText, it'll
+ // think that the old search text is the _new_ value.
+ if (updateProperty)
+ {
+ UpdateProperty(propertyName);
+ }
}
public new void ShowException(Exception ex, string? extensionHint = null)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs
index 9e971ae510..c740341c7a 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/RecentCommandsManager.cs
@@ -10,7 +10,7 @@ namespace Microsoft.CmdPal.UI.ViewModels;
public partial class RecentCommandsManager : ObservableObject
{
[JsonInclude]
- private List History { get; set; } = [];
+ internal List History { get; set; } = [];
public RecentCommandsManager()
{
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
index 27dd119b48..ae97849f7a 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
@@ -93,7 +93,7 @@ public partial class SettingsModel : ObservableObject
// Read the JSON content from the file
var jsonContent = File.ReadAllText(FilePath);
- var loaded = JsonSerializer.Deserialize(jsonContent, _deserializerOptions);
+ var loaded = JsonSerializer.Deserialize(jsonContent, JsonSerializationContext.Default.SettingsModel);
Debug.WriteLine(loaded != null ? "Loaded settings file" : "Failed to parse");
@@ -117,7 +117,7 @@ public partial class SettingsModel : ObservableObject
try
{
// Serialize the main dictionary to JSON and save it to the file
- var settingsJson = JsonSerializer.Serialize(model, _serializerOptions);
+ var settingsJson = JsonSerializer.Serialize(model, JsonSerializationContext.Default.SettingsModel);
// Is it valid JSON?
if (JsonNode.Parse(settingsJson) is JsonObject newSettings)
@@ -133,7 +133,7 @@ public partial class SettingsModel : ObservableObject
savedSettings[item.Key] = item.Value != null ? item.Value.DeepClone() : null;
}
- var serialized = savedSettings.ToJsonString(_serializerOptions);
+ var serialized = savedSettings.ToJsonString(JsonSerializationContext.Default.Options);
File.WriteAllText(FilePath, serialized);
// TODO: Instead of just raising the event here, we should
@@ -166,19 +166,34 @@ public partial class SettingsModel : ObservableObject
return Path.Combine(directory, "settings.json");
}
- private static readonly JsonSerializerOptions _serializerOptions = new()
- {
- WriteIndented = true,
- Converters = { new JsonStringEnumConverter() },
- };
+ // [UnconditionalSuppressMessage("AOT", "IL3050:Calling members annotated with 'RequiresDynamicCodeAttribute' may break functionality when AOT compiling.", Justification = "")]
+ // private static readonly JsonSerializerOptions _serializerOptions = new()
+ // {
+ // WriteIndented = true,
+ // Converters = { new JsonStringEnumConverter() },
+ // };
+ // private static readonly JsonSerializerOptions _deserializerOptions = new()
+ // {
+ // PropertyNameCaseInsensitive = true,
+ // IncludeFields = true,
+ // Converters = { new JsonStringEnumConverter() },
+ // AllowTrailingCommas = true,
+ // };
+}
- private static readonly JsonSerializerOptions _deserializerOptions = new()
- {
- PropertyNameCaseInsensitive = true,
- IncludeFields = true,
- Converters = { new JsonStringEnumConverter() },
- AllowTrailingCommas = true,
- };
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(HistoryItem))]
+[JsonSerializable(typeof(SettingsModel))]
+[JsonSerializable(typeof(AppStateModel))]
+[JsonSerializable(typeof(List), TypeInfoPropertyName = "HistoryList")]
+[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Dictionary")]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
+internal sealed partial class JsonSerializationContext : JsonSerializerContext
+{
}
public enum MonitorBehavior
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs
index 043598196b..d86831d0a1 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs
@@ -109,9 +109,12 @@ public partial class ShellViewModel(IServiceProvider _serviceProvider, TaskSched
// TODO GH #239 switch back when using the new MD text block
// _ = _queue.EnqueueAsync(() =>
_ = Task.Factory.StartNew(
- () =>
+ async () =>
{
- var result = (bool)viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
+ // bool f = await viewModel.InitializeCommand.ExecutionTask.;
+ // var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
+ // var result = viewModel.InitializeCommand.ExecutionTask.GetResultOrDefault()!;
+ var result = await viewModel.InitializeAsync();
CurrentPage = viewModel; // result ? viewModel : null;
////LoadedState = result ? ViewModelLoadedState.Loaded : ViewModelLoadedState.Error;
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
index 896ee11d0d..203009f763 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
@@ -225,27 +225,42 @@
ToolTipService.ToolTip="Ctrl+K"
Visibility="{x:Bind ViewModel.ShouldShowContextMenu, Mode=OneWay}">
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
index e8ca659097..f079f4b513 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml.cs
@@ -18,9 +18,10 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class CommandBar : UserControl,
IRecipient,
+ IRecipient,
ICurrentPageAware
{
- public CommandBarViewModel ViewModel { get; set; } = new();
+ public CommandBarViewModel ViewModel { get; } = new();
public PageViewModel? CurrentPageViewModel
{
@@ -38,6 +39,9 @@ public sealed partial class CommandBar : UserControl,
// RegisterAll isn't AOT compatible
WeakReferenceMessenger.Default.Register(this);
+ WeakReferenceMessenger.Default.Register(this);
+
+ ViewModel.PropertyChanged += ViewModel_PropertyChanged;
}
public void Receive(OpenContextMenuMessage message)
@@ -52,8 +56,41 @@ public sealed partial class CommandBar : UserControl,
ShowMode = FlyoutShowMode.Standard,
};
MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
- CommandsDropdown.SelectedIndex = 0;
- CommandsDropdown.Focus(FocusState.Programmatic);
+ UpdateUiForStackChange();
+ }
+
+ public void Receive(TryCommandKeybindingMessage msg)
+ {
+ if (!ViewModel.ShouldShowContextMenu)
+ {
+ return;
+ }
+
+ var result = ViewModel?.CheckKeybinding(msg.Ctrl, msg.Alt, msg.Shift, msg.Win, msg.Key);
+
+ if (result == ContextKeybindingResult.Hide)
+ {
+ msg.Handled = true;
+ }
+ else if (result == ContextKeybindingResult.KeepOpen)
+ {
+ if (!MoreCommandsButton.Flyout.IsOpen)
+ {
+ var options = new FlyoutShowOptions
+ {
+ ShowMode = FlyoutShowMode.Standard,
+ };
+ MoreCommandsButton.Flyout.ShowAt(MoreCommandsButton, options);
+ }
+
+ UpdateUiForStackChange();
+
+ msg.Handled = true;
+ }
+ else if (result == ContextKeybindingResult.Unhandled)
+ {
+ msg.Handled = false;
+ }
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-aggressively")]
@@ -88,8 +125,14 @@ public sealed partial class CommandBar : UserControl,
{
if (e.ClickedItem is CommandContextItemViewModel item)
{
- ViewModel?.InvokeItemCommand.Execute(item);
- MoreCommandsButton.Flyout.Hide();
+ if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
+ {
+ MoreCommandsButton.Flyout.Hide();
+ }
+ else
+ {
+ UpdateUiForStackChange();
+ }
}
}
@@ -106,9 +149,136 @@ public sealed partial class CommandBar : UserControl,
var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
- if (ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key) ?? false)
+ var result = ViewModel?.CheckKeybinding(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
+
+ if (result == ContextKeybindingResult.Hide)
+ {
+ e.Handled = true;
+ MoreCommandsButton.Flyout.Hide();
+ WeakReferenceMessenger.Default.Send();
+ }
+ else if (result == ContextKeybindingResult.KeepOpen)
{
e.Handled = true;
}
+ else if (result == ContextKeybindingResult.Unhandled)
+ {
+ e.Handled = false;
+ }
+ }
+
+ private void Flyout_Opened(object sender, object e)
+ {
+ UpdateUiForStackChange();
+ }
+
+ private void Flyout_Closing(FlyoutBase sender, FlyoutBaseClosingEventArgs args)
+ {
+ ViewModel?.ClearContextStack();
+ WeakReferenceMessenger.Default.Send();
+ }
+
+ private void ViewModel_PropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
+ {
+ var prop = e.PropertyName;
+ if (prop == nameof(ViewModel.ContextMenu))
+ {
+ UpdateUiForStackChange();
+ }
+ }
+
+ private void ContextFilterBox_TextChanged(object sender, TextChangedEventArgs e)
+ {
+ ViewModel.ContextMenu?.SetSearchText(ContextFilterBox.Text);
+
+ if (CommandsDropdown.SelectedIndex == -1)
+ {
+ CommandsDropdown.SelectedIndex = 0;
+ }
+ }
+
+ private void ContextFilterBox_KeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ var ctrlPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control).HasFlag(CoreVirtualKeyStates.Down);
+ var altPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Menu).HasFlag(CoreVirtualKeyStates.Down);
+ var shiftPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift).HasFlag(CoreVirtualKeyStates.Down);
+ var winPressed = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.LeftWindows).HasFlag(CoreVirtualKeyStates.Down) ||
+ InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.RightWindows).HasFlag(CoreVirtualKeyStates.Down);
+
+ if (e.Key == VirtualKey.Enter)
+ {
+ if (CommandsDropdown.SelectedItem is CommandContextItemViewModel item)
+ {
+ if (ViewModel?.InvokeItem(item) == ContextKeybindingResult.Hide)
+ {
+ MoreCommandsButton.Flyout.Hide();
+ WeakReferenceMessenger.Default.Send();
+ }
+ else
+ {
+ UpdateUiForStackChange();
+ }
+
+ e.Handled = true;
+ }
+ }
+ else if (e.Key == VirtualKey.Escape ||
+ (e.Key == VirtualKey.Left && altPressed))
+ {
+ if (ViewModel.CanPopContextStack())
+ {
+ ViewModel.PopContextStack();
+ UpdateUiForStackChange();
+ }
+ else
+ {
+ MoreCommandsButton.Flyout.Hide();
+ WeakReferenceMessenger.Default.Send();
+ }
+
+ e.Handled = true;
+ }
+
+ CommandsDropdown_KeyDown(sender, e);
+ }
+
+ private void ContextFilterBox_PreviewKeyDown(object sender, KeyRoutedEventArgs e)
+ {
+ if (e.Key == VirtualKey.Up)
+ {
+ // navigate previous
+ if (CommandsDropdown.SelectedIndex > 0)
+ {
+ CommandsDropdown.SelectedIndex--;
+ }
+ else
+ {
+ CommandsDropdown.SelectedIndex = CommandsDropdown.Items.Count - 1;
+ }
+
+ e.Handled = true;
+ }
+ else if (e.Key == VirtualKey.Down)
+ {
+ // navigate next
+ if (CommandsDropdown.SelectedIndex < CommandsDropdown.Items.Count - 1)
+ {
+ CommandsDropdown.SelectedIndex++;
+ }
+ else
+ {
+ CommandsDropdown.SelectedIndex = 0;
+ }
+
+ e.Handled = true;
+ }
+ }
+
+ private void UpdateUiForStackChange()
+ {
+ ContextFilterBox.Text = string.Empty;
+ ViewModel.ContextMenu?.SetSearchText(string.Empty);
+ CommandsDropdown.SelectedIndex = 0;
+ ContextFilterBox.Focus(FocusState.Programmatic);
}
}
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 d939381fb0..c868e3dd5e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs
@@ -8,8 +8,6 @@ using CommunityToolkit.WinUI;
using Microsoft.CmdPal.UI.ViewModels;
using Microsoft.CmdPal.UI.ViewModels.Messages;
using Microsoft.CmdPal.UI.Views;
-using Microsoft.CommandPalette.Extensions;
-using Microsoft.CommandPalette.Extensions.Toolkit;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Input;
using Microsoft.UI.Xaml;
@@ -23,7 +21,6 @@ namespace Microsoft.CmdPal.UI.Controls;
public sealed partial class SearchBar : UserControl,
IRecipient,
IRecipient,
- IRecipient,
ICurrentPageAware
{
private readonly DispatcherQueue _queue = DispatcherQueue.GetForCurrentThread();
@@ -34,8 +31,6 @@ public sealed partial class SearchBar : UserControl,
private readonly DispatcherQueueTimer _debounceTimer = DispatcherQueue.GetForCurrentThread().CreateTimer();
private bool _isBackspaceHeld;
- private Dictionary? _keyBindings;
-
public PageViewModel? CurrentPageViewModel
{
get => (PageViewModel?)GetValue(CurrentPageViewModelProperty);
@@ -74,7 +69,6 @@ public sealed partial class SearchBar : UserControl,
this.InitializeComponent();
WeakReferenceMessenger.Default.Register(this);
WeakReferenceMessenger.Default.Register(this);
- WeakReferenceMessenger.Default.Register(this);
}
public void ClearSearch()
@@ -173,17 +167,14 @@ public sealed partial class SearchBar : UserControl,
WeakReferenceMessenger.Default.Send(new());
}
- if (_keyBindings != null)
+ if (!e.Handled)
{
- // Does the pressed key match any of the keybindings?
- var pressedKeyChord = KeyChordHelpers.FromModifiers(ctrlPressed, altPressed, shiftPressed, winPressed, (int)e.Key, 0);
- if (_keyBindings.TryGetValue(pressedKeyChord, out var item))
- {
- // TODO GH #245: This is a bit of a hack, but we need to make sure that the keybindings are updated before we send the message
- // so that the correct item is activated.
- WeakReferenceMessenger.Default.Send(new(item));
- e.Handled = true;
- }
+ // The CommandBar is responsible for handling all the item keybindings,
+ // since the bound context item may need to then show another
+ // context menu
+ TryCommandKeybindingMessage msg = new(ctrlPressed, altPressed, shiftPressed, winPressed, e.Key);
+ WeakReferenceMessenger.Default.Send(msg);
+ e.Handled = msg.Handled;
}
}
@@ -302,10 +293,5 @@ public sealed partial class SearchBar : UserControl,
public void Receive(GoHomeMessage message) => ClearSearch();
- public void Receive(FocusSearchBoxMessage message) => this.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
-
- public void Receive(UpdateItemKeybindingsMessage message)
- {
- _keyBindings = message.Keys;
- }
+ public void Receive(FocusSearchBoxMessage message) => FilterBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs
index bc0999ca02..869b048dbd 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs
@@ -187,8 +187,6 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page,
WeakReferenceMessenger.Default.Send(new(null));
- WeakReferenceMessenger.Default.Send(new(null));
-
var isMainPage = command is MainListPage;
// Construct our ViewModel of the appropriate type and pass it the UI Thread context.
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
index 51b33f8ede..6c63eeff16 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Strings/en-us/Resources.resw
@@ -394,6 +394,9 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Behavior
+
+ Search commands...
+
Show system tray icon
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
index 9b3f54a21f..5da419cd40 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/AddBookmarkForm.cs
@@ -34,7 +34,7 @@ internal sealed partial class AddBookmarkForm : FormContent
"style": "text",
"id": "name",
"label": "{{Resources.bookmarks_form_name_label}}",
- "value": {{JsonSerializer.Serialize(name)}},
+ "value": {{JsonSerializer.Serialize(name, BookmarkSerializationContext.Default.String)}},
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_name_required}}"
},
@@ -42,7 +42,7 @@ internal sealed partial class AddBookmarkForm : FormContent
"type": "Input.Text",
"style": "text",
"id": "bookmark",
- "value": {{JsonSerializer.Serialize(url)}},
+ "value": {{JsonSerializer.Serialize(url, BookmarkSerializationContext.Default.String)}},
"label": "{{Resources.bookmarks_form_bookmark_label}}",
"isRequired": true,
"errorMessage": "{{Resources.bookmarks_form_bookmark_required}}"
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkSerializationContext.cs
new file mode 100644
index 0000000000..9730bf214d
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkSerializationContext.cs
@@ -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;
+using System.Text.Json.Serialization;
+
+namespace Microsoft.CmdPal.Ext.Bookmarks;
+
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(BookmarkData))]
+[JsonSerializable(typeof(Bookmarks))]
+[JsonSerializable(typeof(List), TypeInfoPropertyName = "BookmarkList")]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+internal sealed partial class BookmarkSerializationContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
index 7c3a1dd1e0..8f2e257782 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Bookmarks.cs
@@ -28,7 +28,7 @@ public sealed class Bookmarks
if (!string.IsNullOrEmpty(jsonStringReading))
{
- data = JsonSerializer.Deserialize(jsonStringReading, _jsonOptions) ?? new Bookmarks();
+ data = JsonSerializer.Deserialize(jsonStringReading, BookmarkSerializationContext.Default.Bookmarks) ?? new Bookmarks();
}
}
@@ -37,7 +37,7 @@ public sealed class Bookmarks
public static void WriteToFile(string path, Bookmarks data)
{
- var jsonString = JsonSerializer.Serialize(data, _jsonOptions);
+ var jsonString = JsonSerializer.Serialize(data, BookmarkSerializationContext.Default.Bookmarks);
File.WriteAllText(BookmarksCommandProvider.StateJsonPath(), jsonString);
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
index 7bbf1efc5f..40c3cca9f2 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/Microsoft.CmdPal.Ext.Bookmarks.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.Bookmarks
enable
@@ -16,7 +17,7 @@
-
+
Resources.resx
@@ -24,7 +25,7 @@
True
-
+
PreserveNewest
@@ -39,5 +40,5 @@
PublicResXFileCodeGenerator
-
+
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs
index 50ff346103..f4b6089229 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Helpers/NativeMethods.cs
@@ -17,7 +17,7 @@ internal static class NativeMethods
internal INPUTTYPE type;
internal InputUnion data;
- internal static int Size => Marshal.SizeOf(typeof(INPUT));
+ internal static int Size => Marshal.SizeOf();
}
[StructLayout(LayoutKind.Explicit)]
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
index 1d583e279b..774753f31d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.ClipboardHistory/Microsoft.CmdPal.Ext.ClipboardHistory.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.ClipboardHistory
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs
index 9afb39b6f1..486eeaa8b5 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Helpers/NetworkConnectionProperties.cs
@@ -311,14 +311,14 @@ internal sealed class NetworkConnectionProperties
{
switch (property)
{
- case string:
- return string.IsNullOrWhiteSpace(property) ? string.Empty : $"\n\n{title}{property}";
+ case string str:
+ return string.IsNullOrWhiteSpace(str) ? string.Empty : $"\n\n{title}{str}";
case List listString:
- return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
+ return listString.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listString)}";
case List listIP:
- return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
+ return listIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", listIP)}";
case IPAddressCollection collectionIP:
- return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", property)}";
+ return collectionIP.Count == 0 ? string.Empty : $"\n\n{title}{string.Join("\n\n* ", collectionIP)}";
case null:
return string.Empty;
default:
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj
index 4c619bc5e5..48e9d6ba82 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.System/Microsoft.CmdPal.Ext.System.csproj
@@ -1,5 +1,6 @@
+
enable
Microsoft.CmdPal.Ext.System
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs
index 84a1c249ba..d381c1e4cc 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/HistoryItem.cs
@@ -13,5 +13,5 @@ public class HistoryItem(string searchString, DateTime timestamp)
public DateTime Timestamp { get; private set; } = timestamp;
- public string ToJson() => JsonSerializer.Serialize(this);
+ public string ToJson() => JsonSerializer.Serialize(this, WebSearchJsonSerializationContext.Default.HistoryItem);
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
index b83ba47a73..8a39bca35b 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/SettingsManager.cs
@@ -80,7 +80,7 @@ public class SettingsManager : JsonSettingsManager
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
- historyItems = JsonSerializer.Deserialize>(existingContent) ?? [];
+ historyItems = JsonSerializer.Deserialize>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
}
else
{
@@ -101,7 +101,7 @@ public class SettingsManager : JsonSettingsManager
}
// Serialize the updated list back to JSON and save it
- var historyJson = JsonSerializer.Serialize(historyItems);
+ var historyJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, historyJson);
}
catch (Exception ex)
@@ -121,7 +121,7 @@ public class SettingsManager : JsonSettingsManager
// Read and deserialize JSON into a list of HistoryItem objects
var fileContent = File.ReadAllText(_historyPath);
- var historyItems = JsonSerializer.Deserialize>(fileContent) ?? [];
+ var historyItems = JsonSerializer.Deserialize>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Convert each HistoryItem to a ListItem
var listItems = new List();
@@ -198,7 +198,7 @@ public class SettingsManager : JsonSettingsManager
if (File.Exists(_historyPath))
{
var existingContent = File.ReadAllText(_historyPath);
- var historyItems = JsonSerializer.Deserialize>(existingContent) ?? [];
+ var historyItems = JsonSerializer.Deserialize>(existingContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
// Check if trimming is needed
if (historyItems.Count > maxHistoryItems)
@@ -207,7 +207,7 @@ public class SettingsManager : JsonSettingsManager
historyItems = historyItems.Skip(historyItems.Count - maxHistoryItems).ToList();
// Save the trimmed history back to the file
- var trimmedHistoryJson = JsonSerializer.Serialize(historyItems);
+ var trimmedHistoryJson = JsonSerializer.Serialize(historyItems, WebSearchJsonSerializationContext.Default.ListHistoryItem);
File.WriteAllText(_historyPath, trimmedHistoryJson);
}
}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/WebSearchJsonSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/WebSearchJsonSerializationContext.cs
new file mode 100644
index 0000000000..443c9cdf40
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/WebSearchJsonSerializationContext.cs
@@ -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;
+using System.Text.Json.Serialization;
+using Microsoft.CmdPal.Ext.WebSearch.Helpers;
+
+namespace Microsoft.CmdPal.Ext.WebSearch;
+
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(HistoryItem))]
+[JsonSerializable(typeof(List))]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+internal sealed partial class WebSearchJsonSerializationContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
index 3ddedfcd71..3fbaeb30a7 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Microsoft.CmdPal.Ext.WebSearch.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.WebSearch
enable
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs
index f99f0ce3b3..18b2548ce2 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Helpers/JsonSettingsListHelper.cs
@@ -4,10 +4,8 @@
using System;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Microsoft.CmdPal.Ext.WindowsSettings.Helpers;
@@ -21,6 +19,8 @@ internal static class JsonSettingsListHelper
///
private const string _settingsFile = "WindowsSettings.json";
+ private const string _extTypeNamespace = "Microsoft.CmdPal.Ext.WindowsSettings";
+
private static readonly JsonSerializerOptions _serializerOptions = new()
{
};
@@ -32,7 +32,6 @@ internal static class JsonSettingsListHelper
internal static Classes.WindowsSettings ReadAllPossibleSettings()
{
var assembly = Assembly.GetExecutingAssembly();
- var type = assembly.GetTypes().FirstOrDefault(x => x.Name == nameof(WindowsSettingsCommandsProvider));
#pragma warning disable CS8632 // The annotation for nullable reference types should only be used in code within a '#nullable' annotations context.
Classes.WindowsSettings? settings = null;
@@ -40,7 +39,7 @@ internal static class JsonSettingsListHelper
try
{
- var resourceName = $"{type?.Namespace}.{_settingsFile}";
+ var resourceName = $"{_extTypeNamespace}.{_settingsFile}";
using var stream = assembly.GetManifestResourceStream(resourceName);
if (stream is null)
{
@@ -48,12 +47,13 @@ internal static class JsonSettingsListHelper
}
var options = _serializerOptions;
- options.Converters.Add(new JsonStringEnumConverter());
+ // Why we need it? I don't see any enum usage in WindowsSettings
+ // options.Converters.Add(new JsonStringEnumConverter());
using var reader = new StreamReader(stream);
var text = reader.ReadToEnd();
- settings = JsonSerializer.Deserialize(text, options);
+ settings = JsonSerializer.Deserialize(text, WindowsSettingsJsonSerializationContext.Default.WindowsSettings);
}
#pragma warning disable CS0168
catch (Exception exception)
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/JsonSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/JsonSerializationContext.cs
new file mode 100644
index 0000000000..e267e8f52e
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/JsonSerializationContext.cs
@@ -0,0 +1,22 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text;
+using System.Text.Json.Serialization;
+using System.Threading.Tasks;
+
+namespace Microsoft.CmdPal.Ext.WindowsSettings;
+
+[JsonSerializable(typeof(float))]
+[JsonSerializable(typeof(int))]
+[JsonSerializable(typeof(string))]
+[JsonSerializable(typeof(bool))]
+[JsonSerializable(typeof(Classes.WindowsSettings))]
+[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
+internal sealed partial class WindowsSettingsJsonSerializationContext : JsonSerializerContext
+{
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj
index d44e907606..9f2d72bc8a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/Microsoft.CmdPal.Ext.WindowsSettings.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.WindowsSettings
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
index 373a1f7891..2fc1218bd7 100644
--- a/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
+++ b/src/modules/cmdpal/ext/SamplePagesExtension/EvilSamplesPage.cs
@@ -6,6 +6,7 @@ using System.Threading;
using System.Threading.Tasks;
using Microsoft.CommandPalette.Extensions;
using Microsoft.CommandPalette.Extensions.Toolkit;
+using Windows.System;
namespace SamplePagesExtension;
@@ -76,7 +77,136 @@ public partial class EvilSamplesPage : ListPage
{
Body = "This is a test for GH#512. If it doesn't appear immediately, it's likely InvokeCommand is happening on the UI thread.",
},
- }
+ },
+
+ // More edge cases than truly evil
+ new ListItem(
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
+ {
+ Title = "anonymous command test",
+ Subtitle = "Try pressing Ctrl+1 with me selected",
+ Icon = new IconInfo("\uE712"), // "More" dots
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
+ {
+ Title = "I'm a second command",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
+ },
+ new CommandContextItem("nested...")
+ {
+ Title = "We can go deeper...",
+ Icon = new IconInfo("\uF148"),
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
+ }
+ ],
+ },
+ new ListItem(
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
+ {
+ Title = "noop command test",
+ Subtitle = "Try pressing Ctrl+1 with me selected",
+ Icon = new IconInfo("\uE712"), // "More" dots
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
+ {
+ Title = "I'm a second command",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
+ },
+ new CommandContextItem(new NoOpCommand())
+ {
+ Title = "We can go deeper...",
+ Icon = new IconInfo("\uF148"),
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
+ }
+ ],
+ },
+ new ListItem(
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
+ {
+ Title = "noop secondary command test",
+ Subtitle = "Try pressing Ctrl+1 with me selected",
+ Icon = new IconInfo("\uE712"), // "More" dots
+ MoreCommands = [
+ new CommandContextItem(new NoOpCommand())
+ {
+ Title = "We can go deeper...",
+ Icon = new IconInfo("\uF148"),
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
+ }
+ ],
+ },
+
];
public EvilSamplesPage()
diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs
index 954a79ce04..3cf987e417 100644
--- a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs
+++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/SampleListPage.cs
@@ -69,62 +69,47 @@ internal sealed partial class SampleListPage : ListPage
},
new ListItem(
- new AnonymousCommand(() =>
- {
- var t = new ToastStatusMessage(new StatusMessage()
- {
- Message = "Primary command invoked",
- State = MessageState.Info,
- });
- t.Show();
- })
- {
- Result = CommandResult.KeepOpen(),
- Icon = new IconInfo("\uE712"),
- })
+ new ToastCommand("Primary command invoked", MessageState.Info) { Name = "Primary command", Icon = new IconInfo("\uF146") }) // dial 1
{
Title = "You can add context menu items too. Press Ctrl+k",
Subtitle = "Try pressing Ctrl+1 with me selected",
- Icon = new IconInfo("\uE712"),
+ Icon = new IconInfo("\uE712"), // "More" dots
MoreCommands = [
new CommandContextItem(
- new AnonymousCommand(() =>
- {
- var t = new ToastStatusMessage(new StatusMessage()
- {
- Message = "Secondary command invoked",
- State = MessageState.Warning,
- });
- t.Show();
- })
- {
- Name = "Secondary command",
- Icon = new IconInfo("\uF147"), // Dial 2
- Result = CommandResult.KeepOpen(),
- })
+ new ToastCommand("Secondary command invoked", MessageState.Warning) { Name = "Secondary command", Icon = new IconInfo("\uF147") }) // dial 2
{
Title = "I'm a second command",
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number1),
},
new CommandContextItem(
- new AnonymousCommand(() =>
- {
- var t = new ToastStatusMessage(new StatusMessage()
- {
- Message = "Third command invoked",
- State = MessageState.Error,
- });
- t.Show();
- })
- {
- Name = "Do it",
- Icon = new IconInfo("\uF148"), // dial 3
- Result = CommandResult.KeepOpen(),
- })
+ new ToastCommand("Third command invoked", MessageState.Error) { Name = "Do 3", Icon = new IconInfo("\uF148") }) // dial 3
{
- Title = "A third command too",
+ Title = "We can go deeper...",
Icon = new IconInfo("\uF148"),
RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.Number2),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested A invoked") { Name = "Do it", Icon = new IconInfo("A") })
+ {
+ Title = "Nested A",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(alt: true, vkey: VirtualKey.A),
+ },
+
+ new CommandContextItem(
+ new ToastCommand("Nested B invoked") { Name = "Do it", Icon = new IconInfo("B") })
+ {
+ Title = "Nested B...",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ MoreCommands = [
+ new CommandContextItem(
+ new ToastCommand("Nested C invoked") { Name = "Do it" })
+ {
+ Title = "You get it",
+ RequestedShortcut = KeyChordHelpers.FromModifiers(ctrl: true, vkey: VirtualKey.B),
+ }
+ ],
+ },
+ ],
}
],
},
@@ -183,7 +168,6 @@ internal sealed partial class SampleListPage : ListPage
{
Title = "Get the name of the Foreground window",
},
-
];
}
}
diff --git a/src/modules/cmdpal/ext/SamplePagesExtension/Pages/ToastCommand.cs b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/ToastCommand.cs
new file mode 100644
index 0000000000..dfbeb5225a
--- /dev/null
+++ b/src/modules/cmdpal/ext/SamplePagesExtension/Pages/ToastCommand.cs
@@ -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.
+
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace SamplePagesExtension;
+
+internal sealed partial class ToastCommand(string message, MessageState state = MessageState.Info) : InvokableCommand
+{
+ public override ICommandResult Invoke()
+ {
+ var t = new ToastStatusMessage(new StatusMessage()
+ {
+ Message = message,
+ State = state,
+ });
+ t.Show();
+
+ return CommandResult.KeepOpen();
+ }
+}
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs
index 6fbc734560..6d92cdc146 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/JsonSerializationContext.cs
@@ -16,7 +16,6 @@ namespace Microsoft.CommandPalette.Extensions.Toolkit;
[JsonSerializable(typeof(List))]
[JsonSerializable(typeof(Dictionary), TypeInfoPropertyName = "Dictionary")]
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true)]
-[System.Diagnostics.CodeAnalysis.SuppressMessage("StyleCop.CSharp.MaintainabilityRules", "SA1402:File may only contain a single type", Justification = "Just used here")]
internal partial class JsonSerializationContext : JsonSerializerContext
{
}
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs
index 91d715b509..ffd20643aa 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions.Toolkit/ListItem.cs
@@ -61,4 +61,9 @@ public partial class ListItem : CommandItem, IListItem
: base(command)
{
}
+
+ public ListItem()
+ : base()
+ {
+ }
}
diff --git a/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs b/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs
index 82b238993d..3f0feaaae3 100644
--- a/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs
+++ b/src/modules/colorPicker/ColorPickerUI/Helpers/ColorRepresentationHelper.cs
@@ -243,6 +243,40 @@ namespace ColorPicker.Helpers
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
}
+ ///
+ /// Returns a representation of a Oklab color
+ ///
+ /// The for the Oklab color presentation
+ /// A representation of a Oklab color
+ private static string ColorToOklab(Color color)
+ {
+ var (lightness, chromaticityA, chromaticityB) = ColorFormatHelper.ConvertToOklabColor(color);
+ lightness = Math.Round(lightness, 2);
+ chromaticityA = Math.Round(chromaticityA, 2);
+ chromaticityB = Math.Round(chromaticityB, 2);
+
+ return $"oklab({lightness.ToString(CultureInfo.InvariantCulture)}" +
+ $", {chromaticityA.ToString(CultureInfo.InvariantCulture)}" +
+ $", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
+ }
+
+ ///
+ /// Returns a representation of a CIE LCh color
+ ///
+ /// The for the CIE LCh color presentation
+ /// A representation of a CIE LCh color
+ private static string ColorToOklch(Color color)
+ {
+ var (lightness, chroma, hue) = ColorFormatHelper.ConvertToOklchColor(color);
+ lightness = Math.Round(lightness, 2);
+ chroma = Math.Round(chroma, 2);
+ hue = Math.Round(hue, 2);
+
+ return $"oklch({lightness.ToString(CultureInfo.InvariantCulture)}" +
+ $", {chroma.ToString(CultureInfo.InvariantCulture)}" +
+ $", {hue.ToString(CultureInfo.InvariantCulture)})";
+ }
+
///
/// Returns a representation of a CIE XYZ color
///
diff --git a/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs b/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs
index 129f365e0d..2f8d5a2348 100644
--- a/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs
+++ b/src/modules/colorPicker/ColorPickerUI/ViewModels/ColorEditorViewModel.cs
@@ -301,6 +301,12 @@ namespace ColorPicker.ViewModels
FormatName = ColorRepresentationType.NCol.ToString(),
Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.NCol.ToString()),
});
+ _allColorRepresentations.Add(
+ new ColorFormatModel()
+ {
+ FormatName = ColorRepresentationType.CIEXYZ.ToString(),
+ Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ.ToString()),
+ });
_allColorRepresentations.Add(
new ColorFormatModel()
{
@@ -310,8 +316,14 @@ namespace ColorPicker.ViewModels
_allColorRepresentations.Add(
new ColorFormatModel()
{
- FormatName = ColorRepresentationType.CIEXYZ.ToString(),
- Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.CIEXYZ.ToString()),
+ FormatName = ColorRepresentationType.Oklab.ToString(),
+ Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.Oklab.ToString()),
+ });
+ _allColorRepresentations.Add(
+ new ColorFormatModel()
+ {
+ FormatName = ColorRepresentationType.Oklch.ToString(),
+ Convert = (Color color) => ColorRepresentationHelper.GetStringRepresentationFromMediaColor(color, ColorRepresentationType.Oklch.ToString()),
});
_allColorRepresentations.Add(
new ColorFormatModel()
diff --git a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs
index eaa5369dd6..288ed0f599 100644
--- a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs
+++ b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorConverterTest.cs
@@ -364,9 +364,6 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("8080FF", 59.20, 33.10, -63.46)] // blue
[DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta
[DataRow("BFBF00", 75.04, -17.35, 76.03)] // yellow
- [DataRow("008000", 46.23, -51.70, 49.90)] // green
- [DataRow("8080FF", 59.20, 33.10, -63.46)] // blue
- [DataRow("BF40BF", 50.10, 65.50, -41.48)] // magenta
[DataRow("0048BA", 34.35, 27.94, -64.80)] // absolute zero
[DataRow("B0BF1A", 73.91, -23.39, 71.15)] // acid green
[DataRow("D0FF14", 93.87, -40.20, 88.97)] // arctic lime
@@ -401,13 +398,121 @@ namespace Microsoft.ColorPicker.UnitTests
var result = ColorFormatHelper.ConvertToCIELABColor(color);
// lightness[0..100]
- Assert.AreEqual(Math.Round(result.Lightness, 2), lightness);
+ Assert.AreEqual(lightness, Math.Round(result.Lightness, 2));
// chromaticityA[-128..127]
- Assert.AreEqual(Math.Round(result.ChromaticityA, 2), chromaticityA);
+ Assert.AreEqual(chromaticityA, Math.Round(result.ChromaticityA, 2));
// chromaticityB[-128..127]
- Assert.AreEqual(Math.Round(result.ChromaticityB, 2), chromaticityB);
+ Assert.AreEqual(chromaticityB, Math.Round(result.ChromaticityB, 2));
+ }
+
+ // Test data calculated using https://oklch.com (which uses https://github.com/Evercoder/culori)
+ [TestMethod]
+ [DataRow("FFFFFF", 1.00, 0.00, 0.00)] // white
+ [DataRow("808080", 0.6, 0.00, 0.00)] // gray
+ [DataRow("000000", 0.00, 0.00, 0.00)] // black
+ [DataRow("FF0000", 0.628, 0.22, 0.13)] // red
+ [DataRow("008000", 0.52, -0.14, 0.11)] // green
+ [DataRow("80FFFF", 0.928, -0.11, -0.03)] // cyan
+ [DataRow("8080FF", 0.661, 0.03, -0.18)] // blue
+ [DataRow("BF40BF", 0.598, 0.18, -0.11)] // magenta
+ [DataRow("BFBF00", 0.779, -0.06, 0.16)] // yellow
+ [DataRow("0048BA", 0.444, -0.03, -0.19)] // absolute zero
+ [DataRow("B0BF1A", 0.767, -0.07, 0.15)] // acid green
+ [DataRow("D0FF14", 0.934, -0.12, 0.19)] // arctic lime
+ [DataRow("1B4D3E", 0.382, -0.06, 0.01)] // brunswick green
+ [DataRow("FFEF00", 0.935, -0.05, 0.19)] // canary yellow
+ [DataRow("FFA600", 0.794, 0.06, 0.16)] // cheese
+ [DataRow("1A2421", 0.25, -0.02, 0)] // dark jungle green
+ [DataRow("003399", 0.371, -0.02, -0.17)] // dark powder blue
+ [DataRow("D70A53", 0.563, 0.22, 0.04)] // debian red
+ [DataRow("80FFD5", 0.916, -0.13, 0.02)] // fathom secret green
+ [DataRow("EFDFBB", 0.907, 0, 0.05)] // dutch white
+ [DataRow("5218FA", 0.489, 0.05, -0.28)] // han purple
+ [DataRow("FF496C", 0.675, 0.21, 0.05)] // infra red
+ [DataRow("545AA7", 0.5, 0.02, -0.12)] // liberty
+ [DataRow("E6A8D7", 0.804, 0.09, -0.04)] // light orchid
+ [DataRow("ADDFAD", 0.856, -0.07, 0.05)] // light moss green
+ [DataRow("E3F988", 0.942, -0.07, 0.12)] // mindaro
+ public void ColorRGBtoOklabTest(string hexValue, double lightness, double chromaticityA, double chromaticityB)
+ {
+ if (string.IsNullOrWhiteSpace(hexValue))
+ {
+ Assert.IsNotNull(hexValue);
+ }
+
+ Assert.IsTrue(hexValue.Length >= 6);
+
+ var red = int.Parse(hexValue.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var green = int.Parse(hexValue.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var blue = int.Parse(hexValue.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+
+ var color = Color.FromArgb(255, red, green, blue);
+ var result = ColorFormatHelper.ConvertToOklabColor(color);
+
+ // lightness[0..1]
+ Assert.AreEqual(lightness, Math.Round(result.Lightness, 3));
+
+ // chromaticityA[-0.5..0.5]
+ Assert.AreEqual(chromaticityA, Math.Round(result.ChromaticityA, 2));
+
+ // chromaticityB[-0.5..0.5]
+ Assert.AreEqual(chromaticityB, Math.Round(result.ChromaticityB, 2));
+ }
+
+ // Test data calculated using https://oklch.com (which uses https://github.com/Evercoder/culori)
+ [TestMethod]
+ [DataRow("FFFFFF", 1.00, 0.00, 0.00)] // white
+ [DataRow("808080", 0.6, 0.00, 0.00)] // gray
+ [DataRow("000000", 0.00, 0.00, 0.00)] // black
+ [DataRow("FF0000", 0.628, 0.258, 29.23)] // red
+ [DataRow("008000", 0.52, 0.177, 142.5)] // green
+ [DataRow("80FFFF", 0.928, 0.113, 195.38)] // cyan
+ [DataRow("8080FF", 0.661, 0.184, 280.13)] // blue
+ [DataRow("BF40BF", 0.598, 0.216, 327.86)] // magenta
+ [DataRow("BFBF00", 0.779, 0.17, 109.77)] // yellow
+ [DataRow("0048BA", 0.444, 0.19, 260.86)] // absolute zero
+ [DataRow("B0BF1A", 0.767, 0.169, 115.4)] // acid green
+ [DataRow("D0FF14", 0.934, 0.224, 122.28)] // arctic lime
+ [DataRow("1B4D3E", 0.382, 0.06, 170.28)] // brunswick green
+ [DataRow("FFEF00", 0.935, 0.198, 104.67)] // canary yellow
+ [DataRow("FFA600", 0.794, 0.171, 71.19)] // cheese
+ [DataRow("1A2421", 0.25, 0.015, 174.74)] // dark jungle green
+ [DataRow("003399", 0.371, 0.173, 262.12)] // dark powder blue
+ [DataRow("D70A53", 0.563, 0.222, 11.5)] // debian red
+ [DataRow("80FFD5", 0.916, 0.129, 169.38)] // fathom secret green
+ [DataRow("EFDFBB", 0.907, 0.05, 86.89)] // dutch white
+ [DataRow("5218FA", 0.489, 0.286, 279.13)] // han purple
+ [DataRow("FF496C", 0.675, 0.217, 14.37)] // infra red
+ [DataRow("545AA7", 0.5, 0.121, 277.7)] // liberty
+ [DataRow("E6A8D7", 0.804, 0.095, 335.4)] // light orchid
+ [DataRow("ADDFAD", 0.856, 0.086, 144.78)] // light moss green
+ [DataRow("E3F988", 0.942, 0.141, 118.24)] // mindaro
+ public void ColorRGBtoOklchTest(string hexValue, double lightness, double chroma, double hue)
+ {
+ if (string.IsNullOrWhiteSpace(hexValue))
+ {
+ Assert.IsNotNull(hexValue);
+ }
+
+ Assert.IsTrue(hexValue.Length >= 6);
+
+ var red = int.Parse(hexValue.AsSpan(0, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var green = int.Parse(hexValue.AsSpan(2, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+ var blue = int.Parse(hexValue.AsSpan(4, 2), NumberStyles.HexNumber, CultureInfo.InvariantCulture);
+
+ var color = Color.FromArgb(255, red, green, blue);
+ var result = ColorFormatHelper.ConvertToOklchColor(color);
+
+ // lightness[0..1]
+ Assert.AreEqual(lightness, Math.Round(result.Lightness, 3));
+
+ // chroma[0..0.5]
+ Assert.AreEqual(chroma, Math.Round(result.Chroma, 3));
+
+ // hue[0°..360°]
+ Assert.AreEqual(hue, Math.Round(result.Hue, 2));
}
// The following results are computed using LittleCMS2, an open-source color management engine,
@@ -428,9 +533,6 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue
[DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta
[DataRow("BFBF00", 40.1154, 48.3384, 7.2171)] // yellow
- [DataRow("008000", 7.7188, 15.4377, 2.5729)] // green
- [DataRow("8080FF", 34.6688, 27.2469, 98.0434)] // blue
- [DataRow("BF40BF", 32.7217, 18.5062, 51.1405)] // magenta
[DataRow("0048BA", 11.1792, 8.1793, 47.4455)] // absolute zero
[DataRow("B0BF1A", 36.7205, 46.5663, 8.0311)] // acid green
[DataRow("D0FF14", 61.8965, 84.9797, 13.8037)] // arctic lime
diff --git a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs
index a96310dc82..f1f0c99e3d 100644
--- a/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs
+++ b/src/modules/colorPicker/UnitTest-ColorPickerUI/Helpers/ColorRepresentationHelperTest.cs
@@ -23,8 +23,10 @@ namespace Microsoft.ColorPicker.UnitTests
[DataRow("HSV", "hsv(0, 0%, 0%)")]
[DataRow("HWB", "hwb(0, 0%, 100%)")]
[DataRow("RGB", "rgb(0, 0, 0)")]
- [DataRow("CIELAB", "CIELab(0, 0, 0)")]
[DataRow("CIEXYZ", "XYZ(0, 0, 0)")]
+ [DataRow("CIELAB", "CIELab(0, 0, 0)")]
+ [DataRow("Oklab", "oklab(0, 0, 0)")]
+ [DataRow("Oklch", "oklch(0, 0, 0)")]
[DataRow("VEC4", "(0f, 0f, 0f, 1f)")]
[DataRow("Decimal", "0")]
[DataRow("HEX Int", "0xFF000000")]
diff --git a/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs b/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs
index 02e6138b30..64a52e8385 100644
--- a/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs
+++ b/src/modules/launcher/PowerLauncher/ViewModel/ResultsViewModel.cs
@@ -282,11 +282,11 @@ namespace PowerLauncher.ViewModel
if (options.SearchQueryTuningEnabled)
{
- sorted = Results.OrderByDescending(x => (x.Result.Metadata.WeightBoost + x.Result.Score + (x.Result.SelectedCount * options.SearchClickedItemWeight))).ToList();
+ sorted = Results.OrderByDescending(x => x.Result.GetSortOrderScore(options.SearchClickedItemWeight)).ToList();
}
else
{
- sorted = Results.OrderByDescending(x => (x.Result.Metadata.WeightBoost + x.Result.Score + (x.Result.SelectedCount * 5))).ToList();
+ sorted = Results.OrderByDescending(x => x.Result.GetSortOrderScore(5)).ToList();
}
// remove history items in they are in the list as non-history items
diff --git a/src/modules/launcher/Wox.Plugin/Result.cs b/src/modules/launcher/Wox.Plugin/Result.cs
index 91f026bbb2..3bbb6dbf5e 100644
--- a/src/modules/launcher/Wox.Plugin/Result.cs
+++ b/src/modules/launcher/Wox.Plugin/Result.cs
@@ -187,5 +187,20 @@ namespace Wox.Plugin
/// Gets plugin ID that generated this result
///
public string PluginID { get; internal set; }
+
+ ///
+ /// Gets or sets a value indicating whether usage based sorting should be applied to this result.
+ ///
+ public bool DisableUsageBasedScoring { get; set; }
+
+ public int GetSortOrderScore(int selectedItemMultiplier)
+ {
+ if (DisableUsageBasedScoring)
+ {
+ return Metadata.WeightBoost + Score;
+ }
+
+ return Metadata.WeightBoost + Score + (SelectedCount * selectedItemMultiplier);
+ }
}
}
diff --git a/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs b/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs
index 406f67c2a4..9102609b6b 100644
--- a/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/CmdPalProperties.cs
@@ -4,9 +4,7 @@
using System;
using System.IO;
-using System.IO.Abstractions;
using System.Text.Json;
-using System.Text.Json.Serialization;
namespace Microsoft.PowerToys.Settings.UI.Library
{
@@ -46,17 +44,13 @@ namespace Microsoft.PowerToys.Settings.UI.Library
if (doc.RootElement.TryGetProperty(nameof(Hotkey), out JsonElement hotkeyElement))
{
Hotkey = JsonSerializer.Deserialize(hotkeyElement.GetRawText());
-
- if (Hotkey == null)
- {
- Hotkey = DefaultHotkeyValue;
- }
}
}
catch (Exception)
{
- Hotkey = DefaultHotkeyValue;
}
+
+ Hotkey ??= DefaultHotkeyValue;
}
}
}
diff --git a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs
index 0d3fc918d6..b82ef56888 100644
--- a/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs
+++ b/src/settings-ui/Settings.UI.Library/ColorPickerProperties.cs
@@ -32,8 +32,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
VisibleColorFormats.Add("HSI", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("HSI")));
VisibleColorFormats.Add("HWB", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("HWB")));
VisibleColorFormats.Add("NCol", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("NCol")));
- VisibleColorFormats.Add("CIELAB", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
VisibleColorFormats.Add("CIEXYZ", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("CIEXYZ")));
+ VisibleColorFormats.Add("CIELAB", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
+ VisibleColorFormats.Add("Oklab", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("Oklab")));
+ VisibleColorFormats.Add("Oklch", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("Oklch")));
VisibleColorFormats.Add("VEC4", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("VEC4")));
VisibleColorFormats.Add("Decimal", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("Decimal")));
VisibleColorFormats.Add("HEX Int", new KeyValuePair(false, ColorFormatHelper.GetDefaultFormat("HEX Int")));
diff --git a/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs b/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs
index 09ef003e88..7e57f5a730 100644
--- a/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs
+++ b/src/settings-ui/Settings.UI.Library/Enumerations/ColorRepresentationType.cs
@@ -80,5 +80,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Enumerations
/// Color presentation as an 8-digit hexadecimal integer (0xFFFFFFFF)
///
HexInteger = 13,
+
+ ///
+ /// Color representation as CIELCh color space (L[0..100], C[0..230], h[0°..360°])
+ ///
+ CIELCh = 14,
+
+ ///
+ /// Color representation as Oklab color space (L[0..1], a[-0.5..0.5], b[-0.5..0.5])
+ ///
+ Oklab = 15,
+
+ ///
+ /// Color representation as Oklch color space (L[0..1], C[0..0.5], h[0°..360°])
+ ///
+ Oklch = 16,
}
}
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml
index 32f0ee488e..ea6d1c9f22 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml
@@ -32,7 +32,8 @@
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind Description}"
TextTrimming="CharacterEllipsis"
- TextWrapping="NoWrap" />
+ TextWrapping="NoWrap"
+ ToolTipService.ToolTip="{x:Bind Description}" />
diff --git a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs
index 76681d8b46..475d399674 100644
--- a/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs
+++ b/src/settings-ui/Settings.UI/SettingsXAML/Controls/ColorFormatEditor.xaml.cs
@@ -47,12 +47,17 @@ namespace Microsoft.PowerToys.Settings.UI.Controls
new ColorFormatParameter() { Parameter = "%In", Description = resourceLoader.GetString("Help_intensity") },
new ColorFormatParameter() { Parameter = "%Hn", Description = resourceLoader.GetString("Help_hueNat") },
new ColorFormatParameter() { Parameter = "%Ll", Description = resourceLoader.GetString("Help_lightnessNat") },
- new ColorFormatParameter() { Parameter = "%Lc", Description = resourceLoader.GetString("Help_lightnessCIE") },
new ColorFormatParameter() { Parameter = "%Va", Description = resourceLoader.GetString("Help_value") },
new ColorFormatParameter() { Parameter = "%Wh", Description = resourceLoader.GetString("Help_whiteness") },
new ColorFormatParameter() { Parameter = "%Bn", Description = resourceLoader.GetString("Help_blackness") },
- new ColorFormatParameter() { Parameter = "%Ca", Description = resourceLoader.GetString("Help_chromaticityA") },
- new ColorFormatParameter() { Parameter = "%Cb", Description = resourceLoader.GetString("Help_chromaticityB") },
+ new ColorFormatParameter() { Parameter = "%Lc", Description = resourceLoader.GetString("Help_lightnessCIE") },
+ new ColorFormatParameter() { Parameter = "%Ca", Description = resourceLoader.GetString("Help_chromaticityACIE") },
+ new ColorFormatParameter() { Parameter = "%Cb", Description = resourceLoader.GetString("Help_chromaticityBCIE") },
+ new ColorFormatParameter() { Parameter = "%Lo", Description = resourceLoader.GetString("Help_lightnessOklab") },
+ new ColorFormatParameter() { Parameter = "%Oa", Description = resourceLoader.GetString("Help_chromaticityAOklab") },
+ new ColorFormatParameter() { Parameter = "%Ob", Description = resourceLoader.GetString("Help_chromaticityBOklab") },
+ new ColorFormatParameter() { Parameter = "%Oc", Description = resourceLoader.GetString("Help_chromaOklch") },
+ new ColorFormatParameter() { Parameter = "%Oh", Description = resourceLoader.GetString("Help_hueOklch") },
new ColorFormatParameter() { Parameter = "%Xv", Description = resourceLoader.GetString("Help_X_value") },
new ColorFormatParameter() { Parameter = "%Yv", Description = resourceLoader.GetString("Help_Y_value") },
new ColorFormatParameter() { Parameter = "%Zv", Description = resourceLoader.GetString("Help_Z_value") },
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 a95cdb86cd..694603707c 100644
--- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
+++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw
@@ -1660,11 +1660,11 @@ Made with 💗 by Microsoft and the PowerToys community.
blackness
-
- chromaticityA
+
+ chromaticity A (CIE Lab)
-
- chromaticityB
+
+ chromaticity B (CIE Lab)
X value
@@ -4460,7 +4460,7 @@ Activate by holding the key for the character you want to add an accent to, then
Commonly used variables
New+ commonly used variables header in the flyout info card
-
+
Year, represented by a full four or five digits, depending on the calendar used.
New+ description of the year $YYYY variable - casing of $YYYY is important
@@ -4999,4 +4999,25 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
Go to Command Palette settings to customize the activation shortcut.
+
+ chroma (CIE LCh)
+
+
+ hue (CIE LCh)
+
+
+ lightness (Oklab/Oklch)
+
+
+ chromaticity A (Oklab)
+
+
+ chromaticity B (Oklab)
+
+
+ chroma (Oklch)
+
+
+ hue (Oklch)
+
\ No newline at end of file
diff --git a/tools/build/cert-management.ps1 b/tools/build/cert-management.ps1
index 5f52f2611f..a085a5ca54 100644
--- a/tools/build/cert-management.ps1
+++ b/tools/build/cert-management.ps1
@@ -116,4 +116,44 @@ function EnsureCertificate {
}
return $cert
-}
\ No newline at end of file
+}
+
+function Export-CertificateFiles {
+ param (
+ [System.Security.Cryptography.X509Certificates.X509Certificate2]$Certificate,
+ [string]$CerPath,
+ [string]$PfxPath,
+ [securestring]$PfxPassword
+ )
+
+ if (-not $Certificate) {
+ Write-Error "No certificate provided to export."
+ return
+ }
+
+ if ($CerPath) {
+ try {
+ Export-Certificate -Cert $Certificate -FilePath $CerPath -Force | Out-Null
+ Write-Host "Exported CER to: $CerPath"
+ } catch {
+ Write-Warning "Failed to export CER file: $_"
+ }
+ }
+
+ if ($PfxPath -and $PfxPassword) {
+ try {
+ Export-PfxCertificate -Cert $Certificate -FilePath $PfxPath -Password $PfxPassword -Force | Out-Null
+ Write-Host "Exported PFX to: $PfxPath"
+ } catch {
+ Write-Warning "Failed to export PFX file: $_"
+ }
+ }
+
+ if (-not $CerPath -and -not $PfxPath) {
+ Write-Warning "No output path specified. Nothing was exported."
+ }
+}
+
+$cert = EnsureCertificate
+$pswd = ConvertTo-SecureString -String "MySecurePassword123!" -AsPlainText -Force
+Export-CertificateFiles -Certificate $cert -CerPath "$env:TEMP\cert.cer" -PfxPath "$env:TEMP\cert.pfx" -PfxPassword $pswd