mirror of
https://github.com/microsoft/PowerToys.git
synced 2025-12-29 16:36:40 +01:00
Compare commits
2 Commits
leilzh/ima
...
dev/migrie
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
acbd438426 | ||
|
|
dd111841fa |
14
.github/actions/spell-check/expect.txt
vendored
14
.github/actions/spell-check/expect.txt
vendored
@@ -198,6 +198,7 @@ CLIPBOARDUPDATE
|
||||
CLIPCHILDREN
|
||||
CLIPSIBLINGS
|
||||
closesocket
|
||||
clp
|
||||
CLSCTX
|
||||
clsids
|
||||
Clusion
|
||||
@@ -1045,6 +1046,7 @@ NOINHERITLAYOUT
|
||||
NOINTERFACE
|
||||
NOINVERT
|
||||
NOLINKINFO
|
||||
nologo
|
||||
NOMCX
|
||||
NOMINMAX
|
||||
NOMIRRORBITMAP
|
||||
@@ -1277,6 +1279,7 @@ pstm
|
||||
PStr
|
||||
pstream
|
||||
pstrm
|
||||
pswd
|
||||
PSYSTEM
|
||||
psz
|
||||
ptb
|
||||
@@ -1423,6 +1426,7 @@ searchterm
|
||||
SEARCHUI
|
||||
SECONDARYDISPLAY
|
||||
secpol
|
||||
securestring
|
||||
SEEMASKINVOKEIDLIST
|
||||
SELCHANGE
|
||||
SENDCHANGE
|
||||
@@ -1978,4 +1982,12 @@ zoomit
|
||||
ZOOMITX
|
||||
ZXk
|
||||
ZXNs
|
||||
zzz
|
||||
zzz
|
||||
ACIE
|
||||
AOklab
|
||||
BCIE
|
||||
BOklab
|
||||
culori
|
||||
Evercoder
|
||||
LCh
|
||||
CIELCh
|
||||
|
||||
@@ -79,10 +79,7 @@
|
||||
<ComponentGroupRef Id="ToolComponentGroup" />
|
||||
<ComponentGroupRef Id="MonacoSRCHeatGenerated" />
|
||||
<ComponentGroupRef Id="WorkspacesComponentGroup" />
|
||||
|
||||
<?if $(var.CIBuild) = "true" ?>
|
||||
<ComponentGroupRef Id="CmdPalComponentGroup" />
|
||||
<?endif?>
|
||||
<ComponentGroupRef Id="CmdPalComponentGroup" />
|
||||
</Feature>
|
||||
|
||||
<SetProperty Id="ARPINSTALLLOCATION" Value="[INSTALLFOLDER]" After="CostFinalize" />
|
||||
|
||||
@@ -141,6 +141,40 @@ namespace ManagedCommon
|
||||
return lab;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a Oklab color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> to convert</param>
|
||||
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
|
||||
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;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a Oklch color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> to convert</param>
|
||||
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> 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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a linear RGB color <see cref="double"/> 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/
|
||||
/// </summary>
|
||||
/// <param name="r">Linear R value</param>
|
||||
/// <param name="g">Linear G value</param>
|
||||
/// <param name="b">Linear B value</param>
|
||||
/// <returns>The perceptual lightness [0..1] and two chromaticities [-0.5..0.5]</returns>
|
||||
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_)
|
||||
);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert an Oklab color <see cref="double"/> from Cartesian form to its polar form Oklch
|
||||
/// https://bottosson.github.io/posts/oklab/#the-oklab-color-space
|
||||
/// </summary>
|
||||
/// <param name="lightness">The <see cref="lightness"/></param>
|
||||
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
|
||||
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
|
||||
/// <returns>The perceptual lightness [0..1], the chroma [0..0.5], and the hue angle [0°..360°]</returns>
|
||||
private static (double Lightness, double Chroma, double Hue)
|
||||
GetOklchColorFromOklab(double lightness, double chromaticity_a, double chromaticity_b)
|
||||
{
|
||||
return GetLCHColorFromLAB(lightness, chromaticity_a, chromaticity_b);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a color in Cartesian form (Lab) to its polar form (LCh)
|
||||
/// </summary>
|
||||
/// <param name="lightness">The <see cref="lightness"/></param>
|
||||
/// <param name="chromaticity_a">The <see cref="chromaticity_a"/></param>
|
||||
/// <param name="chromaticity_b">The <see cref="chromaticity_b"/></param>
|
||||
/// <returns>The lightness, chroma, and hue angle</returns>
|
||||
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);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Convert a given <see cref="Color"/> to a natural color (hue, whiteness, blackness)
|
||||
/// </summary>
|
||||
@@ -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";
|
||||
|
||||
@@ -301,6 +301,7 @@ namespace package
|
||||
if (!std::filesystem::exists(directoryPath))
|
||||
{
|
||||
Logger::error(L"The directory '" + directoryPath + L"' does not exist.");
|
||||
return {};
|
||||
}
|
||||
|
||||
const std::regex pattern(R"(^.+\.(appx|msix|msixbundle)$)", std::regex_constants::icase);
|
||||
|
||||
@@ -18,10 +18,19 @@ public static class OcrHelpers
|
||||
{
|
||||
public static async Task<string> 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);
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
<Project DefaultTargets="Build"
|
||||
xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props" Condition="Exists('..\..\..\..\packages\Microsoft.Windows.CppWinRT.2.0.240111.5\build\native\Microsoft.Windows.CppWinRT.props')" />
|
||||
<Import Project="..\Microsoft.CmdPal.UI\CmdPal.pre.props" Condition="Exists('..\Microsoft.CmdPal.UI\CmdPal.pre.prop')" />
|
||||
<PropertyGroup Label="Globals">
|
||||
<VCProjectVersion>17.0</VCProjectVersion>
|
||||
<Keyword>Win32Proj</Keyword>
|
||||
@@ -49,13 +50,21 @@
|
||||
</ItemGroup>
|
||||
<ItemDefinitionGroup>
|
||||
<ClCompile>
|
||||
<PreprocessorDefinitions>EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions>
|
||||
EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
|
||||
%(PreprocessorDefinitions);
|
||||
$(CommandPaletteBranding)
|
||||
</PreprocessorDefinitions>
|
||||
<PreprocessorDefinitions Condition="'$(CommandPaletteBranding)'=='' or '$(CommandPaletteBranding)'=='Dev'">
|
||||
IS_DEV_BRANDING;%(PreprocessorDefinitions)
|
||||
</PreprocessorDefinitions>
|
||||
<AdditionalIncludeDirectories>..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)</AdditionalIncludeDirectories>
|
||||
</ClCompile>
|
||||
<Link>
|
||||
<OutputFile>$(OutDir)$(TargetName)$(TargetExt)</OutputFile>
|
||||
</Link>
|
||||
</ItemDefinitionGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ClInclude Include="pch.h" />
|
||||
<ClInclude Include="resource.h" />
|
||||
|
||||
@@ -208,7 +208,7 @@ public:
|
||||
try
|
||||
{
|
||||
std::wstring packageName = L"Microsoft.CommandPalette";
|
||||
#ifdef _DEBUG
|
||||
#ifdef IS_DEV_BRANDING
|
||||
packageName = L"Microsoft.CommandPalette.Dev";
|
||||
#endif
|
||||
if (!package::GetRegisteredPackage(packageName, false).has_value())
|
||||
@@ -245,12 +245,21 @@ public:
|
||||
errorMessage += e.what();
|
||||
Logger::error(errorMessage);
|
||||
}
|
||||
|
||||
#if _DEBUG
|
||||
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette.Dev_8wekyb3d8bbwe!App", L"RunFromPT", false);
|
||||
try
|
||||
{
|
||||
#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);
|
||||
LaunchApp(std::wstring{ L"shell:AppsFolder\\" } + L"Microsoft.CommandPalette_8wekyb3d8bbwe!App", L"RunFromPT", false);
|
||||
#endif
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
std::string errorMessage{ "Exception thrown while trying to launch CmdPal: " };
|
||||
errorMessage += e.what();
|
||||
Logger::error(errorMessage);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
virtual void disable()
|
||||
|
||||
@@ -66,8 +66,10 @@ public sealed class CommandProviderWrapper
|
||||
DisplayName = provider.DisplayName;
|
||||
Icon = new(provider.Icon);
|
||||
Icon.InitializeProperties();
|
||||
|
||||
// Note: explicitly not InitializeProperties()ing the settings here. If
|
||||
// we do that, then we'd regress GH #38321
|
||||
Settings = new(provider.Settings, this, _taskScheduler);
|
||||
Settings.InitializeProperties();
|
||||
|
||||
Logger.LogDebug($"Initialized command provider {ProviderId}");
|
||||
}
|
||||
@@ -151,9 +153,11 @@ public sealed class CommandProviderWrapper
|
||||
Icon = new(model.Icon);
|
||||
Icon.InitializeProperties();
|
||||
|
||||
// Note: explicitly not InitializeProperties()ing the settings here. If
|
||||
// we do that, then we'd regress GH #38321
|
||||
Settings = new(model.Settings, this, _taskScheduler);
|
||||
Settings.InitializeProperties();
|
||||
|
||||
// We do need to explicitly initialize commands though
|
||||
InitializeCommands(commands, fallbacks, serviceProvider, pageContext);
|
||||
|
||||
Logger.LogDebug($"Loaded commands from {DisplayName} ({ProviderId})");
|
||||
@@ -194,21 +198,6 @@ public sealed class CommandProviderWrapper
|
||||
}
|
||||
}
|
||||
|
||||
/* This is a View/ExtensionHost piece
|
||||
* public void AllowSetForeground(bool allow)
|
||||
{
|
||||
if (!IsExtension)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var iextn = extensionWrapper?.GetExtensionObject();
|
||||
unsafe
|
||||
{
|
||||
PInvoke.CoAllowSetForegroundWindow(iextn);
|
||||
}
|
||||
}*/
|
||||
|
||||
public override bool Equals(object? obj) => obj is CommandProviderWrapper wrapper && isValid == wrapper.isValid;
|
||||
|
||||
public override int GetHashCode() => _commandProvider.GetHashCode();
|
||||
|
||||
@@ -2,18 +2,25 @@
|
||||
// The Microsoft Corporation licenses this file to you under the MIT license.
|
||||
// See the LICENSE file in the project root for more information.
|
||||
|
||||
using ManagedCommon;
|
||||
using Microsoft.CmdPal.UI.ViewModels.Models;
|
||||
using Microsoft.CommandPalette.Extensions;
|
||||
|
||||
namespace Microsoft.CmdPal.UI.ViewModels;
|
||||
|
||||
public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
|
||||
public partial class CommandSettingsViewModel(ICommandSettings? _unsafeSettings, CommandProviderWrapper provider, TaskScheduler mainThread)
|
||||
{
|
||||
private readonly ExtensionObject<ICommandSettings> _model = new(_unsafeSettings);
|
||||
|
||||
public ContentPageViewModel? SettingsPage { get; private set; }
|
||||
|
||||
public void InitializeProperties()
|
||||
public bool Initialized { get; private set; }
|
||||
|
||||
public bool HasSettings =>
|
||||
_model.Unsafe != null && // We have a settings model AND
|
||||
(!Initialized || SettingsPage != null); // we weren't initialized, OR we were, and we do have a settings page
|
||||
|
||||
private void UnsafeInitializeProperties()
|
||||
{
|
||||
var model = _model.Unsafe;
|
||||
if (model == null)
|
||||
@@ -27,4 +34,27 @@ public partial class CommandSettingsViewModel(ICommandSettings _unsafeSettings,
|
||||
SettingsPage.InitializeProperties();
|
||||
}
|
||||
}
|
||||
|
||||
public void SafeInitializeProperties()
|
||||
{
|
||||
try
|
||||
{
|
||||
UnsafeInitializeProperties();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Logger.LogError($"Failed to load settings page", ex: ex);
|
||||
}
|
||||
|
||||
Initialized = true;
|
||||
}
|
||||
|
||||
public void DoOnUiThread(Action action)
|
||||
{
|
||||
Task.Factory.StartNew(
|
||||
action,
|
||||
CancellationToken.None,
|
||||
TaskCreationOptions.None,
|
||||
mainThread);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,8 @@ public partial class ProviderSettingsViewModel(
|
||||
IServiceProvider _serviceProvider) : ObservableObject
|
||||
{
|
||||
private readonly SettingsModel _settings = _serviceProvider.GetService<SettingsModel>()!;
|
||||
private readonly Lock _initializeSettingsLock = new();
|
||||
private Task? _initializeSettingsTask;
|
||||
|
||||
public string DisplayName => _provider.DisplayName;
|
||||
|
||||
@@ -34,6 +36,9 @@ public partial class ProviderSettingsViewModel(
|
||||
|
||||
public IconInfoViewModel Icon => _provider.Icon;
|
||||
|
||||
[ObservableProperty]
|
||||
public partial bool LoadingSettings { get; set; } = _provider.Settings?.HasSettings ?? false;
|
||||
|
||||
public bool IsEnabled
|
||||
{
|
||||
get => _providerSettings.IsEnabled;
|
||||
@@ -56,15 +61,60 @@ public partial class ProviderSettingsViewModel(
|
||||
}
|
||||
}
|
||||
|
||||
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
|
||||
/// <summary>
|
||||
/// Gets a value indicating whether returns true if we have a settings page
|
||||
/// that's initialized, or we are still working on initializing that
|
||||
/// settings page. If we don't have a settings object, or that settings
|
||||
/// object doesn't have a settings page, then we'll return false.
|
||||
/// </summary>
|
||||
public bool HasSettings
|
||||
{
|
||||
OnPropertyChanged(nameof(ExtensionSubtext));
|
||||
OnPropertyChanged(nameof(TopLevelCommands));
|
||||
get
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_provider.Settings.Initialized)
|
||||
{
|
||||
return _provider.Settings.HasSettings;
|
||||
}
|
||||
|
||||
// settings still need to be loaded.
|
||||
return LoadingSettings;
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasSettings => _provider.Settings != null && _provider.Settings.SettingsPage != null;
|
||||
/// <summary>
|
||||
/// Gets will return the settings page, if we have one, and have initialized it.
|
||||
/// If we haven't initialized it, this will kick off a thread to start
|
||||
/// initializing it.
|
||||
/// </summary>
|
||||
public ContentPageViewModel? SettingsPage
|
||||
{
|
||||
get
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
public ContentPageViewModel? SettingsPage => HasSettings ? _provider?.Settings?.SettingsPage : null;
|
||||
if (_provider.Settings.Initialized)
|
||||
{
|
||||
LoadingSettings = false;
|
||||
return _provider.Settings.SettingsPage;
|
||||
}
|
||||
|
||||
// Don't load the settings if we're already working on it
|
||||
lock (_initializeSettingsLock)
|
||||
{
|
||||
_initializeSettingsTask ??= Task.Run(InitializeSettingsPage);
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
[field: AllowNull]
|
||||
public List<TopLevelViewModel> TopLevelCommands
|
||||
@@ -90,4 +140,30 @@ public partial class ProviderSettingsViewModel(
|
||||
}
|
||||
|
||||
private void Save() => SettingsModel.SaveSettings(_settings);
|
||||
|
||||
private void InitializeSettingsPage()
|
||||
{
|
||||
if (_provider.Settings == null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_provider.Settings.SafeInitializeProperties();
|
||||
_provider.Settings.DoOnUiThread(() =>
|
||||
{
|
||||
// Changing these properties will try to update XAML, and that has
|
||||
// to be handled on the UI thread, so we need to raise them on the
|
||||
// UI thread
|
||||
LoadingSettings = false;
|
||||
OnPropertyChanged(nameof(HasSettings));
|
||||
OnPropertyChanged(nameof(LoadingSettings));
|
||||
OnPropertyChanged(nameof(SettingsPage));
|
||||
});
|
||||
}
|
||||
|
||||
private void Provider_CommandsChanged(CommandProviderWrapper sender, CommandPalette.Extensions.IItemsChangedEventArgs args)
|
||||
{
|
||||
OnPropertyChanged(nameof(ExtensionSubtext));
|
||||
OnPropertyChanged(nameof(TopLevelCommands));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
|
||||
using System.Collections.Immutable;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using CommunityToolkit.Mvvm.Messaging;
|
||||
@@ -44,6 +45,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
public async Task<bool> LoadBuiltinsAsync()
|
||||
{
|
||||
var s = new Stopwatch();
|
||||
s.Start();
|
||||
|
||||
_builtInCommands.Clear();
|
||||
|
||||
// Load built-In commands first. These are all in-proc, and
|
||||
@@ -56,6 +60,10 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
await LoadTopLevelCommandsFromProvider(wrapper);
|
||||
}
|
||||
|
||||
s.Stop();
|
||||
|
||||
Logger.LogDebug($"Loading built-ins took {s.ElapsedMilliseconds}ms");
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -239,6 +247,9 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
|
||||
private async Task StartExtensionsAndGetCommands(IEnumerable<IExtensionWrapper> extensions)
|
||||
{
|
||||
var s = new Stopwatch();
|
||||
s.Start();
|
||||
|
||||
// TODO This most definitely needs a lock
|
||||
foreach (var extension in extensions)
|
||||
{
|
||||
@@ -258,6 +269,10 @@ public partial class TopLevelCommandManager : ObservableObject,
|
||||
Logger.LogError(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
s.Stop();
|
||||
|
||||
Logger.LogDebug($"Loading extensions took {s.ElapsedMilliseconds}ms");
|
||||
}
|
||||
|
||||
private void ExtensionService_OnExtensionRemoved(IExtensionService sender, IEnumerable<IExtensionWrapper> extensions)
|
||||
|
||||
@@ -25,6 +25,7 @@ using Windows.UI;
|
||||
using Windows.UI.WindowManagement;
|
||||
using Windows.Win32;
|
||||
using Windows.Win32.Foundation;
|
||||
using Windows.Win32.Graphics.Dwm;
|
||||
using Windows.Win32.UI.Input.KeyboardAndMouse;
|
||||
using Windows.Win32.UI.Shell;
|
||||
using Windows.Win32.UI.WindowsAndMessaging;
|
||||
@@ -232,6 +233,16 @@ public sealed partial class MainWindow : Window,
|
||||
PositionCentered(display);
|
||||
|
||||
PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_SHOW);
|
||||
|
||||
// instead of showing the window, uncloak it from DWM
|
||||
// This will make it visible to the user, without the animation or frames for
|
||||
// loading XAML with composition
|
||||
unsafe
|
||||
{
|
||||
BOOL value = false;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, (void*)&value, (uint)sizeof(BOOL));
|
||||
}
|
||||
|
||||
PInvoke.SetForegroundWindow(hwnd);
|
||||
PInvoke.SetActiveWindow(hwnd);
|
||||
}
|
||||
@@ -289,7 +300,7 @@ public sealed partial class MainWindow : Window,
|
||||
ShowHwnd(message.Hwnd, settings.SummonOn);
|
||||
}
|
||||
|
||||
public void Receive(HideWindowMessage message) => PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
public void Receive(HideWindowMessage message) => HideWindow();
|
||||
|
||||
public void Receive(QuitMessage message) =>
|
||||
|
||||
@@ -297,7 +308,21 @@ public sealed partial class MainWindow : Window,
|
||||
DispatcherQueue.TryEnqueue(() => Close());
|
||||
|
||||
public void Receive(DismissMessage message) =>
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
HideWindow();
|
||||
|
||||
private void HideWindow()
|
||||
{
|
||||
// Hide our window
|
||||
|
||||
// Instead of hiding the window, cloak it from DWM
|
||||
// This will make it invisible to the user, such that we can show it again
|
||||
// by uncloaking it, which avoids an unnecessary "flicker in" that XAML does
|
||||
unsafe
|
||||
{
|
||||
BOOL value = true;
|
||||
PInvoke.DwmSetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAK, (void*)&value, (uint)sizeof(BOOL));
|
||||
}
|
||||
}
|
||||
|
||||
internal void MainWindow_Closed(object sender, WindowEventArgs args)
|
||||
{
|
||||
@@ -386,7 +411,9 @@ public sealed partial class MainWindow : Window,
|
||||
return;
|
||||
}
|
||||
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
// This will DWM cloak our window:
|
||||
HideWindow();
|
||||
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalDismissedOnLostFocus());
|
||||
}
|
||||
|
||||
@@ -479,10 +506,24 @@ public sealed partial class MainWindow : Window,
|
||||
var isRootHotkey = string.IsNullOrEmpty(commandId);
|
||||
PowerToysTelemetry.Log.WriteEvent(new CmdPalHotkeySummoned(isRootHotkey));
|
||||
|
||||
var isVisible = this.Visible;
|
||||
unsafe
|
||||
{
|
||||
// We need to check if our window is cloaked or not. A cloaked window is still
|
||||
// technically visible, because SHOW/HIDE != iconic (minimized) != cloaked
|
||||
// (these are all separate states)
|
||||
long attr = 0;
|
||||
PInvoke.DwmGetWindowAttribute(_hwnd, DWMWINDOWATTRIBUTE.DWMWA_CLOAKED, &attr, sizeof(long));
|
||||
if (attr == 1 /* DWM_CLOAKED_APP */)
|
||||
{
|
||||
isVisible = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Note to future us: the wParam will have the index of the hotkey we registered.
|
||||
// We can use that in the future to differentiate the hotkeys we've pressed
|
||||
// so that we can bind hotkeys to individual commands
|
||||
if (!this.Visible || !isRootHotkey)
|
||||
if (!isVisible || !isRootHotkey)
|
||||
{
|
||||
Activate();
|
||||
|
||||
@@ -490,7 +531,16 @@ public sealed partial class MainWindow : Window,
|
||||
}
|
||||
else if (isRootHotkey)
|
||||
{
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
// If there's a debugger attached...
|
||||
if (System.Diagnostics.Debugger.IsAttached)
|
||||
{
|
||||
// ... then manually hide our window. When debugged, we won't get the cool cloaking,
|
||||
// but that's the price to pay for having the HWND not light-dismiss while we're debugging.
|
||||
PInvoke.ShowWindow(_hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
return;
|
||||
}
|
||||
|
||||
HideWindow();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -518,22 +568,6 @@ public sealed partial class MainWindow : Window,
|
||||
|
||||
var hotkey = _hotkeys[hotkeyIndex];
|
||||
HandleSummon(hotkey.CommandId);
|
||||
|
||||
// var isRootHotkey = string.IsNullOrEmpty(hotkey.CommandId);
|
||||
|
||||
// // Note to future us: the wParam will have the index of the hotkey we registered.
|
||||
// // We can use that in the future to differentiate the hotkeys we've pressed
|
||||
// // so that we can bind hotkeys to individual commands
|
||||
// if (!this.Visible || !isRootHotkey)
|
||||
// {
|
||||
// Activate();
|
||||
|
||||
// Summon(hotkey.CommandId);
|
||||
// }
|
||||
// else if (isRootHotkey)
|
||||
// {
|
||||
// PInvoke.ShowWindow(hwnd, SHOW_WINDOW_CMD.SW_HIDE);
|
||||
// }
|
||||
}
|
||||
|
||||
return (LRESULT)IntPtr.Zero;
|
||||
|
||||
@@ -36,3 +36,7 @@ ExtractIconEx
|
||||
WM_RBUTTONUP
|
||||
WM_LBUTTONUP
|
||||
WM_LBUTTONDBLCLK
|
||||
|
||||
DwmGetWindowAttribute
|
||||
DwmSetWindowAttribute
|
||||
DWM_CLOAKED_APP
|
||||
@@ -113,7 +113,24 @@
|
||||
Visibility="{x:Bind ViewModel.HasSettings}" />
|
||||
|
||||
<Frame x:Name="SettingsFrame" Visibility="{x:Bind ViewModel.HasSettings}">
|
||||
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
|
||||
|
||||
<controls:SwitchPresenter
|
||||
HorizontalAlignment="Stretch"
|
||||
TargetType="x:Boolean"
|
||||
Value="{x:Bind ViewModel.LoadingSettings, Mode=OneWay}">
|
||||
<controls:Case Value="True">
|
||||
<ProgressRing
|
||||
Width="36"
|
||||
Height="36"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Center"
|
||||
IsIndeterminate="True" />
|
||||
</controls:Case>
|
||||
<controls:Case Value="False">
|
||||
<cmdpalUI:ContentPage ViewModel="{x:Bind ViewModel.SettingsPage, Mode=OneWay}" />
|
||||
</controls:Case>
|
||||
</controls:SwitchPresenter>
|
||||
|
||||
</Frame>
|
||||
|
||||
<TextBlock
|
||||
|
||||
@@ -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<INPUT>();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Explicit)]
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.ClipboardHistory</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
|
||||
@@ -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<string> 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<IPAddress> 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:
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.System</RootNamespace>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ public class SettingsManager : JsonSettingsManager
|
||||
if (File.Exists(_historyPath))
|
||||
{
|
||||
var existingContent = File.ReadAllText(_historyPath);
|
||||
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent) ?? [];
|
||||
historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(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<List<HistoryItem>>(fileContent) ?? [];
|
||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(fileContent, WebSearchJsonSerializationContext.Default.ListHistoryItem) ?? [];
|
||||
|
||||
// Convert each HistoryItem to a ListItem
|
||||
var listItems = new List<ListItem>();
|
||||
@@ -198,7 +198,7 @@ public class SettingsManager : JsonSettingsManager
|
||||
if (File.Exists(_historyPath))
|
||||
{
|
||||
var existingContent = File.ReadAllText(_historyPath);
|
||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(existingContent) ?? [];
|
||||
var historyItems = JsonSerializer.Deserialize<List<HistoryItem>>(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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<HistoryItem>))]
|
||||
[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)]
|
||||
internal sealed partial class WebSearchJsonSerializationContext : JsonSerializerContext
|
||||
{
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.WebSearch</RootNamespace>
|
||||
<Nullable>enable</Nullable>
|
||||
|
||||
@@ -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
|
||||
/// </summary>
|
||||
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<Classes.WindowsSettings>(text, options);
|
||||
settings = JsonSerializer.Deserialize(text, WindowsSettingsJsonSerializationContext.Default.WindowsSettings);
|
||||
}
|
||||
#pragma warning disable CS0168
|
||||
catch (Exception exception)
|
||||
|
||||
@@ -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
|
||||
{
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<Import Project="..\..\..\..\Common.Dotnet.CsWinRT.props" />
|
||||
<Import Project="..\..\..\..\Common.Dotnet.AotCompatibility.props" />
|
||||
<PropertyGroup>
|
||||
<RootNamespace>Microsoft.CmdPal.Ext.WindowsSettings</RootNamespace>
|
||||
<OutputPath>$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal</OutputPath>
|
||||
|
||||
@@ -243,6 +243,40 @@ namespace ColorPicker.Helpers
|
||||
$", {chromaticityB.ToString(CultureInfo.InvariantCulture)})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string"/> representation of a Oklab color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> for the Oklab color presentation</param>
|
||||
/// <returns>A <see cref="string"/> representation of a Oklab color</returns>
|
||||
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)})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string"/> representation of a CIE LCh color
|
||||
/// </summary>
|
||||
/// <param name="color">The <see cref="Color"/> for the CIE LCh color presentation</param>
|
||||
/// <returns>A <see cref="string"/> representation of a CIE LCh color</returns>
|
||||
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)})";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns a <see cref="string"/> representation of a CIE XYZ color
|
||||
/// </summary>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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")]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -187,5 +187,20 @@ namespace Wox.Plugin
|
||||
/// Gets plugin ID that generated this result
|
||||
/// </summary>
|
||||
public string PluginID { get; internal set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value indicating whether usage based sorting should be applied to this result.
|
||||
/// </summary>
|
||||
public bool DisableUsageBasedScoring { get; set; }
|
||||
|
||||
public int GetSortOrderScore(int selectedItemMultiplier)
|
||||
{
|
||||
if (DisableUsageBasedScoring)
|
||||
{
|
||||
return Metadata.WeightBoost + Score;
|
||||
}
|
||||
|
||||
return Metadata.WeightBoost + Score + (SelectedCount * selectedItemMultiplier);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,8 +32,10 @@ namespace Microsoft.PowerToys.Settings.UI.Library
|
||||
VisibleColorFormats.Add("HSI", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("HSI")));
|
||||
VisibleColorFormats.Add("HWB", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("HWB")));
|
||||
VisibleColorFormats.Add("NCol", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("NCol")));
|
||||
VisibleColorFormats.Add("CIELAB", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
|
||||
VisibleColorFormats.Add("CIEXYZ", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("CIEXYZ")));
|
||||
VisibleColorFormats.Add("CIELAB", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("CIELAB")));
|
||||
VisibleColorFormats.Add("Oklab", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("Oklab")));
|
||||
VisibleColorFormats.Add("Oklch", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("Oklch")));
|
||||
VisibleColorFormats.Add("VEC4", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("VEC4")));
|
||||
VisibleColorFormats.Add("Decimal", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("Decimal")));
|
||||
VisibleColorFormats.Add("HEX Int", new KeyValuePair<bool, string>(false, ColorFormatHelper.GetDefaultFormat("HEX Int")));
|
||||
|
||||
@@ -80,5 +80,20 @@ namespace Microsoft.PowerToys.Settings.UI.Library.Enumerations
|
||||
/// Color presentation as an 8-digit hexadecimal integer (0xFFFFFFFF)
|
||||
/// </summary>
|
||||
HexInteger = 13,
|
||||
|
||||
/// <summary>
|
||||
/// Color representation as CIELCh color space (L[0..100], C[0..230], h[0°..360°])
|
||||
/// </summary>
|
||||
CIELCh = 14,
|
||||
|
||||
/// <summary>
|
||||
/// Color representation as Oklab color space (L[0..1], a[-0.5..0.5], b[-0.5..0.5])
|
||||
/// </summary>
|
||||
Oklab = 15,
|
||||
|
||||
/// <summary>
|
||||
/// Color representation as Oklch color space (L[0..1], C[0..0.5], h[0°..360°])
|
||||
/// </summary>
|
||||
Oklch = 16,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,8 @@
|
||||
Style="{StaticResource CaptionTextBlockStyle}"
|
||||
Text="{x:Bind Description}"
|
||||
TextTrimming="CharacterEllipsis"
|
||||
TextWrapping="NoWrap" />
|
||||
TextWrapping="NoWrap"
|
||||
ToolTipService.ToolTip="{x:Bind Description}" />
|
||||
</Grid>
|
||||
</DataTemplate>
|
||||
|
||||
|
||||
@@ -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") },
|
||||
|
||||
@@ -1660,11 +1660,11 @@ Made with 💗 by Microsoft and the PowerToys community.</value>
|
||||
<data name="Help_blackness" xml:space="preserve">
|
||||
<value>blackness</value>
|
||||
</data>
|
||||
<data name="Help_chromaticityA" xml:space="preserve">
|
||||
<value>chromaticityA</value>
|
||||
<data name="Help_chromaticityACIE" xml:space="preserve">
|
||||
<value>chromaticity A (CIE Lab)</value>
|
||||
</data>
|
||||
<data name="Help_chromaticityB" xml:space="preserve">
|
||||
<value>chromaticityB</value>
|
||||
<data name="Help_chromaticityBCIE" xml:space="preserve">
|
||||
<value>chromaticity B (CIE Lab)</value>
|
||||
</data>
|
||||
<data name="Help_X_value" xml:space="preserve">
|
||||
<value>X value</value>
|
||||
@@ -4460,7 +4460,7 @@ Activate by holding the key for the character you want to add an accent to, then
|
||||
<data name="NewPlus_Behaviour_Replace_Variables_Info_Card_Title.Text" xml:space="preserve">
|
||||
<value>Commonly used variables</value>
|
||||
<comment>New+ commonly used variables header in the flyout info card</comment>
|
||||
</data>
|
||||
</data>
|
||||
<data name="NewPlus_Year_YYYY_Variable_Description.Text" xml:space="preserve">
|
||||
<value>Year, represented by a full four or five digits, depending on the calendar used.</value>
|
||||
<comment>New+ description of the year $YYYY variable - casing of $YYYY is important</comment>
|
||||
@@ -4999,4 +4999,25 @@ To record a specific window, enter the hotkey with the Alt key in the opposite m
|
||||
<data name="CmdPal_ActivationShortcut.Description" xml:space="preserve">
|
||||
<value>Go to Command Palette settings to customize the activation shortcut.</value>
|
||||
</data>
|
||||
<data name="Help_chromaCIE" xml:space="preserve">
|
||||
<value>chroma (CIE LCh)</value>
|
||||
</data>
|
||||
<data name="Help_hueCIE" xml:space="preserve">
|
||||
<value>hue (CIE LCh)</value>
|
||||
</data>
|
||||
<data name="Help_lightnessOklab" xml:space="preserve">
|
||||
<value>lightness (Oklab/Oklch)</value>
|
||||
</data>
|
||||
<data name="Help_chromaticityAOklab" xml:space="preserve">
|
||||
<value>chromaticity A (Oklab)</value>
|
||||
</data>
|
||||
<data name="Help_chromaticityBOklab" xml:space="preserve">
|
||||
<value>chromaticity B (Oklab)</value>
|
||||
</data>
|
||||
<data name="Help_chromaOklch" xml:space="preserve">
|
||||
<value>chroma (Oklch)</value>
|
||||
</data>
|
||||
<data name="Help_hueOklch" xml:space="preserve">
|
||||
<value>hue (Oklch)</value>
|
||||
</data>
|
||||
</root>
|
||||
122
tools/build/build-installer.ps1
Normal file
122
tools/build/build-installer.ps1
Normal file
@@ -0,0 +1,122 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Build and package PowerToys (CmdPal and installer) for a specific platform and configuration LOCALLY.
|
||||
|
||||
.DESCRIPTION
|
||||
This script automates the end-to-end build and packaging process for PowerToys, including:
|
||||
- Restoring and building all necessary solutions (CmdPal, BugReportTool, StylesReportTool, etc.)
|
||||
- Cleaning up old output
|
||||
- Signing generated .msix packages
|
||||
- Building the WiX-based MSI and bootstrapper installers
|
||||
|
||||
It is designed to work in local development.
|
||||
|
||||
.PARAMETER Platform
|
||||
Specifies the target platform for the build (e.g., 'arm64', 'x64'). Default is 'arm64'.
|
||||
|
||||
.PARAMETER Configuration
|
||||
Specifies the build configuration (e.g., 'Debug', 'Release'). Default is 'Release'.
|
||||
|
||||
.EXAMPLE
|
||||
.\build-installer.ps1
|
||||
Runs the installer build pipeline for ARM64 Release (default).
|
||||
|
||||
.EXAMPLE
|
||||
.\build-installer.ps1 -Platform x64 -Configuration Release
|
||||
Runs the pipeline for x64 Debug.
|
||||
|
||||
.NOTES
|
||||
- Requires MSBuild, WiX Toolset, and Git to be installed and accessible from your environment.
|
||||
- Make sure to run this script from a Developer PowerShell (e.g., VS2022 Developer PowerShell).
|
||||
- Generated MSIX files will be signed using cert-sign-package.ps1.
|
||||
- This script will clean previous outputs under the build directories and installer directory (except *.exe files).
|
||||
- First time run need admin permission to trust the certificate.
|
||||
- The built installer will be placed under: installer/PowerToysSetup/[Platform]/[Configuration]/UserSetup
|
||||
relative to the solution root directory.
|
||||
- The installer can't be run right after the build, I need to copy it to another file before it can be run.
|
||||
#>
|
||||
|
||||
|
||||
param (
|
||||
[string]$Platform = 'arm64',
|
||||
[string]$Configuration = 'Release'
|
||||
)
|
||||
|
||||
$repoRoot = Resolve-Path "$PSScriptRoot\..\.."
|
||||
Set-Location $repoRoot
|
||||
|
||||
function RunMSBuild {
|
||||
param (
|
||||
[string]$Solution,
|
||||
[string]$ExtraArgs
|
||||
)
|
||||
|
||||
$base = @(
|
||||
$Solution
|
||||
"/p:Platform=`"$Platform`""
|
||||
"/p:Configuration=$Configuration"
|
||||
'/verbosity:normal'
|
||||
'/clp:Summary;PerformanceSummary;ErrorsOnly;WarningsOnly'
|
||||
'/nologo'
|
||||
)
|
||||
|
||||
$cmd = $base + ($ExtraArgs -split ' ')
|
||||
Write-Host ("[MSBUILD] {0} {1}" -f $Solution, ($cmd -join ' '))
|
||||
& msbuild.exe @cmd
|
||||
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
Write-Error ("Build failed: {0} {1}" -f $Solution, $ExtraArgs)
|
||||
exit $LASTEXITCODE
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
function RestoreThenBuild {
|
||||
param ([string]$Solution)
|
||||
|
||||
# 1) restore
|
||||
RunMSBuild $Solution '/t:restore /p:RestorePackagesConfig=true'
|
||||
# 2) build -------------------------------------------------
|
||||
RunMSBuild $Solution '/m'
|
||||
}
|
||||
|
||||
Write-Host ("Make sure wix is installed and available")
|
||||
& "$PSScriptRoot\ensure-wix.ps1"
|
||||
|
||||
Write-Host ("[PIPELINE] Start | Platform={0} Configuration={1}" -f $Platform, $Configuration)
|
||||
Write-Host ''
|
||||
|
||||
$cmdpalOutputPath = Join-Path $repoRoot "$Platform\$Configuration\WinUI3Apps\CmdPal"
|
||||
|
||||
if (Test-Path $cmdpalOutputPath) {
|
||||
Write-Host "[CLEAN] Removing previous output: $cmdpalOutputPath"
|
||||
Remove-Item $cmdpalOutputPath -Recurse -Force -ErrorAction Ignore
|
||||
}
|
||||
|
||||
RestoreThenBuild '.\PowerToys.sln'
|
||||
|
||||
$msixSearchRoot = Join-Path $repoRoot "$Platform\$Configuration"
|
||||
$msixFiles = Get-ChildItem -Path $msixSearchRoot -Recurse -Filter *.msix |
|
||||
Select-Object -ExpandProperty FullName
|
||||
|
||||
if ($msixFiles.Count) {
|
||||
Write-Host ("[SIGN] .msix file(s): {0}" -f ($msixFiles -join '; '))
|
||||
& "$PSScriptRoot\cert-sign-package.ps1" -TargetPaths $msixFiles
|
||||
}
|
||||
else {
|
||||
Write-Warning "[SIGN] No .msix files found in $msixSearchRoot"
|
||||
}
|
||||
|
||||
RestoreThenBuild '.\tools\BugReportTool\BugReportTool.sln'
|
||||
RestoreThenBuild '.\tools\StylesReportTool\StylesReportTool.sln'
|
||||
|
||||
Write-Host '[CLEAN] installer (keep *.exe)'
|
||||
git clean -xfd -e '*.exe' -- .\installer\ | Out-Null
|
||||
|
||||
RunMSBuild '.\installer\PowerToysSetup.sln' '/t:restore /p:RestorePackagesConfig=true'
|
||||
|
||||
RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysInstaller /p:PerUser=true'
|
||||
|
||||
RunMSBuild '.\installer\PowerToysSetup.sln' '/m /t:PowerToysBootstrapper /p:PerUser=true'
|
||||
|
||||
Write-Host '[PIPELINE] Completed'
|
||||
159
tools/build/cert-management.ps1
Normal file
159
tools/build/cert-management.ps1
Normal file
@@ -0,0 +1,159 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ensures a code signing certificate exists and is trusted in all necessary certificate stores.
|
||||
|
||||
.DESCRIPTION
|
||||
This script provides two functions:
|
||||
|
||||
1. EnsureCertificate:
|
||||
- Searches for an existing code signing certificate by subject name.
|
||||
- If not found, creates a new self-signed certificate.
|
||||
- Exports the certificate and attempts to import it into:
|
||||
- CurrentUser\TrustedPeople
|
||||
- CurrentUser\Root
|
||||
- LocalMachine\Root (admin privileges may be required)
|
||||
|
||||
2. ImportAndVerifyCertificate:
|
||||
- Imports a `.cer` file into the specified certificate store if not already present.
|
||||
- Verifies the certificate is successfully imported by checking thumbprint.
|
||||
|
||||
This is useful in build or signing pipelines to ensure a valid and trusted certificate is available before signing MSIX or executable files.
|
||||
|
||||
.PARAMETER certSubject
|
||||
The subject name of the certificate to search for or create. Default is:
|
||||
"CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
|
||||
.PARAMETER cerPath
|
||||
(ImportAndVerifyCertificate only) The file path to a `.cer` certificate file to import.
|
||||
|
||||
.PARAMETER storePath
|
||||
(ImportAndVerifyCertificate only) The destination certificate store path (e.g. Cert:\CurrentUser\Root).
|
||||
|
||||
.EXAMPLE
|
||||
$cert = EnsureCertificate
|
||||
|
||||
Ensures the default certificate exists and is trusted, and returns the certificate object.
|
||||
|
||||
.EXAMPLE
|
||||
ImportAndVerifyCertificate -cerPath "$env:TEMP\temp_cert.cer" -storePath "Cert:\CurrentUser\Root"
|
||||
|
||||
Imports a certificate into the CurrentUser Root store and verifies its presence.
|
||||
|
||||
.NOTES
|
||||
- For full trust, administrative privileges may be needed to import into LocalMachine\Root.
|
||||
- Certificates are created using RSA and SHA256 and marked as CodeSigningCert.
|
||||
#>
|
||||
|
||||
function ImportAndVerifyCertificate {
|
||||
param (
|
||||
[string]$cerPath,
|
||||
[string]$storePath
|
||||
)
|
||||
|
||||
$thumbprint = (Get-PfxCertificate -FilePath $cerPath).Thumbprint
|
||||
|
||||
$existingCert = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
|
||||
if ($existingCert) {
|
||||
Write-Host "Certificate already exists in $storePath"
|
||||
return $true
|
||||
}
|
||||
|
||||
try {
|
||||
$null = Import-Certificate -FilePath $cerPath -CertStoreLocation $storePath -ErrorAction Stop
|
||||
} catch {
|
||||
Write-Warning "Failed to import certificate to $storePath : $_"
|
||||
return $false
|
||||
}
|
||||
|
||||
$imported = Get-ChildItem -Path $storePath | Where-Object { $_.Thumbprint -eq $thumbprint }
|
||||
if ($imported) {
|
||||
Write-Host "Certificate successfully imported to $storePath"
|
||||
return $true
|
||||
} else {
|
||||
Write-Warning "Certificate not found in $storePath after import"
|
||||
return $false
|
||||
}
|
||||
}
|
||||
|
||||
function EnsureCertificate {
|
||||
param (
|
||||
[string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US"
|
||||
)
|
||||
|
||||
$cert = Get-ChildItem -Path Cert:\CurrentUser\My |
|
||||
Where-Object { $_.Subject -eq $certSubject } |
|
||||
Sort-Object NotAfter -Descending |
|
||||
Select-Object -First 1
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Host "Certificate not found. Creating a new one..."
|
||||
|
||||
$cert = New-SelfSignedCertificate -Subject $certSubject `
|
||||
-CertStoreLocation "Cert:\CurrentUser\My" `
|
||||
-KeyAlgorithm RSA `
|
||||
-Type CodeSigningCert `
|
||||
-HashAlgorithm SHA256
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Error "Failed to create a new certificate."
|
||||
return $null
|
||||
}
|
||||
|
||||
Write-Host "New certificate created with thumbprint: $($cert.Thumbprint)"
|
||||
}
|
||||
else {
|
||||
Write-Host "Using existing certificate with thumbprint: $($cert.Thumbprint)"
|
||||
}
|
||||
|
||||
$cerPath = "$env:TEMP\temp_cert.cer"
|
||||
[void](Export-Certificate -Cert $cert -FilePath $cerPath -Force)
|
||||
|
||||
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\TrustedPeople")) { return $null }
|
||||
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\CurrentUser\Root")) { return $null }
|
||||
if (-not (ImportAndVerifyCertificate -cerPath $cerPath -storePath "Cert:\LocalMachine\Root")) {
|
||||
Write-Warning "Failed to import to LocalMachine\Root (admin may be required)"
|
||||
return $null
|
||||
}
|
||||
|
||||
return $cert
|
||||
}
|
||||
|
||||
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
|
||||
29
tools/build/cert-sign-package.ps1
Normal file
29
tools/build/cert-sign-package.ps1
Normal file
@@ -0,0 +1,29 @@
|
||||
param (
|
||||
[string]$certSubject = "CN=Microsoft Corporation, O=Microsoft Corporation, L=Redmond, S=Washington, C=US",
|
||||
[string[]]$TargetPaths = "C:\PowerToys\ARM64\Release\WinUI3Apps\CmdPal\AppPackages\Microsoft.CmdPal.UI_0.0.1.0_Test\Microsoft.CmdPal.UI_0.0.1.0_arm64.msix"
|
||||
)
|
||||
|
||||
. "$PSScriptRoot\cert-management.ps1"
|
||||
$cert = EnsureCertificate -certSubject $certSubject
|
||||
|
||||
if (-not $cert) {
|
||||
Write-Error "Failed to prepare certificate."
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "Certificate ready: $($cert.Thumbprint)"
|
||||
|
||||
if (-not $TargetPaths -or $TargetPaths.Count -eq 0) {
|
||||
Write-Error "No target files provided to sign."
|
||||
exit 1
|
||||
}
|
||||
|
||||
foreach ($filePath in $TargetPaths) {
|
||||
if (-not (Test-Path $filePath)) {
|
||||
Write-Warning "Skipping: File does not exist - $filePath"
|
||||
continue
|
||||
}
|
||||
|
||||
Write-Host "Signing: $filePath"
|
||||
& signtool sign /sha1 $($cert.Thumbprint) /fd SHA256 /t http://timestamp.digicert.com "$filePath"
|
||||
}
|
||||
71
tools/build/ensure-wix.ps1
Normal file
71
tools/build/ensure-wix.ps1
Normal file
@@ -0,0 +1,71 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
Ensure WiX Toolset 3.14 (build 3141) is installed and ready to use.
|
||||
|
||||
.DESCRIPTION
|
||||
- Skips installation if the toolset is already installed (unless -Force is used).
|
||||
- Otherwise downloads the official installer and binaries, verifies SHA-256, installs silently,
|
||||
and copies wix.targets into the installation directory.
|
||||
.PARAMETER Force
|
||||
Forces reinstallation even if the toolset is already detected.
|
||||
.PARAMETER InstallDir
|
||||
The target installation path. Default is 'C:\Program Files (x86)\WiX Toolset v3.14'.
|
||||
.EXAMPLE
|
||||
.\EnsureWix.ps1 # Ensure WiX is installed
|
||||
.\EnsureWix.ps1 -Force # Force reinstall
|
||||
#>
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[switch]$Force,
|
||||
[string]$InstallDir = 'C:\Program Files (x86)\WiX Toolset v3.14'
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$ProgressPreference = 'SilentlyContinue'
|
||||
|
||||
# Download URLs and expected SHA-256 hashes
|
||||
$WixDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314.exe'
|
||||
$WixBinariesDownloadUrl = 'https://github.com/wixtoolset/wix3/releases/download/wix3141rtm/wix314-binaries.zip'
|
||||
$InstallerHashExpected = '6BF6D03D6923D9EF827AE1D943B90B42B8EBB1B0F68EF6D55F868FA34C738A29'
|
||||
$BinariesHashExpected = '6AC824E1642D6F7277D0ED7EA09411A508F6116BA6FAE0AA5F2C7DAA2FF43D31'
|
||||
|
||||
# Check if WiX is already installed
|
||||
$candlePath = Join-Path $InstallDir 'bin\candle.exe'
|
||||
if (-not $Force -and (Test-Path $candlePath)) {
|
||||
Write-Host "WiX Toolset is already installed at `"$InstallDir`". Skipping installation."
|
||||
return
|
||||
}
|
||||
|
||||
# Temp file paths
|
||||
$tmpDir = [IO.Path]::GetTempPath()
|
||||
$installer = Join-Path $tmpDir 'wix314.exe'
|
||||
$binariesZip = Join-Path $tmpDir 'wix314-binaries.zip'
|
||||
|
||||
# Download installer and binaries
|
||||
Write-Host 'Downloading WiX installer...'
|
||||
Invoke-WebRequest -Uri $WixDownloadUrl -OutFile $installer -UseBasicParsing
|
||||
Write-Host 'Downloading WiX binaries...'
|
||||
Invoke-WebRequest -Uri $WixBinariesDownloadUrl -OutFile $binariesZip -UseBasicParsing
|
||||
|
||||
# Verify SHA-256 hashes
|
||||
Write-Host 'Verifying installer hash...'
|
||||
if ((Get-FileHash -Algorithm SHA256 $installer).Hash -ne $InstallerHashExpected) {
|
||||
throw 'wix314.exe SHA256 hash mismatch'
|
||||
}
|
||||
Write-Host 'Verifying binaries hash...'
|
||||
if ((Get-FileHash -Algorithm SHA256 $binariesZip).Hash -ne $BinariesHashExpected) {
|
||||
throw 'wix314-binaries.zip SHA256 hash mismatch'
|
||||
}
|
||||
|
||||
# Perform silent installation
|
||||
Write-Host 'Installing WiX Toolset silently...'
|
||||
Start-Process -FilePath $installer -ArgumentList '/install','/quiet' -Wait
|
||||
|
||||
# Extract binaries and copy wix.targets
|
||||
$expandDir = Join-Path $tmpDir 'wix-binaries'
|
||||
if (Test-Path $expandDir) { Remove-Item $expandDir -Recurse -Force }
|
||||
Expand-Archive -Path $binariesZip -DestinationPath $expandDir -Force
|
||||
Copy-Item -Path (Join-Path $expandDir 'wix.targets') `
|
||||
-Destination (Join-Path $InstallDir 'wix.targets') -Force
|
||||
|
||||
Write-Host "WiX Toolset has been successfully installed at: $InstallDir"
|
||||
Reference in New Issue
Block a user