From d314fa075e05a95bab2fce1486ca9e3b097be108 Mon Sep 17 00:00:00 2001
From: Heiko <61519853+htcfreek@users.noreply.github.com>
Date: Mon, 21 Apr 2025 10:11:07 +0200
Subject: [PATCH 01/31] [RegPreview] Init with header and add NEW button
(#37626)
* default value
* add new button
* fix tool tip
* update button symbol
* feedback changes
* spellcheck
---
.github/actions/spell-check/expect.txt | 1 +
.../RegistryPreviewMainPage.Events.cs | 58 +++++++++++++++++++
.../RegistryPreviewMainPage.Utilities.cs | 24 ++++++++
.../RegistryPreviewMainPage.xaml | 9 +++
.../Strings/en-US/Resources.resw | 3 +
5 files changed, 95 insertions(+)
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index ea8ffa19ad..6a2041ce18 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -1006,6 +1006,7 @@ netsh
newcolor
NEWDIALOGSTYLE
NEWFILE
+NEWFILEHEADER
newitem
newpath
newplus
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
index 0df3a5c0f5..20009533c1 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Events.cs
@@ -192,12 +192,70 @@ namespace RegistryPreviewUILib
// reload the current Registry file and update the toolbar accordingly.
UpdateToolBarAndUI(await OpenRegistryFile(_appFileName), true, true);
+ // disable the Save button as it's a new file
saveButton.IsEnabled = false;
// restore the TextChanged handler
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
}
+ ///
+ /// Resets the editor content
+ ///
+ private async void NewButton_Click(object sender, RoutedEventArgs e)
+ {
+ // Check to see if the current file has been saved
+ if (saveButton.IsEnabled)
+ {
+ ContentDialog contentDialog = new ContentDialog()
+ {
+ Title = resourceLoader.GetString("YesNoCancelDialogTitle"),
+ Content = resourceLoader.GetString("YesNoCancelDialogContent"),
+ PrimaryButtonText = resourceLoader.GetString("YesNoCancelDialogPrimaryButtonText"),
+ SecondaryButtonText = resourceLoader.GetString("YesNoCancelDialogSecondaryButtonText"),
+ CloseButtonText = resourceLoader.GetString("YesNoCancelDialogCloseButtonText"),
+ DefaultButton = ContentDialogButton.Primary,
+ };
+
+ // Use this code to associate the dialog to the appropriate AppWindow by setting
+ // the dialog's XamlRoot to the same XamlRoot as an element that is already present in the AppWindow.
+ if (ApiInformation.IsApiContractPresent("Windows.Foundation.UniversalApiContract", 8))
+ {
+ contentDialog.XamlRoot = this.Content.XamlRoot;
+ }
+
+ ContentDialogResult contentDialogResult = await contentDialog.ShowAsync();
+ switch (contentDialogResult)
+ {
+ case ContentDialogResult.Primary:
+ // Save, then continue the file open
+ SaveFile();
+ break;
+ case ContentDialogResult.Secondary:
+ // Don't save and continue the file open!
+ saveButton.IsEnabled = false;
+ break;
+ default:
+ // Don't open the new file!
+ return;
+ }
+ }
+
+ // mute the TextChanged handler to make for clean UI
+ MonacoEditor.TextChanged -= MonacoEditor_TextChanged;
+
+ // reset editor, file info and ui.
+ _appFileName = string.Empty;
+ ResetEditorAndFile();
+
+ // restore the TextChanged handler
+ MonacoEditor.TextChanged += MonacoEditor_TextChanged;
+
+ // disable buttons that do not make sense
+ saveButton.IsEnabled = false;
+ refreshButton.IsEnabled = false;
+ }
+
///
/// Opens the Registry Editor; UAC is handled by the request to open
///
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
index 819fee949a..3ec275abf8 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.Utilities.cs
@@ -22,6 +22,8 @@ namespace RegistryPreviewUILib
{
public sealed partial class RegistryPreviewMainPage : Page
{
+ private const string NEWFILEHEADER = "Windows Registry Editor Version 5.00\r\n\r\n";
+
private static SemaphoreSlim _dialogSemaphore = new(1);
private string lastKeyPath;
@@ -77,6 +79,9 @@ namespace RegistryPreviewUILib
}
catch
{
+ // Set default value for empty opening
+ await MonacoEditor.SetTextAsync(NEWFILEHEADER);
+
// restore TextChanged handler to make for clean UI
MonacoEditor.TextChanged += MonacoEditor_TextChanged;
@@ -167,6 +172,25 @@ namespace RegistryPreviewUILib
ChangeCursor(gridPreview, false);
}
+ private async void ResetEditorAndFile()
+ {
+ // Disable parts of the UI that can cause trouble when loading
+ ChangeCursor(gridPreview, true);
+
+ // clear the treeView and dataGrid no matter what
+ treeView.RootNodes.Clear();
+ ClearTable();
+
+ // update the current window's title with the current filename
+ _updateWindowTitleFunction(string.Empty);
+
+ // Set default value for empty opening
+ await MonacoEditor.SetTextAsync(NEWFILEHEADER);
+
+ // Reset the cursor but leave editor disabled as no content got loaded
+ ChangeCursor(gridPreview, false);
+ }
+
///
/// Parses the text that is passed in, which should be the same text that's in editor
///
diff --git a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml
index a48222e9aa..18225902e3 100644
--- a/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml
+++ b/src/modules/registrypreview/RegistryPreviewUILib/RegistryPreviewMainPage.xaml
@@ -55,6 +55,15 @@
HorizontalAlignment="Left"
DefaultLabelPosition="Right">
+
+
+
+
+
Copy value with key path
Like "Copy item"
+
+ New
+
\ No newline at end of file
From 5ec2728dea611479665cbd478b2411c50a221d34 Mon Sep 17 00:00:00 2001
From: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Date: Mon, 21 Apr 2025 18:18:55 -0700
Subject: [PATCH 02/31] Fix build break because of miss Signing for new files
(#39014)
* Add CmdPalKeyboardService.dll as part of sign
* Move 3rd party signing to another section
---
.pipelines/ESRPSigning_core.json | 3 +++
1 file changed, 3 insertions(+)
diff --git a/.pipelines/ESRPSigning_core.json b/.pipelines/ESRPSigning_core.json
index 1b78856032..615b5633bf 100644
--- a/.pipelines/ESRPSigning_core.json
+++ b/.pipelines/ESRPSigning_core.json
@@ -220,6 +220,7 @@
"WinUI3Apps\\PowerToys.Settings.exe",
"PowerToys.CmdPalModuleInterface.dll",
+ "CmdPalKeyboardService.dll",
"*Microsoft.CmdPal.UI_*.msix"
],
"SigningInfo": {
@@ -330,6 +331,8 @@
"TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\TestableIO.System.IO.Abstractions.Wrappers.dll",
"WinUI3Apps\\OpenAI.dll",
+ "Testably.Abstractions.FileSystem.Interface.dll",
+ "WinUI3Apps\\Testably.Abstractions.FileSystem.Interface.dll",
"ColorCode.Core.dll",
"ColorCode.UWP.dll",
"UnitsNet.dll",
From 232e1b79bd3aa8280a15bdd3590f23248e998baa Mon Sep 17 00:00:00 2001
From: "Dustin L. Howett"
Date: Mon, 21 Apr 2025 23:16:42 -0500
Subject: [PATCH 03/31] release: don't publish private symbols for 100 years
(#39015)
---
.pipelines/v2/release.yml | 2 ++
1 file changed, 2 insertions(+)
diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml
index 26819c5d14..2a5be93aec 100644
--- a/.pipelines/v2/release.yml
+++ b/.pipelines/v2/release.yml
@@ -148,5 +148,7 @@ extends:
parameters:
versionNumber: ${{ parameters.versionNumber }}
includePublicSymbolServer: ${{ parameters.publishSymbolsToPublic }}
+ ${{ if ne(parameters.publishSymbolsToPublic, true) }}:
+ symbolExpiryTime: 10 # For private builds, expire symbols within 10 days. The default is 100 years.
subscription: $(SymbolPublishingServiceConnection)
symbolProject: $(SymbolPublishingProject)
From e8b02cd79742b82b368a1fdd37ec34d48cfec6b6 Mon Sep 17 00:00:00 2001
From: leileizhang
Date: Wed, 23 Apr 2025 09:35:40 +0800
Subject: [PATCH 04/31] [ImageResizer]Fix: Deleting an Image Resizer preset
deletes the wrong preset (#38476)
* [ImageResizer]Fix: Deleting an Image Resizer preset deletes the wrong preset
* update the helper
* sort items
---
src/common/ManagedCommon/IdRecoveryHelper.cs | 51 +++++++++++++++++++
.../imageresizer/ui/Models/ResizeSize.cs | 14 ++++-
.../imageresizer/ui/Properties/Settings.cs | 12 +++--
.../Settings.UI.Library/ImageSize.cs | 3 +-
.../ViewModels/ImageResizerViewModel.cs | 1 +
5 files changed, 74 insertions(+), 7 deletions(-)
create mode 100644 src/common/ManagedCommon/IdRecoveryHelper.cs
diff --git a/src/common/ManagedCommon/IdRecoveryHelper.cs b/src/common/ManagedCommon/IdRecoveryHelper.cs
new file mode 100644
index 0000000000..cd0fcb57a5
--- /dev/null
+++ b/src/common/ManagedCommon/IdRecoveryHelper.cs
@@ -0,0 +1,51 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Text;
+using System.Threading.Tasks;
+
+namespace ManagedCommon
+{
+ public static class IdRecoveryHelper
+ {
+ ///
+ /// Fixes invalid IDs in the given list by assigning unique values.
+ /// It ensures that all IDs are non-empty and unique, correcting any duplicates or empty IDs.
+ ///
+ /// The list of items that may contain invalid IDs.
+ public static void RecoverInvalidIds(IEnumerable items)
+ where T : class, IHasId
+ {
+ var idSet = new HashSet();
+ int newId = 0;
+ var sortedItems = items.OrderBy(i => i.Id).ToList(); // Sort items by ID for consistent processing
+
+ // Iterate through the list and fix invalid IDs
+ foreach (var item in sortedItems)
+ {
+ // If the ID is invalid or already exists in the set (duplicate), assign a new unique ID
+ if (!idSet.Add(item.Id))
+ {
+ // Find the next available unique ID
+ while (idSet.Contains(newId))
+ {
+ newId++;
+ }
+
+ item.Id = newId;
+ idSet.Add(newId); // Add the newly assigned ID to the set
+ }
+ }
+ }
+ }
+
+ public interface IHasId
+ {
+ int Id { get; set; }
+ }
+}
diff --git a/src/modules/imageresizer/ui/Models/ResizeSize.cs b/src/modules/imageresizer/ui/Models/ResizeSize.cs
index ebfca83963..49869f4bd2 100644
--- a/src/modules/imageresizer/ui/Models/ResizeSize.cs
+++ b/src/modules/imageresizer/ui/Models/ResizeSize.cs
@@ -11,10 +11,11 @@ using System.Text.Json.Serialization;
using ImageResizer.Helpers;
using ImageResizer.Properties;
+using ManagedCommon;
namespace ImageResizer.Models
{
- public class ResizeSize : Observable
+ public class ResizeSize : Observable, IHasId
{
private static readonly Dictionary _tokens = new Dictionary
{
@@ -24,6 +25,7 @@ namespace ImageResizer.Models
["$phone$"] = Resources.Phone,
};
+ private int _id;
private string _name;
private ResizeFit _fit = ResizeFit.Fit;
private double _width;
@@ -31,8 +33,9 @@ namespace ImageResizer.Models
private bool _showHeight = true;
private ResizeUnit _unit = ResizeUnit.Pixel;
- public ResizeSize(string name, ResizeFit fit, double width, double height, ResizeUnit unit)
+ public ResizeSize(int id, string name, ResizeFit fit, double width, double height, ResizeUnit unit)
{
+ Id = id;
Name = name;
Fit = fit;
Width = width;
@@ -44,6 +47,13 @@ namespace ImageResizer.Models
{
}
+ [JsonPropertyName("Id")]
+ public int Id
+ {
+ get => _id;
+ set => Set(ref _id, value);
+ }
+
[JsonPropertyName("name")]
public virtual string Name
{
diff --git a/src/modules/imageresizer/ui/Properties/Settings.cs b/src/modules/imageresizer/ui/Properties/Settings.cs
index 4d7ebb3848..debb26a191 100644
--- a/src/modules/imageresizer/ui/Properties/Settings.cs
+++ b/src/modules/imageresizer/ui/Properties/Settings.cs
@@ -19,6 +19,7 @@ using System.Threading;
using System.Windows.Media.Imaging;
using ImageResizer.Models;
+using ManagedCommon;
namespace ImageResizer.Properties
{
@@ -63,10 +64,10 @@ namespace ImageResizer.Properties
FileName = "%1 (%2)";
Sizes = new ObservableCollection
{
- new ResizeSize("$small$", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel),
- new ResizeSize("$medium$", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel),
- new ResizeSize("$large$", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel),
- new ResizeSize("$phone$", ResizeFit.Fit, 320, 568, ResizeUnit.Pixel),
+ new ResizeSize(0, "$small$", ResizeFit.Fit, 854, 480, ResizeUnit.Pixel),
+ new ResizeSize(1, "$medium$", ResizeFit.Fit, 1366, 768, ResizeUnit.Pixel),
+ new ResizeSize(2, "$large$", ResizeFit.Fit, 1920, 1080, ResizeUnit.Pixel),
+ new ResizeSize(3, "$phone$", ResizeFit.Fit, 320, 568, ResizeUnit.Pixel),
};
KeepDateModified = false;
FallbackEncoder = new System.Guid("19e4a5aa-5662-4fc5-a0c0-1758028e1057");
@@ -480,6 +481,9 @@ namespace ImageResizer.Properties
{
Sizes.Clear();
Sizes.AddRange(jsonSettings.Sizes);
+
+ // Ensure Ids are unique and handle missing Ids
+ IdRecoveryHelper.RecoverInvalidIds(Sizes);
}
});
diff --git a/src/settings-ui/Settings.UI.Library/ImageSize.cs b/src/settings-ui/Settings.UI.Library/ImageSize.cs
index 017f665820..39b712d67f 100644
--- a/src/settings-ui/Settings.UI.Library/ImageSize.cs
+++ b/src/settings-ui/Settings.UI.Library/ImageSize.cs
@@ -8,11 +8,12 @@ using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
+using ManagedCommon;
using Settings.UI.Library.Resources;
namespace Microsoft.PowerToys.Settings.UI.Library;
-public partial class ImageSize : INotifyPropertyChanged
+public partial class ImageSize : INotifyPropertyChanged, IHasId
{
public event PropertyChangedEventHandler PropertyChanged;
diff --git a/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs
index 511a9cb36d..de70cd3311 100644
--- a/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs
+++ b/src/settings-ui/Settings.UI/ViewModels/ImageResizerViewModel.cs
@@ -67,6 +67,7 @@ public partial class ImageResizerViewModel : Observable
try
{
Settings = _settingsUtils.GetSettings(ModuleName);
+ IdRecoveryHelper.RecoverInvalidIds(Settings.Properties.ImageresizerSizes.Value);
}
catch (Exception e)
{
From e1ad7e39c69072ac21eeaec73f0b2de19dc93e4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Ionu=C8=9B=20Man=C8=9Ba?=
Date: Tue, 22 Apr 2025 20:03:05 -0700
Subject: [PATCH 05/31] [PowerAccent] Cancel previous ShowToolbar task if a new
one is triggered (#37757)
* Cancel previous ShowToolbar task if a new one is triggered
* more changes
* Fix space trigger starting at center
* Removed unneded code
* More changes
* Addressed feedback
* Fix another edge case
---
.../PowerAccent.Core/PowerAccent.cs | 18 ++-----
.../KeyboardListener.cpp | 47 +++++++++++++++----
.../KeyboardListener.h | 11 ++++-
.../KeyboardListener.idl | 2 +-
4 files changed, 53 insertions(+), 25 deletions(-)
diff --git a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs
index c2f0698f25..5d32f1d565 100644
--- a/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs
+++ b/src/modules/poweraccent/PowerAccent.Core/PowerAccent.cs
@@ -62,11 +62,11 @@ public partial class PowerAccent : IDisposable
private void SetEvents()
{
- _keyboardListener.SetShowToolbarEvent(new PowerToys.PowerAccentKeyboardService.ShowToolbar((LetterKey letterKey) =>
+ _keyboardListener.SetShowToolbarEvent(new PowerToys.PowerAccentKeyboardService.ShowToolbar((LetterKey letterKey, TriggerKey trigger ) =>
{
System.Windows.Application.Current.Dispatcher.Invoke(() =>
{
- ShowToolbar(letterKey);
+ ShowToolbar(letterKey, trigger);
});
}));
@@ -92,23 +92,15 @@ public partial class PowerAccent : IDisposable
}));
}
- private void ShowToolbar(LetterKey letterKey)
+ private void ShowToolbar(LetterKey letterKey, TriggerKey trigger)
{
_visible = true;
_characters = GetCharacters(letterKey);
_characterDescriptions = GetCharacterDescriptions(_characters);
_showUnicodeDescription = _settingService.ShowUnicodeDescription;
-
- Task.Delay(_settingService.InputTime).ContinueWith(
- t =>
- {
- if (_visible)
- {
- OnChangeDisplay?.Invoke(true, _characters);
- }
- },
- TaskScheduler.FromCurrentSynchronizationContext());
+ OnChangeDisplay?.Invoke(true, _characters);
+ ProcessNextChar(trigger, false);
}
private string[] GetCharacters(LetterKey letterKey)
diff --git a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp
index 93fb5c0230..3d38d20b91 100644
--- a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp
+++ b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.cpp
@@ -13,7 +13,7 @@
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
{
KeyboardListener::KeyboardListener() :
- m_toolbarVisible(false), m_triggeredWithSpace(false), m_leftShiftPressed(false), m_rightShiftPressed(false), m_triggeredWithLeftArrow(false), m_triggeredWithRightArrow(false)
+ m_toolbarVisible(false), m_activationKeyHold(false), m_triggeredWithSpace(false), m_leftShiftPressed(false), m_rightShiftPressed(false), m_triggeredWithLeftArrow(false), m_triggeredWithRightArrow(false)
{
s_instance = this;
LoggerHelpers::init_logger(L"PowerAccent", L"PowerAccentKeyboardService", "PowerAccent");
@@ -53,8 +53,8 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
void KeyboardListener::SetShowToolbarEvent(ShowToolbar showToolbarEvent)
{
- m_showToolbarCb = [trigger = std::move(showToolbarEvent)](LetterKey key) {
- trigger(key);
+ m_showToolbarCb = [trigger = std::move(showToolbarEvent)](LetterKey key, TriggerKey triggerKey) {
+ trigger(key, triggerKey);
};
}
@@ -152,6 +152,17 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
return false;
}
+ void KeyboardListener::BeginShowToolbar(std::chrono::milliseconds delay, LetterKey key, TriggerKey trigger)
+ {
+ std::unique_lock lock(toolbarMutex);
+ auto result = toolbarCV.wait_for(lock, delay);
+ if (result == std::cv_status::timeout)
+ {
+ m_toolbarVisible = true;
+ m_showToolbarCb(key, trigger);
+ }
+ }
+
bool KeyboardListener::OnKeyDown(KBDLLHOOKSTRUCT info) noexcept
{
auto letterKey = static_cast(info.vkCode);
@@ -199,7 +210,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
}
}
- if (!m_toolbarVisible && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
+ if (!m_toolbarVisible && !m_activationKeyHold && letterPressed != LetterKey::None && triggerPressed && !IsSuppressedByGameMode() && !IsForegroundAppExcluded())
{
Logger::debug(L"Show toolbar. Letter: {}, Trigger: {}", letterPressed, triggerPressed);
@@ -207,11 +218,21 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
m_triggeredWithSpace = triggerPressed == VK_SPACE;
m_triggeredWithLeftArrow = triggerPressed == VK_LEFT;
m_triggeredWithRightArrow = triggerPressed == VK_RIGHT;
- m_toolbarVisible = true;
- m_showToolbarCb(letterPressed);
+ m_activationKeyHold = true;
+ m_bothKeysPressed = true;
+ if (toolbarThread != nullptr)
+ {
+ toolbarCV.notify_all();
+ toolbarThread->join();
+ }
+ toolbarThread = std::make_unique(std::bind(&KeyboardListener::BeginShowToolbar, this, m_settings.inputTime, letterPressed,static_cast(triggerPressed)));
}
- if (m_toolbarVisible && triggerPressed)
+ if (m_activationKeyHold && triggerPressed && !m_toolbarVisible)
+ {
+ return true;
+ }
+ else if (m_toolbarVisible && triggerPressed)
{
if (triggerPressed == VK_LEFT)
{
@@ -251,8 +272,9 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
{
letterPressed = LetterKey::None;
- if (m_toolbarVisible)
+ if (m_toolbarVisible || m_bothKeysPressed)
{
+ m_bothKeysPressed = false;
if (m_stopwatch.elapsed() < m_settings.inputTime)
{
Logger::debug(L"Activation too fast. Do nothing.");
@@ -280,11 +302,18 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
Logger::debug(L"Hide toolbar event and input char");
m_hideToolbarCb(InputType::Char);
-
m_toolbarVisible = false;
}
}
+ auto triggerPressed = info.vkCode;
+
+ if (m_activationKeyHold && (letterPressed == LetterKey::None || (triggerPressed == VK_SPACE || triggerPressed == VK_LEFT || triggerPressed == VK_RIGHT)))
+ {
+ m_activationKeyHold = false;
+ toolbarCV.notify_all();
+ }
+
return false;
}
diff --git a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h
index 61c28e1866..6ec1118775 100644
--- a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h
+++ b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.h
@@ -2,6 +2,7 @@
#include "KeyboardListener.g.h"
#include
+#include
#include
namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
@@ -44,6 +45,7 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
static LRESULT CALLBACK LowLevelKeyboardProc(int nCode, WPARAM wParam, LPARAM lParam);
private:
+ void BeginShowToolbar(std::chrono::milliseconds delay, LetterKey key, TriggerKey trigger);
bool OnKeyDown(KBDLLHOOKSTRUCT info) noexcept;
bool OnKeyUp(KBDLLHOOKSTRUCT info) noexcept;
bool IsSuppressedByGameMode();
@@ -51,9 +53,14 @@ namespace winrt::PowerToys::PowerAccentKeyboardService::implementation
static inline KeyboardListener* s_instance;
HHOOK s_llKeyboardHook = nullptr;
- bool m_toolbarVisible;
+ std::atomic m_toolbarVisible;
+ bool m_activationKeyHold;
+ bool m_bothKeysPressed = false;
+ std::unique_ptr toolbarThread;
+ std::mutex toolbarMutex;
+ std::condition_variable toolbarCV;
PowerAccentSettings m_settings;
- std::function m_showToolbarCb;
+ std::function m_showToolbarCb;
std::function m_hideToolbarCb;
std::function m_nextCharCb;
std::function m_isLanguageLetterCb;
diff --git a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl
index 9bc8448c22..9fc0f42e42 100644
--- a/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl
+++ b/src/modules/poweraccent/PowerAccentKeyboardService/KeyboardListener.idl
@@ -67,7 +67,7 @@ namespace PowerToys
Char
};
- [version(1.0), uuid(37197089-5438-4479-af57-30ab3f3c8be4)] delegate void ShowToolbar(LetterKey key);
+ [version(1.0), uuid(37197089-5438-4479-af57-30ab3f3c8be4)] delegate void ShowToolbar(LetterKey key, TriggerKey trigger);
[version(1.0), uuid(8eb79d6b-1826-424f-9fbc-af21ae19725e)] delegate void HideToolbar(InputType inputType);
[version(1.0), uuid(db72d45c-a5a2-446f-bdc1-506e9121764a)] delegate void NextChar(TriggerKey inputSpace, boolean shiftPressed);
[version(1.0), uuid(20be2919-2b91-4313-b6e0-4c3484fe91ef)] delegate void IsLanguageLetter(LetterKey key, [out] boolean* result);
From d4e577bb81ffe71233842fc17b854be563f80655 Mon Sep 17 00:00:00 2001
From: Yu Leng <42196638+moooyo@users.noreply.github.com>
Date: Wed, 23 Apr 2025 11:40:20 +0800
Subject: [PATCH 06/31] [cmdpal] Add setting "ignore hotkey when full screen"
(#38923)
* init
* merge main
* merge main
---------
Co-authored-by: Yu Leng (from Dev Box)
---
.../SettingsModel.cs | 2 ++
.../SettingsViewModel.cs | 10 +++++++
.../Helpers/NativeMethods.cs | 26 +++++++++++++++++
.../Helpers/WindowHelper.cs | 28 +++++++++++++++++++
.../Microsoft.CmdPal.UI/MainWindow.xaml.cs | 12 ++++++++
.../Settings/GeneralPage.xaml | 3 ++
.../Strings/en-us/Resources.resw | 6 ++++
7 files changed, 87 insertions(+)
create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/NativeMethods.cs
create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowHelper.cs
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
index 891c45ace9..27dd119b48 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsModel.cs
@@ -40,6 +40,8 @@ public partial class SettingsModel : ObservableObject
public bool ShowSystemTrayIcon { get; set; } = true;
+ public bool IgnoreShortcutWhenFullscreen { get; set; } = true;
+
public Dictionary ProviderSettings { get; set; } = [];
public Dictionary Aliases { get; set; } = [];
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
index b6c5214890..2083d35191 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/SettingsViewModel.cs
@@ -108,6 +108,16 @@ public partial class SettingsViewModel : INotifyPropertyChanged
}
}
+ public bool IgnoreShortcutWhenFullscreen
+ {
+ get => _settings.IgnoreShortcutWhenFullscreen;
+ set
+ {
+ _settings.IgnoreShortcutWhenFullscreen = value;
+ Save();
+ }
+ }
+
public ObservableCollection CommandProviders { get; } = [];
public SettingsViewModel(SettingsModel settings, IServiceProvider serviceProvider, TaskScheduler scheduler)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/NativeMethods.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/NativeMethods.cs
new file mode 100644
index 0000000000..a3227ca77c
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/NativeMethods.cs
@@ -0,0 +1,26 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+using System.Security;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+[SuppressUnmanagedCodeSecurity]
+internal static class NativeMethods
+{
+ [DllImport("shell32.dll")]
+ public static extern int SHQueryUserNotificationState(out UserNotificationState state);
+}
+
+internal enum UserNotificationState : int
+{
+ QUNS_NOT_PRESENT = 1,
+ QUNS_BUSY,
+ QUNS_RUNNING_D3D_FULL_SCREEN,
+ QUNS_PRESENTATION_MODE,
+ QUNS_ACCEPTS_NOTIFICATIONS,
+ QUNS_QUIET_TIME,
+ QUNS_APP,
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowHelper.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowHelper.cs
new file mode 100644
index 0000000000..c0d257088e
--- /dev/null
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Helpers/WindowHelper.cs
@@ -0,0 +1,28 @@
+// Copyright (c) Microsoft Corporation
+// The Microsoft Corporation licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for more information.
+
+using System.Runtime.InteropServices;
+
+namespace Microsoft.CmdPal.UI.Helpers;
+
+internal sealed partial class WindowHelper
+{
+ public static bool IsWindowFullscreen()
+ {
+ UserNotificationState state;
+
+ // https://learn.microsoft.com/en-us/windows/win32/api/shellapi/ne-shellapi-query_user_notification_state
+ if (Marshal.GetExceptionForHR(NativeMethods.SHQueryUserNotificationState(out state)) == null)
+ {
+ if (state == UserNotificationState.QUNS_RUNNING_D3D_FULL_SCREEN ||
+ state == UserNotificationState.QUNS_BUSY ||
+ state == UserNotificationState.QUNS_PRESENTATION_MODE)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
index be28b0edc3..ce20c9be77 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs
@@ -43,6 +43,7 @@ public sealed partial class MainWindow : Window,
private readonly WNDPROC? _hotkeyWndProc;
private readonly WNDPROC? _originalWndProc;
private readonly List _hotkeys = [];
+ private bool _ignoreHotKeyWhenFullScreen = true;
// Stylistically, window messages are WM_*
#pragma warning disable SA1310 // Field names should not contain underscore
@@ -157,6 +158,8 @@ public sealed partial class MainWindow : Window,
SetupHotkey(settings);
SetupTrayIcon(settings.ShowSystemTrayIcon);
+ _ignoreHotKeyWhenFullScreen = settings.IgnoreShortcutWhenFullscreen;
+
// This will prevent our window from appearing in alt+tab or the taskbar.
// You'll _need_ to use the hotkey to summon it.
AppWindow.IsShownInSwitchers = System.Diagnostics.Debugger.IsAttached;
@@ -504,6 +507,15 @@ public sealed partial class MainWindow : Window,
var hotkeyIndex = (int)wParam.Value;
if (hotkeyIndex < _hotkeys.Count)
{
+ if (_ignoreHotKeyWhenFullScreen)
+ {
+ // If we're in full screen mode, ignore the hotkey
+ if (WindowHelper.IsWindowFullscreen())
+ {
+ return (LRESULT)IntPtr.Zero;
+ }
+ }
+
var hotkey = _hotkeys[hotkeyIndex];
HandleSummon(hotkey.CommandId);
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
index 68763ed5bb..497076bb0e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Settings/GeneralPage.xaml
@@ -48,6 +48,9 @@
+
+
+
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 9ffc14002b..51b33f8ede 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
@@ -328,6 +328,12 @@ Right-click to remove the key combination, thereby deactivating the shortcut.
Try this if there are issues with the shortcut (Command Palette might not get focus when triggered from an elevated window)
+
+ Ignore shortcut in fullscreen mode
+
+
+ Preventing disruption of the program running in fullscreen by unintentional activation of shortcut
+
Go home when activated
From 583614449d3f6dd9585c523ba0b2ea4d9f02a78c Mon Sep 17 00:00:00 2001
From: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date: Wed, 23 Apr 2025 15:39:54 +0800
Subject: [PATCH 07/31] [Workspaces]Fix for steam games capture&launch: capture
and correctly launch steam games. (#38380)
* Workspaces fix: capture steam games.
* minor fix
* Launch steam apps by url appmodeluserid instead of directly exe call.
* fix copilot comment
* fix
* remove unnecessary string
* expect words
* white list words
* Order of alphabet
* exclude thin frame if it's not a steam game.
* fix build
* fix regression
* adjust comment
---
.github/actions/spell-check/allow/code.txt | 2 +-
.github/actions/spell-check/expect.txt | 6 +-
.../WorkspacesLauncher/AppLauncher.cpp | 16 ++
.../Workspaces/WorkspacesLib/AppUtils.cpp | 36 ++++
.../Workspaces/WorkspacesLib/AppUtils.h | 2 +
.../WorkspacesLib/SteamGameHelper.cpp | 171 ++++++++++++++++++
.../Workspaces/WorkspacesLib/SteamHelper.h | 24 +++
.../WorkspacesLib/WorkspacesLib.vcxproj | 2 +
.../WorkspacesLib.vcxproj.filters | 6 +
.../WorkspacesSnapshotTool/SnapshotUtils.cpp | 13 +-
.../WindowArranger.cpp | 8 +
.../workspaces-common/WindowFilter.h | 6 +-
.../workspaces-common/WindowUtils.h | 7 +
13 files changed, 294 insertions(+), 5 deletions(-)
create mode 100644 src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
create mode 100644 src/modules/Workspaces/WorkspacesLib/SteamHelper.h
diff --git a/.github/actions/spell-check/allow/code.txt b/.github/actions/spell-check/allow/code.txt
index 619a036b32..10e9473258 100644
--- a/.github/actions/spell-check/allow/code.txt
+++ b/.github/actions/spell-check/allow/code.txt
@@ -273,4 +273,4 @@ mengyuanchen
testhost
#Tools
-OIP
+OIP
\ No newline at end of file
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 6a2041ce18..1e8b3d91b5 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -8,6 +8,7 @@ Acceleratorkeys
ACCEPTFILES
ACCESSDENIED
ACCESSTOKEN
+acfs
AClient
AColumn
acrt
@@ -523,6 +524,7 @@ FZE
gacutil
Gaeilge
Gaidhlig
+gameid
GC'ed
GCLP
gdi
@@ -712,6 +714,7 @@ INPUTSINK
INPUTTYPE
INSTALLDESKTOPSHORTCUT
INSTALLDIR
+installdir
INSTALLFOLDER
INSTALLFOLDERTOBOOTSTRAPPERINSTALLFOLDER
INSTALLFOLDERTOPREVIOUSINSTALLFOLDER
@@ -1569,6 +1572,7 @@ stdcpp
stdcpplatest
STDMETHODCALLTYPE
STDMETHODIMP
+steamapps
STGC
STGM
STGMEDIUM
@@ -1969,4 +1973,4 @@ zoomit
ZOOMITX
ZXk
ZXNs
-zzz
+zzz
\ No newline at end of file
diff --git a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp
index 4d4b47ea12..7075da2ea7 100644
--- a/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp
+++ b/src/modules/Workspaces/WorkspacesLauncher/AppLauncher.cpp
@@ -121,6 +121,22 @@ namespace AppLauncher
// packaged apps: try launching first by AppUserModel.ID
// usage example: elevated Terminal
if (!launched && !app.appUserModelId.empty() && !app.packageFullName.empty())
+ {
+ Logger::trace(L"Launching {} as {} - {app.packageFullName}", app.name, app.appUserModelId, app.packageFullName);
+ auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
+ if (res.isOk())
+ {
+ launched = true;
+ }
+ else
+ {
+ launchErrors.push_back({ std::filesystem::path(app.path).filename(), res.error() });
+ }
+ }
+
+ // win32 app with appUserModelId:
+ // usage example: steam games
+ if (!launched && !app.appUserModelId.empty())
{
Logger::trace(L"Launching {} as {}", app.name, app.appUserModelId);
auto res = LaunchApp(L"shell:AppsFolder\\" + app.appUserModelId, app.commandLineArgs, app.isElevated);
diff --git a/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp b/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp
index 19b33214b7..a37d82f8ca 100644
--- a/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp
+++ b/src/modules/Workspaces/WorkspacesLib/AppUtils.cpp
@@ -1,5 +1,6 @@
#include "pch.h"
#include "AppUtils.h"
+#include "SteamHelper.h"
#include
#include
@@ -34,6 +35,8 @@ namespace Utils
constexpr const wchar_t* EdgeFilename = L"msedge.exe";
constexpr const wchar_t* ChromeFilename = L"chrome.exe";
+
+ constexpr const wchar_t* SteamUrlProtocol = L"steam:";
}
AppList IterateAppsFolder()
@@ -138,6 +141,34 @@ namespace Utils
else if (prop == NonLocalizable::PackageInstallPathProp || prop == NonLocalizable::InstallPathProp)
{
data.installPath = propVariantString.m_pData;
+
+ if (!data.installPath.empty())
+ {
+ const bool isSteamProtocol = data.installPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
+
+ if (isSteamProtocol)
+ {
+ Logger::info(L"Found steam game: protocol path: {}", data.installPath);
+ data.protocolPath = data.installPath;
+
+ try
+ {
+ auto gameId = Steam::GetGameIdFromUrlProtocolPath(data.installPath);
+ auto gameFolder = Steam::GetSteamGameInfoFromAcfFile(gameId);
+
+ if (gameFolder)
+ {
+ data.installPath = gameFolder->gameInstallationPath;
+ Logger::info(L"Found steam game: physical path: {}", data.installPath);
+ }
+ }
+ catch (std::exception ex)
+ {
+ Logger::error(L"Failed to get installPath for game {}", data.installPath);
+ Logger::error("Error: {}", ex.what());
+ }
+ }
+ }
}
}
@@ -397,5 +428,10 @@ namespace Utils
{
return installPath.ends_with(NonLocalizable::ChromeFilename);
}
+
+ bool AppData::IsSteamGame() const
+ {
+ return protocolPath.rfind(NonLocalizable::SteamUrlProtocol, 0) == 0;
+ }
}
}
\ No newline at end of file
diff --git a/src/modules/Workspaces/WorkspacesLib/AppUtils.h b/src/modules/Workspaces/WorkspacesLib/AppUtils.h
index 3c81049f83..80b5e2fd49 100644
--- a/src/modules/Workspaces/WorkspacesLib/AppUtils.h
+++ b/src/modules/Workspaces/WorkspacesLib/AppUtils.h
@@ -13,10 +13,12 @@ namespace Utils
std::wstring packageFullName;
std::wstring appUserModelId;
std::wstring pwaAppId;
+ std::wstring protocolPath;
bool canLaunchElevated = false;
bool IsEdge() const;
bool IsChrome() const;
+ bool IsSteamGame() const;
};
using AppList = std::vector;
diff --git a/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp b/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
new file mode 100644
index 0000000000..404002e284
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesLib/SteamGameHelper.cpp
@@ -0,0 +1,171 @@
+#include "pch.h"
+#include "SteamHelper.h"
+#include
+#include
+#include
+#include
+#include
+#include
+
+namespace Utils
+{
+
+ static std::wstring Utf8ToWide(const std::string& utf8)
+ {
+ if (utf8.empty())
+ return L"";
+
+ int size = MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), nullptr, 0);
+ if (size <= 0)
+ return L"";
+
+ std::wstring wide(size, L'\0');
+ MultiByteToWideChar(CP_UTF8, 0, utf8.data(), static_cast(utf8.size()), wide.data(), size);
+ return wide;
+ }
+
+ namespace Steam
+ {
+ using namespace std;
+ namespace fs = std::filesystem;
+
+ static std::optional GetSteamExePathFromRegistry()
+ {
+ static std::optional cachedPath;
+ if (cachedPath.has_value())
+ {
+ return cachedPath;
+ }
+
+ const std::vector roots = { HKEY_CLASSES_ROOT, HKEY_LOCAL_MACHINE, HKEY_USERS };
+ const std::vector subKeys = {
+ L"steam\\shell\\open\\command",
+ L"Software\\Classes\\steam\\shell\\open\\command",
+ };
+
+ for (HKEY root : roots)
+ {
+ for (const auto& subKey : subKeys)
+ {
+ HKEY hKey;
+ if (RegOpenKeyExW(root, subKey.c_str(), 0, KEY_READ, &hKey) == ERROR_SUCCESS)
+ {
+ wchar_t value[512];
+ DWORD size = sizeof(value);
+ DWORD type = 0;
+
+ if (RegQueryValueExW(hKey, nullptr, nullptr, &type, reinterpret_cast(value), &size) == ERROR_SUCCESS &&
+ (type == REG_SZ || type == REG_EXPAND_SZ))
+ {
+ std::wregex exeRegex(LR"delim("([^"]+steam\.exe)")delim");
+ std::wcmatch match;
+ if (std::regex_search(value, match, exeRegex) && match.size() > 1)
+ {
+ RegCloseKey(hKey);
+ cachedPath = match[1].str();
+ return cachedPath;
+ }
+ }
+
+ RegCloseKey(hKey);
+ }
+ }
+ }
+
+ cachedPath = std::nullopt;
+ return std::nullopt;
+ }
+
+ static fs::path GetSteamBasePath()
+ {
+ auto steamFolderOpt = GetSteamExePathFromRegistry();
+ if (!steamFolderOpt)
+ {
+ return {};
+ }
+
+ return fs::path(*steamFolderOpt).parent_path() / L"steamapps";
+ }
+
+ static fs::path GetAcfFilePath(const std::wstring& gameId)
+ {
+ auto steamFolderOpt = GetSteamExePathFromRegistry();
+ if (!steamFolderOpt)
+ {
+ return {};
+ }
+
+ return GetSteamBasePath() / (L"appmanifest_" + gameId + L".acf");
+ }
+
+ static fs::path GetGameInstallPath(const std::wstring& gameFolderName)
+ {
+ auto steamFolderOpt = GetSteamExePathFromRegistry();
+ if (!steamFolderOpt)
+ {
+ return {};
+ }
+
+ return GetSteamBasePath() / L"common" / gameFolderName;
+ }
+
+ static unordered_map ParseAcfFile(const fs::path& acfPath)
+ {
+ unordered_map result;
+
+ ifstream file(acfPath);
+ if (!file.is_open())
+ return result;
+
+ string line;
+ while (getline(file, line))
+ {
+ smatch matches;
+ static const regex pattern(R"delim("([^"]+)"\s+"([^"]+)")delim");
+
+ if (regex_search(line, matches, pattern) && matches.size() == 3)
+ {
+ wstring key = Utf8ToWide(matches[1].str());
+ wstring value = Utf8ToWide(matches[2].str());
+ result[key] = value;
+ }
+ }
+
+ return result;
+ }
+
+ std::unique_ptr GetSteamGameInfoFromAcfFile(const std::wstring& gameId)
+ {
+ fs::path acfPath = Steam::GetAcfFilePath(gameId);
+
+ if (!fs::exists(acfPath))
+ return nullptr;
+
+ auto kv = ParseAcfFile(acfPath);
+ if (kv.empty() || kv.find(L"installdir") == kv.end())
+ return nullptr;
+
+ fs::path gamePath = Steam::GetGameInstallPath(kv[L"installdir"]);
+ if (!fs::exists(gamePath))
+ return nullptr;
+
+ auto game = std::make_unique();
+ game->gameId = gameId;
+ game->gameInstallationPath = gamePath.wstring();
+ return game;
+ }
+
+ std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath)
+ {
+ const std::wstring steamGamePrefix = L"steam://rungameid/";
+
+ if (urlPath.rfind(steamGamePrefix, 0) == 0)
+ {
+ return urlPath.substr(steamGamePrefix.length());
+ }
+
+ return L"";
+ }
+
+ }
+}
\ No newline at end of file
diff --git a/src/modules/Workspaces/WorkspacesLib/SteamHelper.h b/src/modules/Workspaces/WorkspacesLib/SteamHelper.h
new file mode 100644
index 0000000000..a80a942f4a
--- /dev/null
+++ b/src/modules/Workspaces/WorkspacesLib/SteamHelper.h
@@ -0,0 +1,24 @@
+#pragma once
+
+#include "pch.h"
+
+namespace Utils
+{
+ namespace NonLocalizable
+ {
+ const std::wstring AcfFileNameTemplate = L"appmanifest_.acfs";
+ }
+
+ namespace Steam
+ {
+ struct SteamGame
+ {
+ std::wstring gameId;
+ std::wstring gameInstallationPath;
+ };
+
+ std::unique_ptr GetSteamGameInfoFromAcfFile(const std::wstring& gameId);
+
+ std::wstring GetGameIdFromUrlProtocolPath(const std::wstring& urlPath);
+ }
+}
diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj
index 27394e29ee..7d29741a0d 100644
--- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj
+++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj
@@ -41,6 +41,7 @@
+
@@ -57,6 +58,7 @@
Create
+
diff --git a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters
index b066c16a57..f4f17c55ee 100644
--- a/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters
+++ b/src/modules/Workspaces/WorkspacesLib/WorkspacesLib.vcxproj.filters
@@ -53,6 +53,9 @@
Header Files
+
+ Header Files
+
@@ -88,6 +91,9 @@
Source Files
+
+ Source Files
+
diff --git a/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp b/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp
index 1d5bc8a179..a8b7c13108 100644
--- a/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp
+++ b/src/modules/Workspaces/WorkspacesSnapshotTool/SnapshotUtils.cpp
@@ -71,6 +71,8 @@ namespace SnapshotUtils
continue;
}
+ Logger::info("Try to get window app:{}", reinterpret_cast(window));
+
DWORD pid{};
GetWindowThreadProcessId(window, &pid);
@@ -118,10 +120,19 @@ namespace SnapshotUtils
auto data = Utils::Apps::GetApp(processPath, pid, installedApps);
if (!data.has_value() || data->name.empty())
{
- Logger::info(L"Installed app not found: {}", processPath);
+ Logger::info(L"Installed app not found:{},{}", reinterpret_cast(window), processPath);
continue;
}
+ if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
+ {
+ // Only care about steam games if it has no thick frame to remain consistent with
+ // the behavior as before.
+ continue;
+ }
+
+ Logger::info(L"Found app for window:{},{}", reinterpret_cast(window), processPath);
+
auto appData = data.value();
bool isEdge = appData.IsEdge();
diff --git a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp
index 7b04135d1a..538579979f 100644
--- a/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp
+++ b/src/modules/Workspaces/WorkspacesWindowArranger/WindowArranger.cpp
@@ -200,6 +200,14 @@ std::optional WindowArranger::GetNearestWindow(const Workspa
}
auto data = Utils::Apps::GetApp(processPath, pid, m_installedApps);
+
+ if (!data->IsSteamGame() && !WindowUtils::HasThickFrame(window))
+ {
+ // Only care about steam games if it has no thick frame to remain consistent with
+ // the behavior as before.
+ continue;
+ }
+
if (!data.has_value())
{
continue;
diff --git a/src/modules/Workspaces/workspaces-common/WindowFilter.h b/src/modules/Workspaces/workspaces-common/WindowFilter.h
index c76ad81237..8ae1a5411b 100644
--- a/src/modules/Workspaces/workspaces-common/WindowFilter.h
+++ b/src/modules/Workspaces/workspaces-common/WindowFilter.h
@@ -9,10 +9,12 @@ namespace WindowFilter
{
auto style = GetWindowLong(window, GWL_STYLE);
bool isPopup = WindowUtils::HasStyle(style, WS_POPUP);
- bool hasThickFrame = WindowUtils::HasStyle(style, WS_THICKFRAME);
bool hasCaption = WindowUtils::HasStyle(style, WS_CAPTION);
bool hasMinimizeMaximizeButtons = WindowUtils::HasStyle(style, WS_MINIMIZEBOX) || WindowUtils::HasStyle(style, WS_MAXIMIZEBOX);
- if (isPopup && !(hasThickFrame && (hasCaption || hasMinimizeMaximizeButtons)))
+
+ Logger::info("Style for window: {}, {:#x}", reinterpret_cast(window), style);
+
+ if (isPopup && !(hasCaption || hasMinimizeMaximizeButtons))
{
// popup windows we want to snap: e.g. Calculator, Telegram
// popup windows we don't want to snap: start menu, notification popup, tray window, etc.
diff --git a/src/modules/Workspaces/workspaces-common/WindowUtils.h b/src/modules/Workspaces/workspaces-common/WindowUtils.h
index 8424591dfa..79051f4ea2 100644
--- a/src/modules/Workspaces/workspaces-common/WindowUtils.h
+++ b/src/modules/Workspaces/workspaces-common/WindowUtils.h
@@ -121,4 +121,11 @@ namespace WindowUtils
return std::wstring(title);
}
+
+
+ inline bool HasThickFrame(HWND window)
+ {
+ auto style = GetWindowLong(window, GWL_STYLE);
+ return WindowUtils::HasStyle(style, WS_THICKFRAME);
+ }
}
\ No newline at end of file
From 4be612983559a2667816221f88d32f16240b5dbf Mon Sep 17 00:00:00 2001
From: leileizhang
Date: Wed, 23 Apr 2025 17:05:29 +0800
Subject: [PATCH 08/31] [Fuzzing] Add PowerRename Fuzzing and C++ Project Setup
Guidance (#38922)
* add fuzz
* update solution
* update pipeline
* update solution
* remove arm64
* use reference
* revert the code
* add fuzzing readme
* update solution
* fix spell-check
* Parent reference don't need update
* remove fuzzing config
* add debug config
* update the config
---
.github/actions/spell-check/expect.txt | 5 +
PowerToys.sln | 9 ++
doc/Fuzzing/CppFuzzingGuide.md | 117 ++++++++++++++++++
.../OneFuzzConfig.json | 40 ++++++
.../PowerRename.FuzzingTest.cpp | 67 ++++++++++
.../PowerRename.FuzzingTest.filters | 25 ++++
.../PowerRename.FuzzingTest.vcxproj | 107 ++++++++++++++++
7 files changed, 370 insertions(+)
create mode 100644 doc/Fuzzing/CppFuzzingGuide.md
create mode 100644 src/modules/powerrename/PowerRename.FuzzingTest/OneFuzzConfig.json
create mode 100644 src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.cpp
create mode 100644 src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.filters
create mode 100644 src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 1e8b3d91b5..daa6449a66 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -519,6 +519,7 @@ FRAMECHANGED
frm
Froml
FROMTOUCH
+fsanitize
fsmgmt
FZE
gacutil
@@ -631,6 +632,7 @@ HOTKEYF
hotkeys
hotlight
hotspot
+Hostx
HPAINTBUFFER
HRAWINPUT
HREDRAW
@@ -1080,6 +1082,7 @@ NOTRACK
NOTSRCCOPY
NOTSRCERASE
NOTXORPEN
+notwindows
NOZORDER
NPH
npmjs
@@ -1401,6 +1404,7 @@ sacl
safeprojectname
SAMEKEYPREVIOUSLYMAPPED
SAMESHORTCUTPREVIOUSLYMAPPED
+sancov
SAVEFAILED
scanled
schedtasks
@@ -1936,6 +1940,7 @@ WUX
Wwanpp
XAxis
xclip
+xcopy
XDocument
XElement
xfd
diff --git a/PowerToys.sln b/PowerToys.sln
index 9fca8d2a3a..201c0fbefe 100644
--- a/PowerToys.sln
+++ b/PowerToys.sln
@@ -708,6 +708,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.CmdPal.Ext.System
EndProject
Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "CmdPalKeyboardService", "src\modules\cmdpal\CmdPalKeyboardService\CmdPalKeyboardService.vcxproj", "{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}"
EndProject
+Project("{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}") = "PowerRename.FuzzingTest", "src\modules\powerrename\PowerRename.FuzzingTest\PowerRename.FuzzingTest.vcxproj", "{2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|ARM64 = Debug|ARM64
@@ -2588,6 +2590,12 @@ Global
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|ARM64.Build.0 = Release|ARM64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.ActiveCfg = Release|x64
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2}.Release|x64.Build.0 = Release|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|ARM64.ActiveCfg = Debug|ARM64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.ActiveCfg = Debug|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Debug|x64.Build.0 = Debug|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|ARM64.ActiveCfg = Release|ARM64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.ActiveCfg = Release|x64
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E}.Release|x64.Build.0 = Release|x64
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -2859,6 +2867,7 @@ Global
{5702B3CC-8575-48D5-83D8-15BB42269CD3} = {929C1324-22E8-4412-A9A8-80E85F3985A5}
{64B88F02-CD88-4ED8-9624-989A800230F9} = {ECB8E0D1-7603-4E5C-AB10-D1E545E6F8E2}
{5F63C743-F6CE-4DBA-A200-2B3F8A14E8C2} = {3846508C-77EB-4034-A702-F8BB263C4F79}
+ {2694E2FB-DCD5-4BFF-A418-B6C3C7CE3B8E} = {89E20BCE-EB9C-46C8-8B50-E01A82E6FDC3}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {C3A2F9D1-7930-4EF4-A6FC-7EE0A99821D0}
diff --git a/doc/Fuzzing/CppFuzzingGuide.md b/doc/Fuzzing/CppFuzzingGuide.md
new file mode 100644
index 0000000000..448d60a7d4
--- /dev/null
+++ b/doc/Fuzzing/CppFuzzingGuide.md
@@ -0,0 +1,117 @@
+# 🧪 C++ Project Fuzzing Test Guide
+
+This guide walks you through setting up a **fuzzing test** project for a C++ module using [libFuzzer](https://llvm.org/docs/LibFuzzer.html).
+.
+
+---
+
+## 🏗️ Step-by-Step Setup
+
+### 1. Create a New C++ Project
+
+- Use **Empty Project** template.
+- Name it `.FuzzingTest`.
+
+---
+
+### 2. Update Build Configuration
+
+- In **Configuration Manager**, Uncheck Build for both Release|ARM64, Debug|ARM64 and Debug|x64 configurations.
+- Note: ARM64 is not supported in this case, so leave ARM64 configurations build disabled.
+---
+
+### 3. Enable ASan and libFuzzer in `.vcxproj`
+
+Edit the project file to enable fuzzing:
+
+```xml
+
+ true
+ true
+
+```
+
+---
+
+### 4. Add Fuzzing Compiler Flags
+
+Add this to `AdditionalOptions` under the `Fuzzing` configuration:
+
+```xml
+/fsanitize=address
+/fsanitize-coverage=inline-8bit-counters
+/fsanitize-coverage=edge
+/fsanitize-coverage=trace-cmp
+/fsanitize-coverage=trace-div
+%(AdditionalOptions)
+```
+
+---
+
+### 5. Link the Sanitizer Coverage Runtime
+
+In `Linker → Input → Additional Dependencies`, add:
+
+```text
+$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib
+```
+
+---
+
+### 6. Copy Required Runtime DLL
+
+Add a `PostBuildEvent` to copy the ASAN DLL:
+
+```xml
+
+ xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
+
+```
+
+---
+
+### 7. Add Preprocessor Definitions
+
+To avoid annotation issues, add these to the `Preprocessor Definitions`:
+
+```text
+_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
+```
+
+---
+
+## 🧬 Required Code
+
+### `LLVMFuzzerTestOneInput` Entry Point
+
+Every fuzzing project must expose this function:
+
+```cpp
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+ std::string input(reinterpret_cast(data), size);
+
+ try
+ {
+ // Call your module with the input here.
+ }
+ catch (...) {}
+
+ return 0;
+}
+```
+
+---
+
+## ⚙️ [Test run in the cloud](https://eng.ms/docs/cloud-ai-platform/azure-edge-platform-aep/aep-security/epsf-edge-and-platform-security-fundamentals/the-onefuzz-service/onefuzz/faq/notwindows/walkthrough)
+
+To submit a job to the cloud you can run with this command:
+
+```
+oip submit --config .\OneFuzzConfig.json --drop-path --platform windows --do-not-file-bugs --duration 1
+```
+You want to run with --do-not-file-bugs because if there is an issue with running the parser in the cloud (which is very possible), you don't want bugs to be created if there is an issue. The --duration task is the number of hours you want the task to run. I recommend just running for 1 hour to make sure things work initially. If you don't specify this parameter, it will default to 48 hours. You can find more about submitting a test job here.
+
+OneFuzz will send you an email when the job has started.
+
+---
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/OneFuzzConfig.json b/src/modules/powerrename/PowerRename.FuzzingTest/OneFuzzConfig.json
new file mode 100644
index 0000000000..41ceb9414a
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/OneFuzzConfig.json
@@ -0,0 +1,40 @@
+{
+ "configVersion": 3,
+ "entries": [
+ {
+ "Fuzzer": {
+ "$type": "libfuzzer",
+ "FuzzingHarnessExecutableName": "PowerRename.FuzzingTest.exe"
+ },
+ "adoTemplate": {
+ // supply the values appropriate to your
+ // project, where bugs will be filed
+ "org": "microsoft",
+ "project": "OS",
+ "AssignedTo": "leilzh@microsoft.com",
+ "AreaPath": "OS\\Windows Client and Services\\WinPD\\DEEP-Developer Experience, Ecosystem and Partnerships\\DIVE\\SALT",
+ "IterationPath": "OS\\Future"
+ },
+ "jobNotificationEmail": "PowerToys@microsoft.com",
+ "skip": false,
+ "rebootAfterSetup": false,
+ "oneFuzzJobs": [
+ // at least one job is required
+ {
+ "projectName": "PowerToys.PowerRename",
+ "targetName": "PowerRename_Fuzzer"
+ }
+ ],
+ "jobDependencies": [
+ // this should contain, at minimum,
+ // the DLL and PDB files
+ // you will need to add any other files required
+ // (globs are supported)
+ "PowerRename.FuzzingTest.exe",
+ "PowerRename.FuzzingTest.pdb",
+ "PowerRename.FuzzingTest.lib",
+ "clang_rt.asan_dynamic-x86_64.dll"
+ ]
+ }
+ ]
+}
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.cpp b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.cpp
new file mode 100644
index 0000000000..5a060ad3fb
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.cpp
@@ -0,0 +1,67 @@
+// Test.cpp : This file contains the 'main' function. Program execution begins and ends there.
+//
+
+#include
+#include
+#include
+
+extern "C" int LLVMFuzzerTestOneInput(const uint8_t* data, size_t size)
+{
+ if (size < 6)
+ return 0;
+
+ size_t offset = 0;
+
+ size_t input_len = size / 3;
+ size_t find_len = size / 3;
+ size_t replace_len = size - input_len - find_len;
+
+ auto read_wstring = [&](size_t len) -> std::wstring {
+ std::wstring result;
+ if (offset + len > size)
+ len = size - offset;
+
+ result.assign(reinterpret_cast(data + offset), len / sizeof(wchar_t));
+ offset += len;
+ return result;
+ };
+
+ std::wstring input = read_wstring(input_len);
+ std::wstring find = read_wstring(find_len);
+ std::wstring replace = read_wstring(replace_len);
+
+ if (find.empty() || replace.empty())
+ return 0;
+
+ CComPtr renamer;
+ CPowerRenameRegEx::s_CreateInstance(&renamer);
+
+ renamer->PutFlags(UseRegularExpressions | CaseSensitive);
+
+ renamer->PutSearchTerm(find.c_str());
+ renamer->PutReplaceTerm(replace.c_str());
+
+ PWSTR result = nullptr;
+ unsigned long index = 0;
+ HRESULT hr = renamer->Replace(input.c_str(), &result, index);
+ if (SUCCEEDED(hr) && result != nullptr)
+ {
+ CoTaskMemFree(result);
+ }
+
+ return 0;
+}
+
+#ifndef DISABLE_FOR_FUZZING
+
+int main(int argc, char** argv)
+{
+ const char8_t raw[] = u8"test_string";
+
+ std::vector data(reinterpret_cast(raw), reinterpret_cast(raw) + sizeof(raw) - 1);
+
+ LLVMFuzzerTestOneInput(data.data(), data.size());
+ return 0;
+}
+
+#endif
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.filters b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.filters
new file mode 100644
index 0000000000..21d23a2ee7
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.filters
@@ -0,0 +1,25 @@
+
+
+
+
+ {4FC737F1-C7A5-4376-A066-2A32D752A2FF}
+ cpp;c;cc;cxx;c++;cppm;ixx;def;odl;idl;hpj;bat;asm;asmx
+
+
+ {93995380-89BD-4b04-88EB-625FBE52EBFB}
+ h;hh;hpp;hxx;h++;hm;inl;inc;ipp;xsd
+
+
+ {67DA6AB6-F800-4c08-8B7A-83BB121AAD01}
+ rc;ico;cur;bmp;dlg;rc2;rct;bin;rgs;gif;jpg;jpeg;jpe;resx;tiff;tif;png;wav;mfcribbon-ms
+
+
+
+
+ Source Files
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
new file mode 100644
index 0000000000..0d0838ad06
--- /dev/null
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
@@ -0,0 +1,107 @@
+
+
+
+
+ 17.0
+ Win32Proj
+ {2694e2fb-dcd5-4bff-a418-b6c3c7ce3b8e}
+ Test
+ 10.0.22621.0
+ PowerRename.FuzzingTest
+
+
+
+ Application
+ false
+ v143
+ Unicode
+
+
+ true
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+ $(VC_LibraryPath_x64);$(WindowsSDK_LibraryPath_x64);$(VCToolsInstallDir)\lib\$(Platform)
+ ..\..\..\..\$(Platform)\$(Configuration)\tests\PowerRename.FuzzTests\
+
+
+
+ Level3
+ true
+ true
+ true
+ NDEBUG;_CONSOLE;DISABLE_FOR_FUZZING;%(PreprocessorDefinitions);_DISABLE_VECTOR_ANNOTATION;_DISABLE_STRING_ANNOTATION
+ true
+ NotUsing
+ /fsanitize=address /fsanitize-coverage=inline-8bit-counters /fsanitize-coverage=edge /fsanitize-coverage=trace-cmp /fsanitize-coverage=trace-div %(AdditionalOptions)
+ MultiThreaded
+ stdcpplatest
+ ..\;..\lib\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ Console
+ true
+ true
+ true
+ legacy_stdio_definitions.lib;$(VCToolsInstallDir)lib\$(Platform)\libsancov.lib;$(CoreLibraryDependencies);%(AdditionalDependencies)
+
+
+ xcopy /y "$(VCToolsInstallDir)bin\Hostx64\x64\clang_rt.asan_dynamic-x86_64.dll" "$(OutDir)"
+ Copy the required ASan runtime DLL to the output directory.
+
+
+
+
+ Level3
+ true
+ true
+ true
+ _DEBUG;_CONSOLE;%(PreprocessorDefinitions)
+ NotUsing
+ ..\;..\lib\;..\..\..\;%(AdditionalIncludeDirectories)
+
+
+ Console
+ $(OutDir)\..\..\WinUI3Apps\PowerRenameLib.lib;comctl32.lib;pathcch.lib;kernel32.lib;user32.lib;gdi32.lib;winspool.lib;comdlg32.lib;advapi32.lib;shell32.lib;ole32.lib;oleaut32.lib;uuid.lib;odbc32.lib;odbccp32.lib;Pathcch.lib;%(AdditionalDependencies)
+
+
+
+
+
+
+
+
+
+
+ {51920f1f-c28c-4adf-8660-4238766796c2}
+
+
+ {6955446d-23f7-4023-9bb3-8657f904af99}
+
+
+
+
+
+
+
+
+
+
+ This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
+
+
+
+
+
+
+
\ No newline at end of file
From 252cf2670f829bcac8e4df1350e54cdab34bb1df Mon Sep 17 00:00:00 2001
From: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com>
Date: Wed, 23 Apr 2025 11:30:36 +0200
Subject: [PATCH 09/31] [CmdPal] Hide commands that shouldn't be visible
(#39011)
hide commands from more commands
---
.../Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
index 420f41f49f..739c3257f3 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
@@ -109,7 +109,7 @@ public partial class CommandBarViewModel : ObservableObject,
if (SelectedItem.MoreCommands.Count() > 1)
{
ShouldShowContextMenu = true;
- ContextCommands = [.. SelectedItem.AllCommands];
+ ContextCommands = [.. SelectedItem.AllCommands.Where(c => c.ShouldBeVisible)];
}
else
{
From 884bfc71d3efa6aaae902013269f9d9f137091d3 Mon Sep 17 00:00:00 2001
From: Niels Laute
Date: Wed, 23 Apr 2025 13:35:19 +0200
Subject: [PATCH 10/31] [CmdPal] Tweaks to the detailspane UX (#38972)
Details pane UX tweaks. Closes: https://github.com/microsoft/PowerToys/issues/38973
Before:

After:

---
.../cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml | 9 +++++----
1 file changed, 5 insertions(+), 4 deletions(-)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
index b45f685986..f97334466e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml
@@ -356,9 +356,8 @@
From f085ba0cd26aee7aac2beb8080ad0bd3aca0f83a Mon Sep 17 00:00:00 2001
From: Niels Laute
Date: Wed, 23 Apr 2025 13:40:33 +0200
Subject: [PATCH 11/31] [CmdPal] Better support for long labels in empty
content and commandbar (#38974)
Closes: #38970
Before:
After:
---
.../Microsoft.CmdPal.UI/Controls/CommandBar.xaml | 4 +++-
.../Microsoft.CmdPal.UI/ExtViews/ListPage.xaml | 13 +++++++++----
2 files changed, 12 insertions(+), 5 deletions(-)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
index 38ea017125..896ee11d0d 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/CommandBar.xaml
@@ -71,9 +71,9 @@
Padding="4"
ColumnSpacing="8">
-
+
+ Text="{x:Bind ViewModel.EmptyContent.Title, Mode=OneWay}"
+ TextAlignment="Center"
+ TextWrapping="Wrap" />
+ Text="{x:Bind ViewModel.EmptyContent.Subtitle, Mode=OneWay}"
+ TextAlignment="Center"
+ TextWrapping="Wrap" />
From b0e7473760d89da6a6da378470c99559499b9745 Mon Sep 17 00:00:00 2001
From: Mike Griese
Date: Wed, 23 Apr 2025 06:45:34 -0500
Subject: [PATCH 12/31] CmdPal: Tidy up some winget experiences (#38174)
I'm filing this so that I don't lose it on this machine I use less often. We can probably hold it out of 0.90
Fixes:
* If a package is installed, we always display the version as "Unknown"
* also deals with a case where getting the package metadata could fail, and we'd hide the list item. That's only possible in the "installed, no updates available" case
* Allow package updates, add an icon for updates
* moves off the preview winget API onto a higher stable version
---
Directory.Packages.props | 2 +-
NOTICE.md | 2 +-
.../Pages/InstallPackageCommand.cs | 52 +++++++++++++++----
.../Pages/InstallPackageListItem.cs | 25 +++++++--
.../Properties/Resources.Designer.cs | 9 ++++
.../Properties/Resources.resx | 4 ++
6 files changed, 76 insertions(+), 18 deletions(-)
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 4393655dab..0d8879746d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -45,7 +45,7 @@
-
+
diff --git a/NOTICE.md b/NOTICE.md
index 92bdf5fe39..edf8e75c51 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1473,7 +1473,7 @@ SOFTWARE.
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
- Microsoft.WindowsAppSDK 1.6.250205002
-- Microsoft.WindowsPackageManager.ComInterop 1.10.120-preview
+- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
- ModernWpfUI 0.9.4
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs
index 1aa22bfa26..cf85359bdf 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageCommand.cs
@@ -22,10 +22,12 @@ public partial class InstallPackageCommand : InvokableCommand
private IAsyncOperationWithProgress? _unInstallAction;
private Task? _installTask;
- public bool IsInstalled { get; private set; }
+ public PackageInstallCommandState InstallCommandState { get; private set; }
public static IconInfo CompletedIcon { get; } = new("\uE930"); // Completed
+ public static IconInfo UpdateIcon { get; } = new("\uE74A"); // Up
+
public static IconInfo DownloadIcon { get; } = new("\uE896"); // Download
public static IconInfo DeleteIcon { get; } = new("\uE74D"); // Delete
@@ -44,23 +46,41 @@ public partial class InstallPackageCommand : InvokableCommand
internal bool SkipDependencies { get; set; }
- public InstallPackageCommand(CatalogPackage package, bool isInstalled)
+ public InstallPackageCommand(CatalogPackage package, PackageInstallCommandState isInstalled)
{
_package = package;
- IsInstalled = isInstalled;
+ InstallCommandState = isInstalled;
UpdateAppearance();
}
internal void FakeChangeStatus()
{
- IsInstalled = !IsInstalled;
+ InstallCommandState = InstallCommandState switch
+ {
+ PackageInstallCommandState.Install => PackageInstallCommandState.Uninstall,
+ PackageInstallCommandState.Update => PackageInstallCommandState.Uninstall,
+ PackageInstallCommandState.Uninstall => PackageInstallCommandState.Install,
+ _ => throw new NotImplementedException(),
+ };
UpdateAppearance();
}
private void UpdateAppearance()
{
- Icon = IsInstalled ? CompletedIcon : DownloadIcon;
- Name = IsInstalled ? Properties.Resources.winget_uninstall_name : Properties.Resources.winget_install_name;
+ Icon = InstallCommandState switch
+ {
+ PackageInstallCommandState.Install => DownloadIcon,
+ PackageInstallCommandState.Update => UpdateIcon,
+ PackageInstallCommandState.Uninstall => CompletedIcon,
+ _ => throw new NotImplementedException(),
+ };
+ Name = InstallCommandState switch
+ {
+ PackageInstallCommandState.Install => Properties.Resources.winget_install_name,
+ PackageInstallCommandState.Update => Properties.Resources.winget_update_name,
+ PackageInstallCommandState.Uninstall => Properties.Resources.winget_uninstall_name,
+ _ => throw new NotImplementedException(),
+ };
}
public override ICommandResult Invoke()
@@ -72,7 +92,7 @@ public partial class InstallPackageCommand : InvokableCommand
return CommandResult.KeepOpen();
}
- if (IsInstalled)
+ if (InstallCommandState == PackageInstallCommandState.Uninstall)
{
// Uninstall
_installBanner.State = MessageState.Info;
@@ -88,7 +108,8 @@ public partial class InstallPackageCommand : InvokableCommand
_installTask = Task.Run(() => TryDoInstallOperation(_unInstallAction));
}
- else
+ else if (InstallCommandState is PackageInstallCommandState.Install or
+ PackageInstallCommandState.Update)
{
// Install
_installBanner.State = MessageState.Info;
@@ -117,7 +138,8 @@ public partial class InstallPackageCommand : InvokableCommand
try
{
await action.AsTask();
- _installBanner.Message = IsInstalled ?
+
+ _installBanner.Message = InstallCommandState == PackageInstallCommandState.Uninstall ?
string.Format(CultureInfo.CurrentCulture, UninstallPackageFinished, _package.Name) :
string.Format(CultureInfo.CurrentCulture, InstallPackageFinished, _package.Name);
@@ -125,9 +147,10 @@ public partial class InstallPackageCommand : InvokableCommand
_installBanner.State = MessageState.Success;
_installTask = null;
- _ = Task.Run(() =>
+ _ = Task.Run(async () =>
{
- Thread.Sleep(2500);
+ await Task.Delay(2500).ConfigureAwait(false);
+
if (_installTask == null)
{
WinGetExtensionHost.Instance.HideStatus(_installBanner);
@@ -228,3 +251,10 @@ public partial class InstallPackageCommand : InvokableCommand
}
}
}
+
+public enum PackageInstallCommandState
+{
+ Uninstall = 0,
+ Update = 1,
+ Install = 2,
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs
index 2ce192f262..5c556ec3cb 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Pages/InstallPackageListItem.cs
@@ -6,6 +6,7 @@ using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
+using System.Runtime.InteropServices;
using System.Threading.Tasks;
using ManagedCommon;
using Microsoft.CommandPalette.Extensions;
@@ -31,7 +32,7 @@ public partial class InstallPackageListItem : ListItem
{
_package = package;
- var version = _package.DefaultInstallVersion;
+ var version = _package.DefaultInstallVersion ?? _package.InstalledVersion;
var versionTagText = "Unknown";
if (version != null)
{
@@ -49,7 +50,16 @@ public partial class InstallPackageListItem : ListItem
private Details? BuildDetails(PackageVersionInfo? version)
{
- var metadata = version?.GetCatalogPackageMetadata();
+ CatalogPackageMetadata? metadata = null;
+ try
+ {
+ metadata = version?.GetCatalogPackageMetadata();
+ }
+ catch (COMException ex)
+ {
+ Logger.LogWarning($"{ex.ErrorCode}");
+ }
+
if (metadata != null)
{
if (metadata.Tags.Where(t => t.Equals(WinGetExtensionPage.ExtensionsTag, StringComparison.OrdinalIgnoreCase)).Any())
@@ -149,12 +159,17 @@ public partial class InstallPackageListItem : ListItem
var status = await _package.CheckInstalledStatusAsync();
var isInstalled = _package.InstalledVersion != null;
+ var installedState = isInstalled ?
+ (_package.IsUpdateAvailable ?
+ PackageInstallCommandState.Update : PackageInstallCommandState.Uninstall) :
+ PackageInstallCommandState.Install;
+
// might be an uninstall command
- InstallPackageCommand installCommand = new(_package, isInstalled);
+ InstallPackageCommand installCommand = new(_package, installedState);
if (isInstalled)
{
- this.Icon = InstallPackageCommand.CompletedIcon;
+ this.Icon = installCommand.Icon;
this.Command = new NoOpCommand();
List contextMenu = [];
CommandContextItem uninstallContextItem = new(installCommand)
@@ -180,7 +195,7 @@ public partial class InstallPackageListItem : ListItem
}
// didn't find the app
- _installCommand = new InstallPackageCommand(_package, isInstalled);
+ _installCommand = new InstallPackageCommand(_package, installedState);
this.Command = _installCommand;
Icon = _installCommand.Icon;
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs
index f62a291d36..418cccf58c 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.Designer.cs
@@ -330,6 +330,15 @@ namespace Microsoft.CmdPal.Ext.WinGet.Properties {
}
}
+ ///
+ /// Looks up a localized string similar to Update.
+ ///
+ public static string winget_update_name {
+ get {
+ return ResourceManager.GetString("winget_update_name", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to View online.
///
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx
index 2be07cec68..ac2128430d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WinGet/Properties/Resources.resx
@@ -154,6 +154,10 @@
Install
+
+ Update
+
+
Uninstalling {0}...
{0} will be replaced by the name of an app package
From 54e058e82dd7ff7186b293b3efa4d0b738bcc375 Mon Sep 17 00:00:00 2001
From: Mike Griese
Date: Wed, 23 Apr 2025 10:54:01 -0500
Subject: [PATCH 13/31] Fully initialize context menus when they change
(#38998)
If we don't slow-initialize the whole menu when it changes, then we won't see that there's secondary (& more) commands.
Tested this with the extension from [waaverecords/CmdPal.Ext.Spotify#4](https://github.com/waaverecords/CmdPal.Ext.Spotify/pull/4)
Closes #38959
Also seemingly closes #38347 - seems that needed additional bumping of the `EmptyContent`'s
---
.../Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs | 6 ++++++
.../Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs | 2 +-
.../cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs | 4 +++-
3 files changed, 10 insertions(+), 2 deletions(-)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
index 739c3257f3..a476fba179 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandBarViewModel.cs
@@ -29,6 +29,8 @@ public partial class CommandBarViewModel : ObservableObject,
field = value;
SetSelectedItem(value);
+
+ OnPropertyChanged(nameof(SelectedItem));
}
}
@@ -115,6 +117,10 @@ public partial class CommandBarViewModel : ObservableObject,
{
ShouldShowContextMenu = false;
}
+
+ OnPropertyChanged(nameof(HasSecondaryCommand));
+ OnPropertyChanged(nameof(SecondaryCommand));
+ OnPropertyChanged(nameof(ShouldShowContextMenu));
}
// InvokeItemCommand is what this will be in Xaml due to source generator
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs
index 73bb041b99..f8b0e90834 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContentPageViewModel.cs
@@ -167,7 +167,7 @@ public partial class ContentPageViewModel : PageViewModel, ICommandBarContext
Commands.ForEach(contextItem =>
{
- contextItem.InitializeProperties();
+ contextItem.SlowInitializeProperties();
});
}
else
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
index b45ea08f54..bcafb0235e 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ListViewModel.cs
@@ -436,7 +436,7 @@ public partial class ListViewModel : PageViewModel, IDisposable
break;
case nameof(EmptyContent):
EmptyContent = new(new(model.EmptyContent), PageContext);
- EmptyContent.InitializeProperties();
+ EmptyContent.SlowInitializeProperties();
break;
case nameof(IsLoading):
UpdateEmptyContent();
@@ -454,6 +454,8 @@ public partial class ListViewModel : PageViewModel, IDisposable
return;
}
+ UpdateProperty(nameof(EmptyContent));
+
DoOnUiThread(
() =>
{
From 6c317c4ee196376fdcfd063d18b2118a4b5de7e7 Mon Sep 17 00:00:00 2001
From: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Date: Wed, 23 Apr 2025 16:09:51 -0700
Subject: [PATCH 14/31] Pre-creation of generated folder for any csproj
(#39018)
* Test for Precreation of generated folder for any csproj to prevent random build break
---
src/Common.Dotnet.CsWinRT.props | 2 ++
src/Common.Dotnet.PrepareGeneratedFolder.targets | 16 ++++++++++++++++
.../EnvironmentVariablesUILib.csproj | 5 -----
3 files changed, 18 insertions(+), 5 deletions(-)
create mode 100644 src/Common.Dotnet.PrepareGeneratedFolder.targets
diff --git a/src/Common.Dotnet.CsWinRT.props b/src/Common.Dotnet.CsWinRT.props
index e4731ce2fd..cde2ce69ce 100644
--- a/src/Common.Dotnet.CsWinRT.props
+++ b/src/Common.Dotnet.CsWinRT.props
@@ -1,6 +1,8 @@
+
+
10.0.22621.57
net9.0-windows10.0.22621.0
diff --git a/src/Common.Dotnet.PrepareGeneratedFolder.targets b/src/Common.Dotnet.PrepareGeneratedFolder.targets
new file mode 100644
index 0000000000..d017590064
--- /dev/null
+++ b/src/Common.Dotnet.PrepareGeneratedFolder.targets
@@ -0,0 +1,16 @@
+
+
+
+
+
+ $(ProjectDir)obj\g
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj
index 24820b7d83..3973f8d2eb 100644
--- a/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj
+++ b/src/modules/EnvironmentVariables/EnvironmentVariablesUILib/EnvironmentVariablesUILib.csproj
@@ -15,8 +15,6 @@
PowerToys.EnvironmentVariablesUILib.pri
true
true
-
- $(ProjectDir)obj\g
@@ -56,7 +54,4 @@
-
-
-
From 8dfa55fe28d6f89d3cd80e88f65064281b6cb223 Mon Sep 17 00:00:00 2001
From: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Date: Wed, 23 Apr 2025 18:25:47 -0700
Subject: [PATCH 15/31] Update to WinAppSDK 1.7 latest version (#39016)
* Update to WinAppSDK 1.7 latest version
* Update UpdateVersions.ps1
Signed-off-by: Shawn Yuan
Co-authored-by: Dustin L. Howett
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Co-authored-by: Shawn Yuan
Co-authored-by: Clint Rutkas
---
.pipelines/UpdateVersions.ps1 | 72 ++++++++++++++-----
.../v2/ci-using-the-latest-winappsdk.yml | 2 +-
...eps-update-winappsdk-and-restore-nuget.yml | 1 +
Directory.Packages.props | 2 +-
NOTICE.md | 2 +-
.../PowerToys.MeasureToolCore.vcxproj | 8 +--
.../MeasureToolCore/packages.config | 2 +-
.../Directory.Packages.props | 2 +-
.../Microsoft.Terminal.UI.vcxproj | 2 +-
...icrosoft.CommandPalette.Extensions.vcxproj | 2 +-
.../packages.config | 2 +-
.../PowerRenameUILib/PowerRenameUI.vcxproj | 8 +--
.../PowerRenameUILib/packages.config | 2 +-
13 files changed, 73 insertions(+), 34 deletions(-)
diff --git a/.pipelines/UpdateVersions.ps1 b/.pipelines/UpdateVersions.ps1
index c19dfb5dec..a1bc5bef9a 100644
--- a/.pipelines/UpdateVersions.ps1
+++ b/.pipelines/UpdateVersions.ps1
@@ -1,16 +1,24 @@
Param(
- # Using the default value of 1.6 for winAppSdkVersionNumber and useExperimentalVersion as false
+ # Using the default value of 1.7 for winAppSdkVersionNumber and useExperimentalVersion as false
[Parameter(Mandatory=$False,Position=1)]
- [string]$winAppSdkVersionNumber = "1.6",
+ [string]$winAppSdkVersionNumber = "1.7",
# When the pipeline calls the PS1 file, the passed parameters are converted to string type
[Parameter(Mandatory=$False,Position=2)]
- [boolean]$useExperimentalVersion = $False
+ [boolean]$useExperimentalVersion = $False,
+
+ # Root folder Path for processing
+ [Parameter(Mandatory=$False,Position=3)]
+ [string]$rootPath = $(Split-Path -Parent (Split-Path -Parent $MyInvocation.MyCommand.Path)),
+
+ # Root folder Path for processing
+ [Parameter(Mandatory=$False,Position=4)]
+ [string]$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
)
function Update-NugetConfig {
param (
- [string]$filePath = "nuget.config"
+ [string]$filePath = [System.IO.Path]::Combine($rootPath, "nuget.config")
)
Write-Host "Updating nuget.config file"
@@ -35,7 +43,33 @@ function Update-NugetConfig {
$xml.Save($filePath)
}
-$sourceLink = "https://microsoft.pkgs.visualstudio.com/ProjectReunion/_packaging/Project.Reunion.nuget.internal/nuget/v3/index.json"
+function Read-FileWithEncoding {
+ param (
+ [string]$Path
+ )
+
+ $reader = New-Object System.IO.StreamReader($Path, $true) # auto-detect encoding
+ $content = $reader.ReadToEnd()
+ $encoding = $reader.CurrentEncoding
+ $reader.Close()
+
+ return [PSCustomObject]@{
+ Content = $content
+ Encoding = $encoding
+ }
+}
+
+function Write-FileWithEncoding {
+ param (
+ [string]$Path,
+ [string]$Content,
+ [System.Text.Encoding]$Encoding
+ )
+
+ $writer = New-Object System.IO.StreamWriter($Path, $false, $Encoding)
+ $writer.Write($Content)
+ $writer.Close()
+}
# Execute nuget list and capture the output
if ($useExperimentalVersion) {
@@ -79,50 +113,54 @@ if ($latestVersion) {
}
# Update packages.config files
-Get-ChildItem -Recurse packages.config | ForEach-Object {
- $content = Get-Content $_.FullName -Raw
+Get-ChildItem -Path $rootPath -Recurse packages.config | ForEach-Object {
+ $file = Read-FileWithEncoding -Path $_.FullName
+ $content = $file.Content
if ($content -match 'package id="Microsoft.WindowsAppSDK"') {
$newVersionString = 'package id="Microsoft.WindowsAppSDK" version="' + $WinAppSDKVersion + '"'
$oldVersionString = 'package id="Microsoft.WindowsAppSDK" version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $_.FullName -Value $content
+ Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update Directory.Packages.props file
-$propsFile = "Directory.Packages.props"
+$propsFile = [System.IO.Path]::Combine($rootPath,"Directory.Packages.props")
if (Test-Path $propsFile) {
- $content = Get-Content $propsFile -Raw
+ $file = Read-FileWithEncoding -Path $propsFile
+ $content = $file.Content
if ($content -match ''
$oldVersionString = ''
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $propsFile -Value $content
+ Write-FileWithEncoding -Path $propsFile -Content $content -Encoding $file.encoding
Write-Host "Modified " $propsFile
}
}
# Update .vcxproj files
-Get-ChildItem -Recurse *.vcxproj | ForEach-Object {
- $content = Get-Content $_.FullName -Raw
+Get-ChildItem -Path $rootPath -Recurse *.vcxproj | ForEach-Object {
+ $file = Read-FileWithEncoding -Path $_.FullName
+ $content = $file.Content
if ($content -match '\\Microsoft.WindowsAppSDK.') {
$newVersionString = '\Microsoft.WindowsAppSDK.' + $WinAppSDKVersion + '\'
$oldVersionString = '\\Microsoft.WindowsAppSDK.[-.0-9a-zA-Z]*\\'
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $_.FullName -Value $content
+ Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
# Update .csproj files
-Get-ChildItem -Recurse *.csproj | ForEach-Object {
- $content = Get-Content $_.FullName -Raw
+Get-ChildItem -Path $rootPath -Recurse *.csproj | ForEach-Object {
+ $file = Read-FileWithEncoding -Path $_.FullName
+ $content = $file.Content
if ($content -match 'PackageReference Include="Microsoft.WindowsAppSDK"') {
$newVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="'+ $WinAppSDKVersion + '"'
$oldVersionString = 'PackageReference Include="Microsoft.WindowsAppSDK" Version="[-.0-9a-zA-Z]*"'
$content = $content -replace $oldVersionString, $newVersionString
- Set-Content -Path $_.FullName -Value $content
+ Write-FileWithEncoding -Path $_.FullName -Content $content -Encoding $file.encoding
Write-Host "Modified " $_.FullName
}
}
diff --git a/.pipelines/v2/ci-using-the-latest-winappsdk.yml b/.pipelines/v2/ci-using-the-latest-winappsdk.yml
index cc9f00f80d..3d628b13a9 100644
--- a/.pipelines/v2/ci-using-the-latest-winappsdk.yml
+++ b/.pipelines/v2/ci-using-the-latest-winappsdk.yml
@@ -33,7 +33,7 @@ parameters:
default: true
- name: winAppSDKVersionNumber
type: string
- default: 1.6
+ default: 1.7
- name: useExperimentalVersion
type: boolean
default: false
diff --git a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml
index 1fccd6de74..9c59312844 100644
--- a/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml
+++ b/.pipelines/v2/templates/steps-update-winappsdk-and-restore-nuget.yml
@@ -17,6 +17,7 @@ steps:
arguments: >
-winAppSdkVersionNumber ${{ parameters.versionNumber }}
-useExperimentalVersion $${{ parameters.useExperimentalVersion }}
+ -rootPath "$(build.sourcesdirectory)"
- script: echo $(WinAppSDKVersion)
displayName: 'Display WinAppSDK Version Found'
diff --git a/Directory.Packages.props b/Directory.Packages.props
index 0d8879746d..b3b404ea51 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -55,7 +55,7 @@
-->
-
+
diff --git a/NOTICE.md b/NOTICE.md
index edf8e75c51..d604be7d7f 100644
--- a/NOTICE.md
+++ b/NOTICE.md
@@ -1472,7 +1472,7 @@ SOFTWARE.
- Microsoft.Windows.CsWin32 0.2.46-beta
- Microsoft.Windows.CsWinRT 2.2.0
- Microsoft.Windows.SDK.BuildTools 10.0.22621.2428
-- Microsoft.WindowsAppSDK 1.6.250205002
+- Microsoft.WindowsAppSDK 1.7.250401001
- Microsoft.WindowsPackageManager.ComInterop 1.10.340
- Microsoft.Xaml.Behaviors.WinUI.Managed 2.0.9
- Microsoft.Xaml.Behaviors.Wpf 1.1.39
diff --git a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
index 86d258854f..f0b12187ad 100644
--- a/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
+++ b/src/modules/MeasureTool/MeasureToolCore/PowerToys.MeasureToolCore.vcxproj
@@ -1,6 +1,6 @@
-
+
@@ -141,7 +141,7 @@
-
+
@@ -153,7 +153,7 @@
-
-
+
+
\ No newline at end of file
diff --git a/src/modules/MeasureTool/MeasureToolCore/packages.config b/src/modules/MeasureTool/MeasureToolCore/packages.config
index 61ff4b9f07..996736442d 100644
--- a/src/modules/MeasureTool/MeasureToolCore/packages.config
+++ b/src/modules/MeasureTool/MeasureToolCore/packages.config
@@ -4,5 +4,5 @@
-
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
index 782ec68bf5..18f07f5f10 100644
--- a/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
+++ b/src/modules/cmdpal/ExtensionTemplate/TemplateCmdPalExtension/Directory.Packages.props
@@ -9,7 +9,7 @@
-
+
diff --git a/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj b/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj
index 039bf41817..69e3c64635 100644
--- a/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj
+++ b/src/modules/cmdpal/Microsoft.Terminal.UI/Microsoft.Terminal.UI.vcxproj
@@ -3,7 +3,7 @@
..\..\..\..\
- $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.6.250205002
+ $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250401001
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj
index 1090e58f25..0f87df5625 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/Microsoft.CommandPalette.Extensions.vcxproj
@@ -2,7 +2,7 @@
..\..\..\..\..\
- $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.6.250205002
+ $(PathToRoot)packages\Microsoft.WindowsAppSDK.1.7.250401001
$(PathToRoot)packages\Microsoft.Windows.CppWinRT.2.0.240111.5
$(PathToRoot)packages\Microsoft.Windows.SDK.BuildTools.10.0.22621.2428
$(PathToRoot)packages\Microsoft.Web.WebView2.1.0.2903.40
diff --git a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config
index 93d095a2b1..608661db25 100644
--- a/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config
+++ b/src/modules/cmdpal/extensionsdk/Microsoft.CommandPalette.Extensions/packages.config
@@ -3,5 +3,5 @@
-
+
\ No newline at end of file
diff --git a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
index 40bbb7a682..806da3904b 100644
--- a/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
+++ b/src/modules/powerrename/PowerRenameUILib/PowerRenameUI.vcxproj
@@ -1,6 +1,6 @@
-
+
@@ -207,7 +207,7 @@
-
+
@@ -221,8 +221,8 @@
-
-
+
+
diff --git a/src/modules/powerrename/PowerRenameUILib/packages.config b/src/modules/powerrename/PowerRenameUILib/packages.config
index c8aa0dc6df..77b75fad7e 100644
--- a/src/modules/powerrename/PowerRenameUILib/packages.config
+++ b/src/modules/powerrename/PowerRenameUILib/packages.config
@@ -6,5 +6,5 @@
-
+
\ No newline at end of file
From 25c29ade8ea820ab165f8ab92562dfb97182b5ae Mon Sep 17 00:00:00 2001
From: leileizhang
Date: Thu, 24 Apr 2025 12:00:03 +0800
Subject: [PATCH 16/31] upgrade the boost dependencies for Fuzzing Project
(#39057)
---
.../PowerRename.FuzzingTest.vcxproj | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
index 0d0838ad06..6dc9ddcdb2 100644
--- a/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
+++ b/src/modules/powerrename/PowerRename.FuzzingTest/PowerRename.FuzzingTest.vcxproj
@@ -92,8 +92,8 @@
-
-
+
+
@@ -101,7 +101,7 @@
-
-
+
+
\ No newline at end of file
From 100d560f9eb4beea5a82a96ce94774a60803875a Mon Sep 17 00:00:00 2001
From: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
Date: Thu, 24 Apr 2025 14:58:01 +0800
Subject: [PATCH 17/31] [CmdPal] Added fallback for time and date (#38918)
* Added fallback for time and date
Signed-off-by: Shawn Yuan
* only support week/now/time/year query
Signed-off-by: Shawn Yuan
* Add week option
Signed-off-by: Shawn Yuan
* Changed setting for time date fallback.
Signed-off-by: Shawn Yuan
* update globalization string
Signed-off-by: Shawn Yuan
* use week of year.
Signed-off-by: Shawn Yuan
* update
Signed-off-by: Shawn Yuan
---------
Signed-off-by: Shawn Yuan
---
.../FallbackTimeDateItem.cs | 104 ++++++++++++++++++
.../Helpers/AvailableResultsList.cs | 18 +--
.../Helpers/SettingsManager.cs | 25 ++---
.../Helpers/TimeDateCalculator.cs | 19 +---
.../Properties/Resources.Designer.cs | 63 ++++++-----
.../Properties/Resources.resx | 21 ++--
.../TimeDateCommandsProvider.cs | 3 +
7 files changed, 172 insertions(+), 81 deletions(-)
create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs
new file mode 100644
index 0000000000..267ab43989
--- /dev/null
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/FallbackTimeDateItem.cs
@@ -0,0 +1,104 @@
+// 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.Drawing;
+using System.Globalization;
+using System.Linq;
+using Microsoft.CmdPal.Ext.TimeDate.Helpers;
+using Microsoft.CommandPalette.Extensions;
+using Microsoft.CommandPalette.Extensions.Toolkit;
+
+namespace Microsoft.CmdPal.Ext.TimeDate;
+
+internal sealed partial class FallbackTimeDateItem : FallbackCommandItem
+{
+ private readonly HashSet _validOptions;
+ private SettingsManager _settingsManager;
+
+ public FallbackTimeDateItem(SettingsManager settings)
+ : base(new NoOpCommand(), Resources.Microsoft_plugin_timedate_fallback_display_title)
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ _settingsManager = settings;
+ _validOptions = new(StringComparer.OrdinalIgnoreCase)
+ {
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDate", CultureInfo.CurrentCulture),
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagDateNow", CultureInfo.CurrentCulture),
+
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTime", CultureInfo.CurrentCulture),
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagTimeNow", CultureInfo.CurrentCulture),
+
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormat", CultureInfo.CurrentCulture),
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagFormatNow", CultureInfo.CurrentCulture),
+
+ Resources.ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", CultureInfo.CurrentCulture),
+ };
+ }
+
+ public override void UpdateQuery(string query)
+ {
+ if (!_settingsManager.EnableFallbackItems || string.IsNullOrWhiteSpace(query) || !IsValidQuery(query))
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ Command = new NoOpCommand();
+ return;
+ }
+
+ var availableResults = AvailableResultsList.GetList(false, _settingsManager);
+ ListItem result = null;
+ var maxScore = 0;
+
+ foreach (var f in availableResults)
+ {
+ var score = f.Score(query, f.Label, f.AlternativeSearchTag);
+ if (score > maxScore)
+ {
+ maxScore = score;
+ result = f.ToListItem();
+ }
+ }
+
+ if (result != null)
+ {
+ Title = result.Title;
+ Subtitle = result.Subtitle;
+ Icon = result.Icon;
+ }
+ else
+ {
+ Title = string.Empty;
+ Subtitle = string.Empty;
+ Command = new NoOpCommand();
+ }
+ }
+
+ private bool IsValidQuery(string query)
+ {
+ if (_validOptions.Contains(query))
+ {
+ return true;
+ }
+
+ foreach (var option in _validOptions)
+ {
+ if (option == null)
+ {
+ continue;
+ }
+
+ var parts = option.Split(';', StringSplitOptions.TrimEntries | StringSplitOptions.RemoveEmptyEntries);
+
+ if (parts.Any(part => string.Equals(part, query, StringComparison.OrdinalIgnoreCase)))
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+}
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs
index bc6a3b972c..60ccaf38b5 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/AvailableResultsList.cs
@@ -33,6 +33,7 @@ internal static class AvailableResultsList
var dateTimeNowUtc = dateTimeNow.ToUniversalTime();
var firstWeekRule = firstWeekOfYear ?? TimeAndDateHelper.GetCalendarWeekRule(settings.FirstWeekOfYear);
var firstDayOfTheWeek = firstDayOfWeek ?? TimeAndDateHelper.GetFirstDayOfWeek(settings.FirstDayOfWeek);
+ var weekOfYear = calendar.GetWeekOfYear(dateTimeNow, firstWeekRule, firstDayOfTheWeek);
results.AddRange(new[]
{
@@ -59,14 +60,20 @@ internal static class AvailableResultsList
AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagFormat"),
IconType = ResultIconType.DateTime,
},
+ new AvailableResult()
+ {
+ Value = weekOfYear.ToString(CultureInfo.CurrentCulture),
+ Label = Resources.Microsoft_plugin_timedate_WeekOfYear,
+ AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"),
+ IconType = ResultIconType.Date,
+ },
});
- if (isKeywordSearch || !settings.OnlyDateTimeNowGlobal)
+ if (isKeywordSearch)
{
// We use long instead of int for unix time stamp because int is too small after 03:14:07 UTC 2038-01-19
var unixTimestamp = ((DateTimeOffset)dateTimeNowUtc).ToUnixTimeSeconds();
var unixTimestampMilliseconds = ((DateTimeOffset)dateTimeNowUtc).ToUnixTimeMilliseconds();
- var weekOfYear = calendar.GetWeekOfYear(dateTimeNow, firstWeekRule, firstDayOfTheWeek);
var era = DateTimeFormatInfo.CurrentInfo.GetEraName(calendar.GetEra(dateTimeNow));
var eraShort = DateTimeFormatInfo.CurrentInfo.GetAbbreviatedEraName(calendar.GetEra(dateTimeNow));
@@ -251,13 +258,6 @@ internal static class AvailableResultsList
IconType = ResultIconType.Date,
},
new AvailableResult()
- {
- Value = weekOfYear.ToString(CultureInfo.CurrentCulture),
- Label = Resources.Microsoft_plugin_timedate_WeekOfYear,
- AlternativeSearchTag = ResultHelper.SelectStringFromResources(isSystemDateTime, "Microsoft_plugin_timedate_SearchTagDate"),
- IconType = ResultIconType.Date,
- },
- new AvailableResult()
{
Value = DateTimeFormatInfo.CurrentInfo.GetMonthName(dateTimeNow.Month),
Label = Resources.Microsoft_plugin_timedate_Month,
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs
index 5b30a4816f..7b351fe3b8 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/SettingsManager.cs
@@ -75,11 +75,11 @@ public class SettingsManager : JsonSettingsManager
Resources.Microsoft_plugin_timedate_SettingFirstDayOfWeek,
_firstDayOfWeekChoices);
- private readonly ToggleSetting _onlyDateTimeNowGlobal = new(
- Namespaced(nameof(OnlyDateTimeNowGlobal)),
- Resources.Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal,
- Resources.Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description,
- true); // TODO -- double check default value
+ private readonly ToggleSetting _enableFallbackItems = new(
+ Namespaced(nameof(EnableFallbackItems)),
+ Resources.Microsoft_plugin_timedate_SettingEnableFallbackItems,
+ Resources.Microsoft_plugin_timedate_SettingEnableFallbackItems_Description,
+ true);
private readonly ToggleSetting _timeWithSeconds = new(
Namespaced(nameof(TimeWithSecond)),
@@ -93,12 +93,6 @@ public class SettingsManager : JsonSettingsManager
Resources.Microsoft_plugin_timedate_SettingDateWithWeekday_Description,
false); // TODO -- double check default value
- private readonly ToggleSetting _hideNumberMessageOnGlobalQuery = new(
- Namespaced(nameof(HideNumberMessageOnGlobalQuery)),
- Resources.Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery,
- Resources.Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery,
- true); // TODO -- double check default value
-
private readonly TextSetting _customFormats = new(
Namespaced(nameof(CustomFormats)),
Resources.Microsoft_plugin_timedate_Setting_CustomFormats,
@@ -145,14 +139,12 @@ public class SettingsManager : JsonSettingsManager
}
}
- public bool OnlyDateTimeNowGlobal => _onlyDateTimeNowGlobal.Value;
+ public bool EnableFallbackItems => _enableFallbackItems.Value;
public bool TimeWithSecond => _timeWithSeconds.Value;
public bool DateWithWeekday => _dateWithWeekday.Value;
- public bool HideNumberMessageOnGlobalQuery => _hideNumberMessageOnGlobalQuery.Value;
-
public List CustomFormats => _customFormats.Value.Split(TEXTBOXNEWLINE).ToList();
internal static string SettingsJsonPath()
@@ -168,10 +160,7 @@ public class SettingsManager : JsonSettingsManager
{
FilePath = SettingsJsonPath();
- /* The following two settings make no sense with current CmdPal behavior.
- Settings.Add(_onlyDateTimeNowGlobal);
- Settings.Add(_hideNumberMessageOnGlobalQuery); */
-
+ Settings.Add(_enableFallbackItems);
Settings.Add(_timeWithSeconds);
Settings.Add(_dateWithWeekday);
Settings.Add(_firstWeekOfYear);
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs
index eb4eed18cd..38f417ad5b 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Helpers/TimeDateCalculator.cs
@@ -40,7 +40,7 @@ public sealed partial class TimeDateCalculator
var lastInputParsingErrorMsg = string.Empty;
// Switch search type
- if (isEmptySearchInput || (!isKeywordSearch && settings.OnlyDateTimeNowGlobal))
+ if (isEmptySearchInput || (!isKeywordSearch))
{
// Return all results for system time/date on empty keyword search
// or only time, date and now results for system time on global queries if the corresponding setting is enabled
@@ -91,23 +91,6 @@ public sealed partial class TimeDateCalculator
}
}
- /*htcfreek:Code obsolete with current CmdPal behavior.
- // If search term is only a number that can't be parsed return an error message
- if (!isEmptySearchInput && results.Count == 0 && Regex.IsMatch(query, @"\w+\d+.*$") && !query.Any(char.IsWhiteSpace) && (TimeAndDateHelper.IsSpecialInputParsing(query) || !Regex.IsMatch(query, @"\d+[\.:/]\d+")))
- {
- // Without plugin key word show only if message is not hidden by setting
- if (!settings.HideNumberMessageOnGlobalQuery)
- {
- var er = ResultHelper.CreateInvalidInputErrorResult();
- if (!string.IsNullOrEmpty(lastInputParsingErrorMsg))
- {
- er.Details = new Details() { Body = lastInputParsingErrorMsg };
- }
-
- results.Add(er);
- }
- } */
-
if (results.Count == 0)
{
var er = ResultHelper.CreateInvalidInputErrorResult();
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs
index d62d76eefd..a21759d10a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.Designer.cs
@@ -213,6 +213,15 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
+ ///
+ /// Looks up a localized string similar to Open Time Data Command.
+ ///
+ public static string Microsoft_plugin_timedate_fallback_display_title {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_fallback_display_title", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Date and time in filename-compatible format.
///
@@ -609,6 +618,15 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
+ ///
+ /// Looks up a localized string similar to Current Week; Calendar week; Week of the year; Week.
+ ///
+ public static string Microsoft_plugin_timedate_SearchTagWeek {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_SearchTagWeek", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Second.
///
@@ -663,6 +681,24 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
+ ///
+ /// Looks up a localized string similar to Enable fallback items for TimeDate (week, year, now, time, date).
+ ///
+ public static string Microsoft_plugin_timedate_SettingEnableFallbackItems {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_SettingEnableFallbackItems", resourceCulture);
+ }
+ }
+
+ ///
+ /// Looks up a localized string similar to Show time and date results when typing keywords like "week", "year", "now", "time", or "date".
+ ///
+ public static string Microsoft_plugin_timedate_SettingEnableFallbackItems_Description {
+ get {
+ return ResourceManager.GetString("Microsoft_plugin_timedate_SettingEnableFallbackItems_Description", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to First day of the week.
///
@@ -780,33 +816,6 @@ namespace Microsoft.CmdPal.Ext.TimeDate {
}
}
- ///
- /// Looks up a localized string similar to Hide 'Invalid number input' error message on global queries.
- ///
- public static string Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery {
- get {
- return ResourceManager.GetString("Microsoft_plugin_timedate_SettingHideNumberMessageOnGlobalQuery", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Show only 'Time', 'Date' and 'Now' result for system time on global queries.
- ///
- public static string Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal {
- get {
- return ResourceManager.GetString("Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal", resourceCulture);
- }
- }
-
- ///
- /// Looks up a localized string similar to Regardless of this setting, for global queries the first word of the query has to be a complete match..
- ///
- public static string Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description {
- get {
- return ResourceManager.GetString("Microsoft_plugin_timedate_SettingOnlyDateTimeNowGlobal_Description", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Show time with seconds.
///
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx
index f1a36e2a90..35862592ca 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Properties/Resources.resx
@@ -265,15 +265,6 @@
This setting applies to the 'Date' and 'Now' result.
-
- Hide 'Invalid number input' error message on global queries
-
-
- Show only 'Time', 'Date' and 'Now' result for system time on global queries
-
-
- Regardless of this setting, for global queries the first word of the query has to be a complete match.
-
Show time with seconds
@@ -433,4 +424,16 @@
Days in month
+
+ Open Time Data Command
+
+
+ Current Week; Calendar week; Week of the year; Week
+
+
+ Enable fallback items for TimeDate (week, year, now, time, date)
+
+
+ Show time and date results when typing keywords like "week", "year", "now", "time", or "date"
+
\ No newline at end of file
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs
index 05597c4553..d29356fa77 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/TimeDateCommandsProvider.cs
@@ -18,6 +18,7 @@ public partial class TimeDateCommandsProvider : CommandProvider
private static readonly SettingsManager _settingsManager = new();
private static readonly CompositeFormat MicrosoftPluginTimedatePluginDescription = System.Text.CompositeFormat.Parse(Resources.Microsoft_plugin_timedate_plugin_description);
private static readonly TimeDateExtensionPage _timeDateExtensionPage = new(_settingsManager);
+ private readonly FallbackTimeDateItem _fallbackTimeDateItem = new(_settingsManager);
public TimeDateCommandsProvider()
{
@@ -45,4 +46,6 @@ public partial class TimeDateCommandsProvider : CommandProvider
}
public override ICommandItem[] TopLevelCommands() => [_command];
+
+ public override IFallbackCommandItem[] FallbackCommands() => [_fallbackTimeDateItem];
}
From 5691c5754bfbde78e47eb777601ffd1ab08e5f27 Mon Sep 17 00:00:00 2001
From: Yu Leng <42196638+moooyo@users.noreply.github.com>
Date: Thu, 24 Apr 2025 16:07:10 +0800
Subject: [PATCH 18/31] [cmdpal] Ref to AotCompatibility in some cmdpal
project. (#39061)
* Ref to AotCompatibility
* Typo issue
---------
Co-authored-by: Yu Leng (from Dev Box)
---
.../Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj | 1 +
.../Microsoft.CmdPal.Ext.Registry.csproj | 2 ++
.../Microsoft.CmdPal.Ext.Shell.csproj | 2 ++
.../Microsoft.CmdPal.Ext.TimeDate.csproj | 3 ++-
.../Microsoft.CmdPal.Ext.WindowWalker.csproj | 2 ++
.../Microsoft.CmdPal.Ext.WindowsServices.csproj | 2 ++
.../Microsoft.CmdPal.Ext.WindowsTerminal.csproj | 2 ++
7 files changed, 13 insertions(+), 1 deletion(-)
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj
index 85f06768ce..59f5ccbc70 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Calc/Microsoft.CmdPal.Ext.Calc.csproj
@@ -1,5 +1,6 @@
+
Microsoft.CmdPal.Ext.Calc
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj
index 2ff67f859f..c7633d7356 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Registry/Microsoft.CmdPal.Ext.Registry.csproj
@@ -1,5 +1,7 @@
+
+
Microsoft.CmdPal.Ext.Registry
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj
index c95f2a93b2..934e6d264a 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Shell/Microsoft.CmdPal.Ext.Shell.csproj
@@ -1,5 +1,7 @@
+
+
enable
Microsoft.CmdPal.Ext.Shell
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj
index 733ed8634e..34301712cf 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.TimeDate/Microsoft.CmdPal.Ext.TimeDate.csproj
@@ -1,6 +1,7 @@
-
+
+
Microsoft.CmdPal.Ext.TimeDate
false
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj
index 2ed59ad6a3..e346e824c8 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowWalker/Microsoft.CmdPal.Ext.WindowWalker.csproj
@@ -1,5 +1,7 @@
+
+
enable
Microsoft.CmdPal.Ext.WindowWalker
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj
index 2b7b9345ec..9038a2d671 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsServices/Microsoft.CmdPal.Ext.WindowsServices.csproj
@@ -1,5 +1,7 @@
+
+
Microsoft.CmdPal.Ext.WindowsServices
$(SolutionDir)$(Platform)\$(Configuration)\WinUI3Apps\CmdPal
diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj
index 7ea6f17148..1c09c35c5d 100644
--- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj
+++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsTerminal/Microsoft.CmdPal.Ext.WindowsTerminal.csproj
@@ -1,6 +1,8 @@
+
+
Microsoft.CmdPal.Ext.WindowsTerminal
From 195ff24a854eca63b5deaafe6c172bb9ed8a9743 Mon Sep 17 00:00:00 2001
From: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com>
Date: Thu, 24 Apr 2025 12:18:38 +0200
Subject: [PATCH 19/31] [Settings] Fix null CmdPal HotKey crash (#39052)
fixed null cmdpal hotkey crash when settings.json not exists or not define hotkey
---
.../Settings.UI.Library/CmdPalProperties.cs | 10 ++--------
1 file changed, 2 insertions(+), 8 deletions(-)
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;
}
}
}
From f63fcfd91cd67e8973de9ffc7192a8feeadfdc97 Mon Sep 17 00:00:00 2001
From: Mike Griese
Date: Thu, 24 Apr 2025 13:32:07 -0500
Subject: [PATCH 20/31] Add support for filterable, nested context menus
(#38776)
_targets #38573_
At first I just wanted to add support for nested context menus.
But then I also had to add a search box, so the focus wouldn't get weird.
End result:

This gets rid of the need to have the search box and the command bar both track item keybindings - now it's just in the command bar.
Closes #38299
Closes #38442
---
.../CommandBarViewModel.cs | 97 +++++++---
.../CommandItemViewModel.cs | 46 ++---
.../ContextMenuStackViewModel.cs | 82 ++++++++
.../ListViewModel.cs | 2 -
...sage.cs => TryCommandKeybindingMessage.cs} | 7 +-
.../Messages/UpdateCommandBarMessage.cs | 37 +++-
.../Controls/CommandBar.xaml | 57 ++++--
.../Controls/CommandBar.xaml.cs | 182 +++++++++++++++++-
.../Controls/SearchBar.xaml.cs | 30 +--
.../Pages/ShellPage.xaml.cs | 2 -
.../Strings/en-us/Resources.resw | 3 +
.../SamplePagesExtension/EvilSamplesPage.cs | 132 ++++++++++++-
.../Pages/SampleListPage.cs | 72 +++----
.../Pages/ToastCommand.cs | 23 +++
.../ListItem.cs | 5 +
15 files changed, 607 insertions(+), 170 deletions(-)
create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ContextMenuStackViewModel.cs
rename src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/Messages/{UpdateItemKeybindingsMessage.cs => TryCommandKeybindingMessage.cs} (59%)
create mode 100644 src/modules/cmdpal/ext/SamplePagesExtension/Pages/ToastCommand.cs
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/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/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/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/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()
+ {
+ }
}
From fc804a81563a72d7782a20105765d6b7554cbd77 Mon Sep 17 00:00:00 2001
From: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date: Fri, 25 Apr 2025 09:57:42 +0800
Subject: [PATCH 21/31] [Tool] Script to build an installer locally (#39017)
* add script to build a installer
* minor fix
* fix search path for msix file
* fix sign
* fix sign
* fix spelling
* Fix powershell5 can't recognize emoji
* ensure-wix
* bring cmdpal available during local build
* remove early quit
* fix marco
* add logger
* doc
* add a note
* self review
* fix macro def
* add functionality to export cert so that other machine can install it.
* spelling
---
.github/actions/spell-check/expect.txt | 4 +
installer/PowerToysSetup/Product.wxs | 5 +-
src/common/utils/package.h | 1 +
.../CmdPalModuleInterface.vcxproj | 11 +-
.../cmdpal/CmdPalModuleInterface/dllmain.cpp | 19 ++-
tools/build/build-installer.ps1 | 122 ++++++++++++++
tools/build/cert-management.ps1 | 159 ++++++++++++++++++
tools/build/cert-sign-package.ps1 | 29 ++++
tools/build/ensure-wix.ps1 | 71 ++++++++
9 files changed, 411 insertions(+), 10 deletions(-)
create mode 100644 tools/build/build-installer.ps1
create mode 100644 tools/build/cert-management.ps1
create mode 100644 tools/build/cert-sign-package.ps1
create mode 100644 tools/build/ensure-wix.ps1
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index daa6449a66..52f203a9de 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -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
diff --git a/installer/PowerToysSetup/Product.wxs b/installer/PowerToysSetup/Product.wxs
index 37256bdd68..f15b8a4714 100644
--- a/installer/PowerToysSetup/Product.wxs
+++ b/installer/PowerToysSetup/Product.wxs
@@ -79,10 +79,7 @@
-
-
-
-
+
diff --git a/src/common/utils/package.h b/src/common/utils/package.h
index 60bde7ea53..138f3b8e5b 100644
--- a/src/common/utils/package.h
+++ b/src/common/utils/package.h
@@ -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);
diff --git a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
index 4395e340fa..5bd12f316e 100644
--- a/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
+++ b/src/modules/cmdpal/CmdPalModuleInterface/CmdPalModuleInterface.vcxproj
@@ -2,6 +2,7 @@
+
17.0
Win32Proj
@@ -49,13 +50,21 @@
- EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;%(PreprocessorDefinitions)
+
+ EXAMPLEPOWERTOY_EXPORTS;_WINDOWS;_USRDLL;
+ %(PreprocessorDefinitions);
+ $(CommandPaletteBranding)
+
+
+ IS_DEV_BRANDING;%(PreprocessorDefinitions)
+
..\..\..\common\inc;..\..\..\common\Telemetry;..\..\;..\..\..\;%(AdditionalIncludeDirectories)
$(OutDir)$(TargetName)$(TargetExt)
+
diff --git a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
index 134249f049..be3eb6a3b7 100644
--- a/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
+++ b/src/modules/cmdpal/CmdPalModuleInterface/dllmain.cpp
@@ -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()
diff --git a/tools/build/build-installer.ps1 b/tools/build/build-installer.ps1
new file mode 100644
index 0000000000..28e6939760
--- /dev/null
+++ b/tools/build/build-installer.ps1
@@ -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'
\ No newline at end of file
diff --git a/tools/build/cert-management.ps1 b/tools/build/cert-management.ps1
new file mode 100644
index 0000000000..a085a5ca54
--- /dev/null
+++ b/tools/build/cert-management.ps1
@@ -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
diff --git a/tools/build/cert-sign-package.ps1 b/tools/build/cert-sign-package.ps1
new file mode 100644
index 0000000000..8bb57762a5
--- /dev/null
+++ b/tools/build/cert-sign-package.ps1
@@ -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"
+}
\ No newline at end of file
diff --git a/tools/build/ensure-wix.ps1 b/tools/build/ensure-wix.ps1
new file mode 100644
index 0000000000..988d382f07
--- /dev/null
+++ b/tools/build/ensure-wix.ps1
@@ -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"
From 06b56a10bd51fd3c7ca8acb178e7778ac1eb51a6 Mon Sep 17 00:00:00 2001
From: cryolithic
Date: Thu, 24 Apr 2025 19:45:11 -0700
Subject: [PATCH 22/31] 37405 Advanced Paste: Image To Text doesn't work with
English (Canada) (#37806)
* [AdvancedPaste] [Fix Bug] Create ocrEngine from user profile language
GetOCRLanguage may fail based on language tag not matching (en-CA does not match en-GB or en-US), however user profile language may be valid.
* Update exception message.
Signed-off-by: Shawn Yuan
* update
Signed-off-by: Shawn Yuan
---------
Signed-off-by: Shawn Yuan
Co-authored-by: Shawn Yuan
---
.../AdvancedPaste/Helpers/OcrHelpers.cs | 13 +++++++++++--
1 file changed, 11 insertions(+), 2 deletions(-)
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);
From 26fe36ab8d9e99bd201f80105d4499b24c35a09f Mon Sep 17 00:00:00 2001
From: Lemonyte <49930425+lemonyte@users.noreply.github.com>
Date: Thu, 24 Apr 2025 19:48:19 -0700
Subject: [PATCH 23/31] Color Picker: add Oklab and Oklch color formats
(#38052)
* Resolve Resources.resw conflict
* Update CIE LCh chroma practical upper bound according to CSS spec
* Add review suggestions
* Add WIP tests (lch and oklch do not pass yet)
* Deduplicate Lab to LCh converter method
* Update expect.txt
* Fix liberty test color
* Reimplement oklab with better precision
* Remove CIE LCh
* Add tooltip for color param descriptions
* Update spell-check expect.txt with new words
* Remove 'cielch' and 'lch' from expect.txt
---------
Co-authored-by: Gordon Lam (SH)
Co-authored-by: Clint Rutkas
Co-authored-by: Shawn Yuan <128874481+shuaiyuanxx@users.noreply.github.com>
---
.github/actions/spell-check/expect.txt | 10 +-
src/common/ManagedCommon/ColorFormatHelper.cs | 131 ++++++++++++++++--
.../Helpers/ColorRepresentationHelper.cs | 34 +++++
.../ViewModels/ColorEditorViewModel.cs | 16 ++-
.../Helpers/ColorConverterTest.cs | 120 ++++++++++++++--
.../Helpers/ColorRepresentationHelperTest.cs | 4 +-
.../ColorPickerProperties.cs | 4 +-
.../Enumerations/ColorRepresentationType.cs | 15 ++
.../Controls/ColorFormatEditor.xaml | 3 +-
.../Controls/ColorFormatEditor.xaml.cs | 11 +-
.../Settings.UI/Strings/en-us/Resources.resw | 31 ++++-
11 files changed, 348 insertions(+), 31 deletions(-)
diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt
index 52f203a9de..43447793a0 100644
--- a/.github/actions/spell-check/expect.txt
+++ b/.github/actions/spell-check/expect.txt
@@ -1982,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/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/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/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
From 7dc2a05c45e6be92c02f284870c4b2a8159427ae Mon Sep 17 00:00:00 2001
From: Corey Hayward <72159232+CoreyHayward@users.noreply.github.com>
Date: Fri, 25 Apr 2025 05:27:54 +0100
Subject: [PATCH 24/31] [PTRun] Allow preventing usage based ordering results
(#37491)
* Allow preventing selected result data retrieval
* Updated implementation to calculate sort order on result and update property name to better reflect purpose
* Update Result.cs sort order method name
Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
* Align with the name GetSortOrderScore
---------
Co-authored-by: Heiko <61519853+htcfreek@users.noreply.github.com>
Co-authored-by: Gordon Lam (SH)
---
.../PowerLauncher/ViewModel/ResultsViewModel.cs | 4 ++--
src/modules/launcher/Wox.Plugin/Result.cs | 15 +++++++++++++++
2 files changed, 17 insertions(+), 2 deletions(-)
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);
+ }
}
}
From 9a6c64f9c0c24dbffb560dfd848c00f18812987d Mon Sep 17 00:00:00 2001
From: Yu Leng <42196638+moooyo@users.noreply.github.com>
Date: Fri, 25 Apr 2025 16:22:50 +0800
Subject: [PATCH 25/31] [cmdpal] [AOT] make
Clipboard/System/WebSearch/WindowsSettings ext become AOT compatible.
(#39080)
Co-authored-by: Yu Leng (from Dev Box)
---
.../Helpers/NativeMethods.cs | 2 +-
...crosoft.CmdPal.Ext.ClipboardHistory.csproj | 1 +
.../Helpers/NetworkConnectionProperties.cs | 10 ++++-----
.../Microsoft.CmdPal.Ext.System.csproj | 1 +
.../Helpers/HistoryItem.cs | 2 +-
.../Helpers/SettingsManager.cs | 10 ++++-----
.../WebSearchJsonSerializationContext.cs | 20 +++++++++++++++++
.../Microsoft.CmdPal.Ext.WebSearch.csproj | 1 +
.../Helpers/JsonSettingsListHelper.cs | 12 +++++-----
.../JsonSerializationContext.cs | 22 +++++++++++++++++++
...icrosoft.CmdPal.Ext.WindowsSettings.csproj | 1 +
11 files changed, 64 insertions(+), 18 deletions(-)
create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Helpers/WebSearchJsonSerializationContext.cs
create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WindowsSettings/JsonSerializationContext.cs
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
From 30df5e0df298d5fe7f3699c517cc37a6bb7cfa93 Mon Sep 17 00:00:00 2001
From: Gordon Lam <73506701+yeelam-gordon@users.noreply.github.com>
Date: Sat, 26 Apr 2025 17:59:44 -0700
Subject: [PATCH 26/31] Update the ADO path for tsa.json (#39079)
The previous ADO area path for Powertoys was gone, we need to update to new one.
---
.pipelines/tsa.json | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
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"
}
From ba230eca070322c2010f5e0a304229691882bc7b Mon Sep 17 00:00:00 2001
From: Clint Rutkas
Date: Sun, 27 Apr 2025 05:44:47 +0000
Subject: [PATCH 27/31] Start progress on AoT. (#39051)
* starting AoT flag push
* Few more
* bookmarks
* Really? The VM project compiles?
* Disable publish AOT before we really testing it.
---------
Co-authored-by: Mike Griese
Co-authored-by: Yu Leng (from Dev Box)
---
.../AppStateModel.cs | 33 +++++++-------
.../Commands/CreatedExtensionForm.cs | 15 ++++---
.../Commands/NewExtensionForm.cs | 7 ++-
.../ContentFormViewModel.cs | 11 ++---
.../Microsoft.CmdPal.UI.ViewModels.csproj | 13 ++++++
.../Models/ExtensionService.cs | 2 +-
.../Models/ExtensionWrapper.cs | 42 ++++++++++-------
.../NativeMethods.json | 4 ++
.../PageViewModel.cs | 2 +-
.../RecentCommandsManager.cs | 2 +-
.../SettingsModel.cs | 45 ++++++++++++-------
.../ShellViewModel.cs | 7 ++-
.../AddBookmarkForm.cs | 4 +-
.../BookmarkSerializationContext.cs | 20 +++++++++
.../Bookmarks.cs | 4 +-
.../Microsoft.CmdPal.Ext.Bookmarks.csproj | 7 +--
.../JsonSerializationContext.cs | 1 -
17 files changed, 144 insertions(+), 75 deletions(-)
create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/NativeMethods.json
create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Bookmark/BookmarkSerializationContext.cs
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/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/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..9a1cda196b 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.
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/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/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
{
}
From 7efbd2f01364128efaeab334f0f7b7ba0d824963 Mon Sep 17 00:00:00 2001
From: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date: Sun, 27 Apr 2025 13:47:56 +0800
Subject: [PATCH 28/31] [CmdPal] Launch cmd pal with retry (#39039)
* Add Retry when enable
* Add correct for the checking logic
* Retry in another thread (#39042)
* launch thread
* dev
* fix a thread safety
* improve
* improve
* make code clear
* Fix comment
* fix comment
* improve
* self review
* fix & log
* silent fail if not reach 10 times
* fix a ci build flag error
* fix a macro
* some simple improve
---------
Co-authored-by: Gordon Lam (SH)
---
.../CmdPalModuleInterface.vcxproj | 3 +-
.../cmdpal/CmdPalModuleInterface/dllmain.cpp | 124 +++++++++++++-----
2 files changed, 93 insertions(+), 34 deletions(-)
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
From 49e5bbb5f070cdab49fb334bb5b18236f0e234a3 Mon Sep 17 00:00:00 2001
From: Yu Leng <42196638+moooyo@users.noreply.github.com>
Date: Sun, 27 Apr 2025 17:18:01 +0800
Subject: [PATCH 29/31] [cmdpal][aot] Remove some unused file in CmdPal.Common
and mark it as AOT compatible. (#39110)
* Remove unused com interface
* Remove unused file
* Remove unused file
* Remove unused file
---------
Co-authored-by: Yu Leng (from Dev Box)
---
.../Contracts/IFileService.cs | 14 --
.../Contracts/ILocalSettingsService.cs | 16 ---
.../Extensions/ApplicationExtensions.cs | 40 ------
.../Extensions/IHostExtensions.cs | 40 ------
.../Microsoft.CmdPal.Common/Helpers/Json.cs | 36 ------
.../Helpers/NativeEventWaiter.cs | 2 +-
.../Helpers/RuntimeHelper.cs | 2 +-
.../Messages/HideWindowMessage.cs | 2 +-
.../Microsoft.CmdPal.Common.csproj | 1 +
.../Models/LocalSettingsOptions.cs | 18 ---
.../Microsoft.CmdPal.Common/NativeMethods.txt | 9 --
.../Services/FileService.cs | 48 -------
.../Microsoft.CmdPal.Common/Services/IApp.cs | 18 ---
.../Services/LocalSettingsService.cs | 120 ------------------
14 files changed, 4 insertions(+), 362 deletions(-)
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/IFileService.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Contracts/ILocalSettingsService.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/ApplicationExtensions.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Extensions/IHostExtensions.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Helpers/Json.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Models/LocalSettingsOptions.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Services/FileService.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Services/IApp.cs
delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.Common/Services/LocalSettingsService.cs
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));
- }
- }
-}
From ad974bd6790597645b130df4e9013d5b0ddd2e36 Mon Sep 17 00:00:00 2001
From: Mike Griese
Date: Sun, 27 Apr 2025 15:29:15 -0500
Subject: [PATCH 30/31] Wait to update SearchText until we've actually updated
SearchText (#39093)
Closes #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.
---
.../PageViewModel.cs | 15 ++++++++++++++-
1 file changed, 14 insertions(+), 1 deletion(-)
diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
index 9a1cda196b..126efa83ca 100644
--- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
+++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs
@@ -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)
From 9cb99be4e9548c48bad517ce7c075b6de3c1e11e Mon Sep 17 00:00:00 2001
From: Kai Tao <69313318+vanzue@users.noreply.github.com>
Date: Tue, 29 Apr 2025 18:31:21 +0800
Subject: [PATCH 31/31] [Tool] Delete export pfx function to remove use of hard
coded password (#39144)
don't need export pfx functionality
---
tools/build/cert-management.ps1 | 6 +-----
1 file changed, 1 insertion(+), 5 deletions(-)
diff --git a/tools/build/cert-management.ps1 b/tools/build/cert-management.ps1
index a085a5ca54..ed7031c1e9 100644
--- a/tools/build/cert-management.ps1
+++ b/tools/build/cert-management.ps1
@@ -152,8 +152,4 @@ function Export-CertificateFiles {
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
+}
\ No newline at end of file