From e041556395e89f293b183065a127e4b149450ae2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Fri, 11 Jul 2025 18:08:17 +0200 Subject: [PATCH 01/12] CmdPal: Handle Alt+Left Arrow even when search bar contains text (#40516) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, the `KeyDown` handler for `Alt+Left Arrow` was not invoked if the search bar contained text. By moving the logic to the `PreviewKeyDown` handler, we ensure the shortcut is handled regardless of the search bar’s content. The handler was added to the `ShellPage`'s `PreviewKeyDown` handler, rather than the one on `SearchBar`. This will allow the shortcut to be available throughout the entire window and will ensure it works from within the page content, e.g. from forms ## Summary of the Pull Request ## PR Checklist - [x] **Closes:** #40515 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs | 4 ---- .../cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs | 10 ++++++++++ 2 files changed, 10 insertions(+), 4 deletions(-) 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 07fc055961..cc8b600526 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/SearchBar.xaml.cs @@ -162,10 +162,6 @@ public sealed partial class SearchBar : UserControl, CurrentPageViewModel.Filter = FilterBox.Text; } } - else if (e.Key == VirtualKey.Left && altPressed) - { - WeakReferenceMessenger.Default.Send(new()); - } if (!e.Handled) { 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 c4e3843568..1844a77dc0 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml.cs @@ -20,6 +20,7 @@ using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Input; using Microsoft.UI.Xaml.Media.Animation; using DispatcherQueue = Microsoft.UI.Dispatching.DispatcherQueue; +using VirtualKey = Windows.System.VirtualKey; namespace Microsoft.CmdPal.UI.Pages; @@ -81,6 +82,7 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, WeakReferenceMessenger.Default.Register(this); WeakReferenceMessenger.Default.Register(this); + AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true); AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true); RootFrame.Navigate(typeof(LoadingPage), ViewModel); @@ -447,6 +449,14 @@ public sealed partial class ShellPage : Microsoft.UI.Xaml.Controls.Page, } } + private void ShellPage_OnPreviewKeyDown(object sender, KeyRoutedEventArgs e) + { + if (e.Key == VirtualKey.Left && e.KeyStatus.IsMenuKeyDown) + { + WeakReferenceMessenger.Default.Send(new()); + } + } + private void ShellPage_OnPointerPressed(object sender, PointerRoutedEventArgs e) { try From 22f565ca496db1c2c2120e6fce566655ca1801d4 Mon Sep 17 00:00:00 2001 From: octastylos-pseudodipteros <128853928+octastylos-pseudodipteros@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:08:42 +0200 Subject: [PATCH 02/12] [QuickAccent] Add Vietnamese (#40164) ## Summary of the Pull Request Adds support for Vietnamese in QuickAccent ## PR Checklist - [x] **Closes:** #36491 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** no need - [x] **New binaries:** no need - [x] **Documentation updated:** no need ## Detailed Description of the Pull Request / Additional comments Vietnamese character set based of on [Wikipedia](https://en.wikipedia.org/wiki/Vietnamese_alphabet) ## Validation Steps Performed Manually tested --- .../poweraccent/PowerAccent.Core/Languages.cs | 19 +++++++++++++++++++ .../Settings.UI/Strings/en-us/Resources.resw | 3 +++ .../ViewModels/PowerAccentViewModel.cs | 1 + 3 files changed, 23 insertions(+) diff --git a/src/modules/poweraccent/PowerAccent.Core/Languages.cs b/src/modules/poweraccent/PowerAccent.Core/Languages.cs index 567c0ecc2d..207811aaec 100644 --- a/src/modules/poweraccent/PowerAccent.Core/Languages.cs +++ b/src/modules/poweraccent/PowerAccent.Core/Languages.cs @@ -51,6 +51,7 @@ namespace PowerAccent.Core SR_CYRL, SV, TK, + VI, } internal sealed class Languages @@ -113,6 +114,7 @@ namespace PowerAccent.Core Language.SR_CYRL => GetDefaultLetterKeySRCyrillic(letter), // Serbian Cyrillic Language.SV => GetDefaultLetterKeySV(letter), // Swedish Language.TK => GetDefaultLetterKeyTK(letter), // Turkish + Language.VI => GetDefaultLetterKeyVI(letter), // Vietnamese _ => throw new ArgumentException("The language {0} is not known in this context", lang.ToString()), }); } @@ -168,6 +170,7 @@ namespace PowerAccent.Core .Union(GetDefaultLetterKeySRCyrillic(letter)) .Union(GetDefaultLetterKeySV(letter)) .Union(GetDefaultLetterKeyTK(letter)) + .Union(GetDefaultLetterKeyVI(letter)) .Union(GetDefaultLetterKeySPECIAL(letter)) .ToArray(); @@ -890,6 +893,22 @@ namespace PowerAccent.Core }; } + // Vietnamese + private static string[] GetDefaultLetterKeyVI(LetterKey letter) + { + return letter switch + { + LetterKey.VK_A => new[] { "à", "ả", "ã", "á", "ạ", "ă", "ằ", "ẳ", "ẵ", "ắ", "ặ", "â", "ầ", "ẩ", "ẫ", "ấ", "ậ" }, + LetterKey.VK_D => new[] { "đ" }, + LetterKey.VK_E => new[] { "è", "ẻ", "ẽ", "é", "ẹ", "ê", "ề", "ể", "ễ", "ế", "ệ" }, + LetterKey.VK_I => new[] { "ì", "ỉ", "ĩ", "í", "ị" }, + LetterKey.VK_O => new[] { "ò", "ỏ", "õ", "ó", "ọ", "ô", "ồ", "ổ", "ỗ", "ố", "ộ", "ơ", "ờ", "ở", "ỡ", "ớ", "ợ" }, + LetterKey.VK_U => new[] { "ù", "ủ", "ũ", "ú", "ụ", "ư", "ừ", "ử", "ữ", "ứ", "ự" }, + LetterKey.VK_Y => new[] { "ỳ", "ỷ", "ỹ", "ý", "ỵ" }, + _ => Array.Empty(), + }; + } + // IPA (International Phonetic Alphabet) private static string[] GetDefaultLetterKeyIPA(LetterKey letter) { 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 f9b971816c..368984e6c0 100644 --- a/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw +++ b/src/settings-ui/Settings.UI/Strings/en-us/Resources.resw @@ -3566,6 +3566,9 @@ Activate by holding the key for the character you want to add an accent to, then Turkish + + Vietnamese + Icelandic diff --git a/src/settings-ui/Settings.UI/ViewModels/PowerAccentViewModel.cs b/src/settings-ui/Settings.UI/ViewModels/PowerAccentViewModel.cs index 320f23c4ae..c8b1f9d907 100644 --- a/src/settings-ui/Settings.UI/ViewModels/PowerAccentViewModel.cs +++ b/src/settings-ui/Settings.UI/ViewModels/PowerAccentViewModel.cs @@ -67,6 +67,7 @@ namespace Microsoft.PowerToys.Settings.UI.ViewModels new PowerAccentLanguageModel("SR_CYRL", "QuickAccent_SelectedLanguage_Serbian_Cyrillic", LanguageGroup), new PowerAccentLanguageModel("SV", "QuickAccent_SelectedLanguage_Swedish", LanguageGroup), new PowerAccentLanguageModel("TK", "QuickAccent_SelectedLanguage_Turkish", LanguageGroup), + new PowerAccentLanguageModel("VI", "QuickAccent_SelectedLanguage_Vietnamese", LanguageGroup), new PowerAccentLanguageModel("CY", "QuickAccent_SelectedLanguage_Welsh", LanguageGroup), ]; From a0495736f14678805481afd330129d907945da54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Fri, 11 Jul 2025 19:08:31 +0200 Subject: [PATCH 03/12] CmdPal: Hide Open URL fallback item when search query is empty or doesn't contain valid URL (#40514) ## Summary of the Pull Request Clears and hides fallback item for URL when query changes and URL is no longer a valid URL. Fixes the situation when the item remains visible in the list with generic text and pointing the last valid URL it was updated with. ## PR Checklist - [x] **Closes:** #40512 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** no new strings - [x] **Dev docs:** nothing to update - [x] **New binaries:** none - [x] **Documentation updated:** none ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed Entered URL to search field. Observed the Open URL item is present. Cleared URL. Observed that Open URL item is not present. --- .../ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs index c702990716..45a65f4902 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/FallbackOpenURLItem.cs @@ -32,6 +32,8 @@ internal sealed partial class FallbackOpenURLItem : FallbackCommandItem { if (!IsValidUrl(query)) { + _executeItem.Url = string.Empty; + _executeItem.Name = string.Empty; Title = string.Empty; Subtitle = string.Empty; return; From 3686d6ac1941f3f501a771e1ffca47195f1b17fa Mon Sep 17 00:00:00 2001 From: ruslanlap <106077551+ruslanlap@users.noreply.github.com> Date: Fri, 11 Jul 2025 20:18:17 +0300 Subject: [PATCH 04/12] Add PowerToysRun-Hotkeys plugin to third-party plugins documentation (#40530) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This PR adds the Hotkeys and RandomGen plugins to the third-party plugins documentation in the General plugins section. ## Hotkeys Plugin ⌨️ Hotkeys for PowerToys Run - Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run ![Hotkeys Demo](https://github.com/ruslanlap/PowerToysRun-Hotkeys/blob/main/assets/demo-hotkeys.gif) This plugin enables users to create and manage custom keyboard shortcuts without leaving PowerToys Run, making it easier to automate repetitive tasks and improve workflow efficiency. ### Features - 🔑 Create custom keyboard shortcuts - 🔄 Manage existing shortcuts - ⚡ Trigger shortcuts directly from PowerToys Run - 📋 Import/export shortcuts - 🔍 Search through your shortcuts - ⚙️ Configure shortcut behavior - 🌐 Application-specific shortcuts - 📝 Add descriptions to shortcuts ## RandomGen Plugin 🎲 RandomGen for PowerToys Run - Generate random data instantly with a single keystroke ![RandomGen Demo](https://github.com/ruslanlap/PowerToysRun-RandomGen/blob/master/assets/demo-randomgen-password.gif) This plugin is particularly useful for designers who need random color codes and placeholder content, as well as developers and testers who need quick access to various types of random data. ### Features - 🔐 Cryptographically Secure Passwords - 📍 Secure PIN Codes - 👤 Personal Data (names, emails, phone numbers) - 🏢 Business Data - 📅 Date Generation - 🔢 Number Generation - 🆔 Unique Identifiers (GUIDs/UUIDs) - 🎨 Color Codes (especially useful for designers) - 🌐 Web Data - 💳 Payment Testing Data - 📝 Lorem Ipsum placeholder text ## Links to plugins - https://github.com/ruslanlap/PowerToysRun-Hotkeys - https://github.com/ruslanlap/PowerToysRun-RandomGen --- doc/thirdPartyRunPlugins.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/doc/thirdPartyRunPlugins.md b/doc/thirdPartyRunPlugins.md index 19c516c68b..30a297a6a7 100644 --- a/doc/thirdPartyRunPlugins.md +++ b/doc/thirdPartyRunPlugins.md @@ -47,6 +47,8 @@ Contact the developers of a plugin directly for assistance with a specific plugi | [Weather](https://github.com/ruslanlap/PowerToysRun-Weather) | [ruslanlap](https://github.com/ruslanlap) | Get real-time weather information directly from PowerToys Run. | | [Pomodoro](https://github.com/ruslanlap/PowerToysRun-Pomodoro) | [ruslanlap](https://github.com/ruslanlap) | Manage Pomodoro productivity sessions directly from PowerToys Run. | | [Definition](https://github.com/ruslanlap/PowerToysRun-Definition) | [ruslanlap](https://github.com/ruslanlap) | Lookup word definitions, phonetics, and synonyms directly in PowerToys Run. | +| [Hotkeys](https://github.com/ruslanlap/PowerToysRun-Hotkeys) | [ruslanlap](https://github.com/ruslanlap) | Create, manage, and trigger custom keyboard shortcuts directly from PowerToys Run. | +| [RandomGen](https://github.com/ruslanlap/PowerToysRun-RandomGen) | [ruslanlap](https://github.com/ruslanlap) | 🎲 Generate random data instantly with a single keystroke. Perfect for developers, testers, designers, and anyone who needs quick access to random data. Features include secure passwords, PINs, names, business data, dates, numbers, GUIDs, color codes, and more. Especially useful for designers who need random color codes and placeholder content. | ## Extending software plugins From 1d464cc3076a94ee10cb9294dc89a03f73fb19ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Sat, 12 Jul 2025 04:11:13 +0200 Subject: [PATCH 05/12] Use Empty content for empty Web search page (#40549) ## Summary of the Pull Request Display full page message when the Web Search extension page is empty image image ## PR Checklist - [x] **Closes:** #38969 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** yay - [x] **Localization:** nope - [x] **Dev docs:** none - [x] **New binaries:** zilch - [x] **Documentation updated:** no need ## Detailed Description of the Pull Request / Additional comments -- ## Validation Steps Performed Tested with and without enabled history. --- .../Pages/WebSearchListPage.cs | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs index 406f008a84..c96efe24c7 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.WebSearch/Pages/WebSearchListPage.cs @@ -23,28 +23,31 @@ internal sealed partial class WebSearchListPage : DynamicListPage private readonly SettingsManager _settingsManager; private static readonly CompositeFormat PluginInBrowserName = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_in_browser_name); private static readonly CompositeFormat PluginOpen = System.Text.CompositeFormat.Parse(Properties.Resources.plugin_open); - private List allItems; + private List _allItems; public WebSearchListPage(SettingsManager settingsManager) { Name = Resources.command_item_title; Title = Resources.command_item_title; - PlaceholderText = Resources.plugin_description; Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"); - allItems = [new(new NoOpCommand()) - { - Icon = IconHelpers.FromRelativePath("Assets\\WebSearch.png"), - Title = Properties.Resources.plugin_description, - Subtitle = string.Format(CultureInfo.CurrentCulture, PluginOpen, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), - } - ]; + _allItems = []; Id = "com.microsoft.cmdpal.websearch"; _settingsManager = settingsManager; _historyItems = _settingsManager.ShowHistory != Resources.history_none ? _settingsManager.LoadHistory() : null; if (_historyItems != null) { - allItems.AddRange(_historyItems); + _allItems.AddRange(_historyItems); } + + // It just looks viewer to have string twice on the page, and default placeholder is good enough + PlaceholderText = _allItems.Count > 0 ? Resources.plugin_description : string.Empty; + + EmptyContent = new CommandItem(new NoOpCommand()) + { + Icon = Icon, + Title = Properties.Resources.plugin_description, + Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), + }; } public List Query(string query) @@ -59,17 +62,7 @@ internal sealed partial class WebSearchListPage : DynamicListPage var results = new List(); - // empty query - if (string.IsNullOrEmpty(query)) - { - results.Add(new ListItem(new SearchWebCommand(string.Empty, _settingsManager)) - { - Title = Properties.Resources.plugin_description, - Subtitle = string.Format(CultureInfo.CurrentCulture, PluginInBrowserName, BrowserInfo.Name ?? BrowserInfo.MSEdgeName), - Icon = new IconInfo(_iconPath), - }); - } - else + if (!string.IsNullOrEmpty(query)) { var searchTerm = query; var result = new ListItem(new SearchWebCommand(searchTerm, _settingsManager)) @@ -91,9 +84,9 @@ internal sealed partial class WebSearchListPage : DynamicListPage public override void UpdateSearchText(string oldSearch, string newSearch) { - allItems = [.. Query(newSearch)]; + _allItems = [.. Query(newSearch)]; RaiseItemsChanged(0); } - public override IListItem[] GetItems() => [.. allItems]; + public override IListItem[] GetItems() => [.. _allItems]; } From d258dcd61bd2496739c70fb4237202f5765a7072 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Sat, 12 Jul 2025 21:38:01 +0200 Subject: [PATCH 06/12] CmdPal: Remove backdrop targets from existing backdrop controller before assigning a new one (#40540) ## Summary of the Pull Request Also runs the code through UI thread. it will ensure correct access to _acrylicController, and also because the method touches UI element. ## PR Checklist - [x] **Closes:** #38659 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [x] **Dev docs:** Added/updated - [x] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [x] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index 3020c85859..ec82d3357e 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -107,7 +107,7 @@ public sealed partial class MainWindow : WindowEx, App.Current.Services.GetService()!.SettingsChanged += SettingsChangedHandler; // Make sure that we update the acrylic theme when the OS theme changes - RootShellPage.ActualThemeChanged += (s, e) => UpdateAcrylic(); + RootShellPage.ActualThemeChanged += (s, e) => DispatcherQueue.TryEnqueue(UpdateAcrylic); // Hardcoding event name to avoid bringing in the PowerToys.interop dependency. Event name must match CMDPAL_SHOW_EVENT from shared_constants.h NativeEventWaiter.WaitForEventLoop("Local\\PowerToysCmdPal-ShowEvent-62336fcd-8611-4023-9b30-091a6af4cc5a", () => @@ -175,6 +175,8 @@ public sealed partial class MainWindow : WindowEx, private void UpdateAcrylic() { + _acrylicController?.RemoveAllSystemBackdropTargets(); + _acrylicController = GetAcrylicConfig(Content); // Enable the system backdrop. From 227c5d8147f60564cf5f080719fb841409a7933f Mon Sep 17 00:00:00 2001 From: Davide Giacometti <25966642+davidegiacometti@users.noreply.github.com> Date: Mon, 14 Jul 2025 02:50:45 +0200 Subject: [PATCH 07/12] [Peek] Terminate Preview Handlers Processes (#40116) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary of the Pull Request This is an attempt to release Preview Handlers instantiated by Peek and close the related processe. ⚠️ Note that even if the PR improve the current behavior, the solution doesn't work 100% of times. I noticed that sometimes the process gets leaked also when Preview Handler is used in Explorer 🤔 ## PR Checklist - [x] **Closes:** #40117 - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments - Ported the same fix applied to CmdPal to ensure the process is terminated gracefully instead of being killed: https://github.com/microsoft/PowerToys/pull/39589 - Attempt to cleanup Preview Handlers and close the relative process ## Validation Steps Performed Tested manually: - Preview through some Excel and Word files - Close Peek window - Excel and Word processes are closed --- .../ShellPreviewHandlerPreviewer.cs | 16 ++++++++++++++-- src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs | 2 ++ .../peek/Peek.UI/PeekXAML/MainWindow.xaml.cs | 3 +++ src/modules/peek/peek/dllmain.cpp | 16 +++++++++++----- 4 files changed, 30 insertions(+), 7 deletions(-) diff --git a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs index 608ce78f3f..7dbcada42d 100644 --- a/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs +++ b/src/modules/peek/Peek.FilePreviewer/Previewers/ShellPreviewHandlerPreviewer/ShellPreviewHandlerPreviewer.cs @@ -90,14 +90,12 @@ namespace Peek.FilePreviewer.Previewers unsafe { // This runs the preview handler in a separate process (prevhost.exe) - // TODO: Figure out how to get it to run in a low integrity level if (!HandlerFactories.TryGetValue(clsid, out var factory)) { var hr = PInvoke_FilePreviewer.CoGetClassObject(clsid, CLSCTX.CLSCTX_LOCAL_SERVER, null, typeof(IClassFactory).GUID, out object pFactory); Marshal.ThrowExceptionForHR(hr); // Storing the factory in memory helps makes the handlers load faster - // TODO: Maybe free them after some inactivity or when Peek quits? factory = (IClassFactory)pFactory; factory.LockServer(true); HandlerFactories.AddOrUpdate(clsid, factory, (_, _) => factory); @@ -213,6 +211,20 @@ namespace Peek.FilePreviewer.Previewers return !string.IsNullOrEmpty(GetPreviewHandlerGuid(item.Extension)); } + public static void ReleaseHandlerFactories() + { + foreach (var factory in HandlerFactories.Values) + { + try + { + Marshal.FinalReleaseComObject(factory); + } + catch + { + } + } + } + private static string? GetPreviewHandlerGuid(string fileExt) { const string PreviewHandlerKeyPath = "shellex\\{8895b1c6-b41f-4c1c-a562-0d564250836f}"; diff --git a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs index 9b753b3224..9bd66e380f 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/App.xaml.cs @@ -12,6 +12,7 @@ using Microsoft.UI.Xaml; using Peek.Common; using Peek.FilePreviewer; using Peek.FilePreviewer.Models; +using Peek.FilePreviewer.Previewers; using Peek.UI.Native; using Peek.UI.Telemetry.Events; using Peek.UI.Views; @@ -111,6 +112,7 @@ namespace Peek.UI NativeEventWaiter.WaitForEventLoop(Constants.ShowPeekEvent(), OnPeekHotkey); NativeEventWaiter.WaitForEventLoop(Constants.TerminatePeekEvent(), () => { + ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); EtwTrace?.Dispose(); Environment.Exit(0); }); diff --git a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs index 3ce9a82bf9..4edad9a807 100644 --- a/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs +++ b/src/modules/peek/Peek.UI/PeekXAML/MainWindow.xaml.cs @@ -15,6 +15,7 @@ using Microsoft.UI.Xaml.Input; using Peek.Common.Constants; using Peek.Common.Extensions; using Peek.FilePreviewer.Models; +using Peek.FilePreviewer.Previewers; using Peek.UI.Extensions; using Peek.UI.Helpers; using Peek.UI.Telemetry.Events; @@ -204,6 +205,8 @@ namespace Peek.UI ViewModel.ScalingFactor = 1; this.Content.KeyUp -= Content_KeyUp; + + ShellPreviewHandlerPreviewer.ReleaseHandlerFactories(); } /// diff --git a/src/modules/peek/peek/dllmain.cpp b/src/modules/peek/peek/dllmain.cpp index 5fd4da8039..4c3da5d999 100644 --- a/src/modules/peek/peek/dllmain.cpp +++ b/src/modules/peek/peek/dllmain.cpp @@ -405,13 +405,19 @@ public: { ResetEvent(m_hInvokeEvent); SetEvent(m_hTerminateEvent); - WaitForSingleObject(m_hProcess, 1500); - auto result = TerminateProcess(m_hProcess, 1); - if (result == 0) + + HANDLE hProcess = OpenProcess(SYNCHRONIZE | PROCESS_TERMINATE, FALSE, m_processPid); + if (WaitForSingleObject(hProcess, 1500) == WAIT_TIMEOUT) { - int error = GetLastError(); - Logger::trace("Couldn't terminate the process. Last error: {}", error); + auto result = TerminateProcess(hProcess, 1); + if (result == 0) + { + int error = GetLastError(); + Logger::trace("Couldn't terminate the process. Last error: {}", error); + } } + + CloseHandle(hProcess); CloseHandle(m_hProcess); m_hProcess = 0; m_processPid = 0; From d96c29d22d95a7ba406ab56d12a42ae9333ce9ea Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Mon, 14 Jul 2025 13:58:59 -0500 Subject: [PATCH 08/12] Replace our nuget.org feed with a public azure artifacts feed (#40486) _This madness has gone on too long, I say_ This replaces our default nuget.org feed with a public azure artifacts feed in the shine-oss org. This is what literally everyone else does, I don't know why we don't. This should unblock _wait where'd that issue go_ since we can just add the community toolkit labs feed as an upstream This has the negative side effect that it did prompt me to log in to azure artifacts with my MSA. I've cancelled like 5 prompts now, but it seems to still be working on it? --- nuget.config | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nuget.config b/nuget.config index e6a17ffdfe..51f9b3b3f7 100644 --- a/nuget.config +++ b/nuget.config @@ -2,10 +2,10 @@ - + - + From 5800b816386a7bad40b9ffe9c7327f88b4a5ab73 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ji=C5=99=C3=AD=20Pol=C3=A1=C5=A1ek?= Date: Tue, 15 Jul 2025 03:03:52 +0200 Subject: [PATCH 09/12] Fix loading top-level commands (#40602) ## Summary of the Pull Request Updates LoadTopLevelCommandsFromProvider so it returns list of top-level commands for further instead of modifying TopLevelCommands collection directly. Reverts unintended change from cfa5f75 where LoadTopLevelCommandsFromProvider updates the shared TopLevelCommands collection directly from in a task, causing thread-safety issues and bypassing synchronization lock on the collection. ## PR Checklist - [ ] **Closes:** -- - [ ] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [ ] **Tests:** Added/updated and all pass - [ ] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --- .../Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index 752ca0e38e..d9beb0995d 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -104,14 +104,14 @@ public partial class TopLevelCommandManager : ObservableObject, List commands = []; foreach (var item in commandProvider.TopLevelItems) { - TopLevelCommands.Add(item); + commands.Add(item); } foreach (var item in commandProvider.FallbackItems) { if (item.IsEnabled) { - TopLevelCommands.Add(item); + commands.Add(item); } } From 19390e319879d6f751a16c8248608232a1aa36de Mon Sep 17 00:00:00 2001 From: Michael Jolley Date: Tue, 15 Jul 2025 08:59:32 -0500 Subject: [PATCH 10/12] Users can now pin/unpin apps from the top of AllApps extension (#40544) Users can now use Ctrl+P to toggle pinning and unpinning applications from the top of the All Apps extension. This pinned status persists through restarts & reboots. https://github.com/user-attachments/assets/86895a38-7312-438a-9409-b50a85979d12 Reference: #40543 --------- Co-authored-by: Mike Griese --- .../AllAppsCommandProvider.cs | 14 +- .../Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs | 188 ++++++++++++------ .../ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs | 5 +- .../Microsoft.CmdPal.Ext.Apps/AppListItem.cs | 41 +++- .../Commands/CopyPathCommand.cs | 5 +- .../Commands/OpenInConsoleCommand.cs | 5 +- .../Commands/OpenPathCommand.cs | 5 +- .../Commands/PinAppCommand.cs | 29 +++ .../Commands/RunAsAdminCommand.cs | 5 +- .../Commands/RunAsUserCommand.cs | 9 +- .../Commands/UnpinAppCommand.cs | 28 +++ .../Helpers/Icons.cs | 26 +++ .../JsonSerializationContext.cs | 21 ++ .../Programs/UWPApplication.cs | 31 ++- .../Programs/Win32Program.cs | 37 +++- .../Properties/Resources.Designer.cs | 18 ++ .../Properties/Resources.resx | 6 + .../State/PinStateChangedEventArgs.cs | 34 ++++ .../State/PinnedApps.cs | 47 +++++ .../State/PinnedAppsManager.cs | 82 ++++++++ 20 files changed, 548 insertions(+), 88 deletions(-) create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/PinAppCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/UnpinAppCommand.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/Icons.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinStateChangedEventArgs.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedApps.cs create mode 100644 src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedAppsManager.cs diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs index e62fc71e2b..6d860ee6cd 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsCommandProvider.cs @@ -2,8 +2,12 @@ // 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 Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -29,9 +33,12 @@ public partial class AllAppsCommandProvider : CommandProvider Subtitle = Resources.search_installed_apps, MoreCommands = [new CommandContextItem(AllAppsSettings.Instance.Settings.SettingsPage)], }; + + // Subscribe to pin state changes to refresh the command provider + PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; } - public override ICommandItem[] TopLevelCommands() => [_listItem]; + public override ICommandItem[] TopLevelCommands() => [_listItem, ..Page.GetPinnedApps()]; public ICommandItem? LookupApp(string displayName) { @@ -62,4 +69,9 @@ public partial class AllAppsCommandProvider : CommandProvider return null; } + + private void OnPinStateChanged(object? sender, System.EventArgs e) + { + RaiseItemsChanged(0); + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs index f271d4d556..46300f6bc7 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AllAppsPage.cs @@ -2,14 +2,20 @@ // The Microsoft Corporation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; +using System.Numerics; +using System.Runtime.CompilerServices; using System.Threading; using System.Threading.Tasks; using ManagedCommon; +using Microsoft.CmdPal.Ext.Apps.Commands; +using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Programs; using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -18,16 +24,22 @@ namespace Microsoft.CmdPal.Ext.Apps; public sealed partial class AllAppsPage : ListPage { private readonly Lock _listLock = new(); - private AppListItem[] allAppsSection = []; + + private AppItem[] allApps = []; + private AppListItem[] unpinnedApps = []; + private AppListItem[] pinnedApps = []; public AllAppsPage() { this.Name = Resources.all_apps; - this.Icon = IconHelpers.FromRelativePath("Assets\\AllApps.svg"); + this.Icon = Icons.AllAppsIcon; this.ShowDetails = true; this.IsLoading = true; this.PlaceholderText = Resources.search_installed_apps_placeholder; + // Subscribe to pin state changes to refresh the command provider + PinnedAppsManager.Instance.PinStateChanged += OnPinStateChanged; + Task.Run(() => { lock (_listLock) @@ -37,89 +49,139 @@ public sealed partial class AllAppsPage : ListPage }); } + internal AppListItem[] GetPinnedApps() + { + BuildListItems(); + return pinnedApps; + } + public override IListItem[] GetItems() { - if (allAppsSection.Length == 0 || AppCache.Instance.Value.ShouldReload()) - { - lock (_listLock) - { - BuildListItems(); - } - } - - return allAppsSection; + // Build or update the list if needed + BuildListItems(); + return pinnedApps.Concat(unpinnedApps).ToArray(); } private void BuildListItems() { - this.IsLoading = true; + if (allApps.Length == 0 || AppCache.Instance.Value.ShouldReload()) + { + lock (_listLock) + { + this.IsLoading = true; - Stopwatch stopwatch = new(); - stopwatch.Start(); + Stopwatch stopwatch = new(); + stopwatch.Start(); - var apps = GetPrograms(); + var apps = GetPrograms(); + this.allApps = apps.AllApps; + this.pinnedApps = apps.PinnedItems; + this.unpinnedApps = apps.UnpinnedItems; - this.allAppsSection = apps - .Select((app) => new AppListItem(app, true)) - .ToArray(); + this.IsLoading = false; - this.IsLoading = false; + AppCache.Instance.Value.ResetReloadFlag(); - AppCache.Instance.Value.ResetReloadFlag(); - - stopwatch.Stop(); - Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms"); + stopwatch.Stop(); + Logger.LogTrace($"{nameof(AllAppsPage)}.{nameof(BuildListItems)} took: {stopwatch.ElapsedMilliseconds} ms"); + } + } } - internal List GetPrograms() + private AppItem[] GetAllApps() { var uwpResults = AppCache.Instance.Value.UWPs - .Where((application) => application.Enabled) - .Select(UwpToAppItem); + .Where((application) => application.Enabled) + .Select(app => app.ToAppItem()); var win32Results = AppCache.Instance.Value.Win32s .Where((application) => application.Enabled && application.Valid) - .Select(app => - { - var icoPath = string.IsNullOrEmpty(app.IcoPath) ? - (app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ? - app.IcoPath : - app.FullPath) : - app.IcoPath; + .Select(app => app.ToAppItem()); - // icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? (icoPath + ",0") : icoPath; - icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? - app.FullPath : - icoPath; - return new AppItem() - { - Name = app.Name, - Subtitle = app.Description, - Type = app.Type(), - IcoPath = icoPath, - ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath, - DirPath = app.Location, - Commands = app.GetCommands(), - }; - }); - - return uwpResults.Concat(win32Results).OrderBy(app => app.Name).ToList(); + var allApps = uwpResults.Concat(win32Results).ToArray(); + return allApps; } - private AppItem UwpToAppItem(UWPApplication app) + internal (AppItem[] AllApps, AppListItem[] PinnedItems, AppListItem[] UnpinnedItems) GetPrograms() { - var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty; - var item = new AppItem() + var allApps = GetAllApps(); + var pinned = new List(); + var unpinned = new List(); + + foreach (var app in allApps) { - Name = app.Name, - Subtitle = app.Description, - Type = UWPApplication.Type(), - IcoPath = iconPath, - DirPath = app.Location, - UserModelId = app.UserModelId, - IsPackaged = true, - Commands = app.GetCommands(), - }; - return item; + var isPinned = PinnedAppsManager.Instance.IsAppPinned(app.AppIdentifier); + var appListItem = new AppListItem(app, true, isPinned); + + if (isPinned) + { + appListItem.Tags = appListItem.Tags + .Concat([new Tag() { Icon = Icons.PinIcon }]) + .ToArray(); + pinned.Add(appListItem); + } + else + { + unpinned.Add(appListItem); + } + } + + return ( + allApps + .ToArray(), + pinned + .OrderBy(app => app.Title) + .ToArray(), + unpinned + .OrderBy(app => app.Title) + .ToArray()); + } + + private void OnPinStateChanged(object? sender, PinStateChangedEventArgs e) + { + /* + * Rebuilding all the lists is pretty expensive. + * So, instead, we'll just compare pinned items to move existing + * items between the two lists. + */ + var existingAppItem = allApps.FirstOrDefault(f => f.AppIdentifier == e.AppIdentifier); + + if (existingAppItem != null) + { + var appListItem = new AppListItem(existingAppItem, true, e.IsPinned); + + if (e.IsPinned) + { + // Remove it from the unpinned apps array + this.unpinnedApps = this.unpinnedApps + .Where(app => app.AppIdentifier != existingAppItem.AppIdentifier) + .OrderBy(app => app.Title) + .ToArray(); + + var newPinned = this.pinnedApps.ToList(); + newPinned.Add(appListItem); + + this.pinnedApps = newPinned + .OrderBy(app => app.Title) + .ToArray(); + } + else + { + // Remove it from the pinned apps array + this.pinnedApps = this.pinnedApps + .Where(app => app.AppIdentifier != existingAppItem.AppIdentifier) + .OrderBy(app => app.Title) + .ToArray(); + + var newUnpinned = this.unpinnedApps.ToList(); + newUnpinned.Add(appListItem); + + this.unpinnedApps = newUnpinned + .OrderBy(app => app.Title) + .ToArray(); + } + + RaiseItemsChanged(0); + } } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs index 4bb26bed67..2e5ba78b1f 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppItem.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using Microsoft.CmdPal.Ext.Apps.Programs; +using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; namespace Microsoft.CmdPal.Ext.Apps; @@ -26,7 +27,9 @@ internal sealed class AppItem public bool IsPackaged { get; set; } - public List? Commands { get; set; } + public List? Commands { get; set; } + + public string AppIdentifier { get; set; } = string.Empty; public AppItem() { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs index 57c9175d4d..4ac8f79aea 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/AppListItem.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Apps.Commands; using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.Storage.Streams; @@ -23,14 +24,17 @@ internal sealed partial class AppListItem : ListItem public override IIconInfo? Icon { get => _icon.Value; set => base.Icon = value; } - public AppListItem(AppItem app, bool useThumbnails) + public string AppIdentifier => _app.AppIdentifier; + + public AppListItem(AppItem app, bool useThumbnails, bool isPinned) : base(new AppCommand(app)) { _app = app; Title = app.Name; Subtitle = app.Subtitle; Tags = [_appTag]; - MoreCommands = _app.Commands!.ToArray(); + + MoreCommands = AddPinCommands(_app.Commands!, isPinned); _details = new Lazy
(() => { @@ -121,4 +125,37 @@ internal sealed partial class AppListItem : ListItem return icon; } + + private IContextItem[] AddPinCommands(List commands, bool isPinned) + { + var newCommands = new List(); + newCommands.AddRange(commands); + + newCommands.Add(new SeparatorContextItem()); + + // 0x50 = P + // Full key chord would be Ctrl+P + var pinKeyChord = KeyChordHelpers.FromModifiers(true, false, false, false, 0x50, 0); + + if (isPinned) + { + newCommands.Add( + new CommandContextItem( + new UnpinAppCommand(this.AppIdentifier)) + { + RequestedShortcut = pinKeyChord, + }); + } + else + { + newCommands.Add( + new CommandContextItem( + new PinAppCommand(this.AppIdentifier)) + { + RequestedShortcut = pinKeyChord, + }); + } + + return newCommands.ToArray(); + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/CopyPathCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/CopyPathCommand.cs index 30ad044f37..9618e2fa43 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/CopyPathCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/CopyPathCommand.cs @@ -6,6 +6,7 @@ using System; using System.Globalization; using System.Text; using ManagedCommon; +using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -13,14 +14,12 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands; internal sealed partial class CopyPathCommand : InvokableCommand { - private static readonly IconInfo TheIcon = new("\ue8c8"); - private readonly string _target; public CopyPathCommand(string target) { Name = Resources.copy_path; - Icon = TheIcon; + Icon = Icons.CopyIcon; _target = target; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs index 41e934759a..e4f6ec5228 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenInConsoleCommand.cs @@ -6,6 +6,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; using ManagedCommon; +using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -13,14 +14,12 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands; internal sealed partial class OpenInConsoleCommand : InvokableCommand { - private static readonly IconInfo TheIcon = new("\ue838"); - private readonly string _target; public OpenInConsoleCommand(string target) { Name = Resources.open_path_in_console; - Icon = TheIcon; + Icon = Icons.OpenConsoleIcon; _target = target; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs index 88c7df5d94..06ad9c67ce 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/OpenPathCommand.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -11,14 +12,12 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands; internal sealed partial class OpenPathCommand : InvokableCommand { - private static readonly IconInfo TheIcon = new("\ue838"); - private readonly string _target; public OpenPathCommand(string target) { Name = Resources.open_location; - Icon = TheIcon; + Icon = Icons.OpenPathIcon; _target = target; } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/PinAppCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/PinAppCommand.cs new file mode 100644 index 0000000000..6f26fde6c2 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/PinAppCommand.cs @@ -0,0 +1,29 @@ +// 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.Diagnostics.CodeAnalysis; +using Microsoft.CmdPal.Ext.Apps.Helpers; +using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.State; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps.Commands; + +internal sealed partial class PinAppCommand : InvokableCommand +{ + private readonly string _appIdentifier; + + public PinAppCommand(string appIdentifier) + { + _appIdentifier = appIdentifier; + Name = Resources.pin_app; + Icon = Icons.PinIcon; + } + + public override CommandResult Invoke() + { + PinnedAppsManager.Instance.PinApp(_appIdentifier); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs index 0a6d43f608..d3714ea8ae 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsAdminCommand.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Utils; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -13,8 +14,6 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands; internal sealed partial class RunAsAdminCommand : InvokableCommand { - private static readonly IconInfo TheIcon = new("\uE7EF"); - private readonly string _target; private readonly string _parentDir; private readonly bool _packaged; @@ -22,7 +21,7 @@ internal sealed partial class RunAsAdminCommand : InvokableCommand public RunAsAdminCommand(string target, string parentDir, bool packaged) { Name = Resources.run_as_administrator; - Icon = TheIcon; + Icon = Icons.RunAsIcon; _target = target; _parentDir = parentDir; diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs index c897aed560..7afa8e7e13 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/RunAsUserCommand.cs @@ -5,6 +5,7 @@ using System; using System.Diagnostics; using System.Threading.Tasks; +using Microsoft.CmdPal.Ext.Apps.Helpers; using Microsoft.CmdPal.Ext.Apps.Properties; using Microsoft.CmdPal.Ext.Apps.Utils; using Microsoft.CommandPalette.Extensions.Toolkit; @@ -13,21 +14,19 @@ namespace Microsoft.CmdPal.Ext.Apps.Commands; internal sealed partial class RunAsUserCommand : InvokableCommand { - private static readonly IconInfo TheIcon = new("\uE7EE"); - private readonly string _target; private readonly string _parentDir; public RunAsUserCommand(string target, string parentDir) { Name = Resources.run_as_different_user; - Icon = TheIcon; + Icon = Icons.RunAsUserIcon; _target = target; _parentDir = parentDir; } - internal static async Task RunAsAdmin(string target, string parentDir) + internal static async Task RunAsUser(string target, string parentDir) { await Task.Run(() => { @@ -39,7 +38,7 @@ internal sealed partial class RunAsUserCommand : InvokableCommand public override CommandResult Invoke() { - _ = RunAsAdmin(_target, _parentDir).ConfigureAwait(false); + _ = RunAsUser(_target, _parentDir).ConfigureAwait(false); return CommandResult.Dismiss(); } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/UnpinAppCommand.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/UnpinAppCommand.cs new file mode 100644 index 0000000000..cf829f8521 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Commands/UnpinAppCommand.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 Microsoft.CmdPal.Ext.Apps.Helpers; +using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.State; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps.Commands; + +internal sealed partial class UnpinAppCommand : InvokableCommand +{ + private readonly string _appIdentifier; + + public UnpinAppCommand(string appIdentifier) + { + _appIdentifier = appIdentifier; + Name = Resources.unpin_app; + Icon = Icons.UnpinIcon; + } + + public override CommandResult Invoke() + { + PinnedAppsManager.Instance.UnpinApp(_appIdentifier); + return CommandResult.KeepOpen(); + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/Icons.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/Icons.cs new file mode 100644 index 0000000000..45a10af4e5 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Helpers/Icons.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 Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps.Helpers; + +public static partial class Icons +{ + public static IconInfo UnpinIcon { get; } = new("\uE77A"); + + public static IconInfo PinIcon { get; } = new("\uE840"); + + public static IconInfo RunAsIcon { get; } = new("\uE7EF"); + + public static IconInfo RunAsUserIcon { get; } = new("\uE7EE"); + + public static IconInfo CopyIcon { get; } = new("\ue8c8"); + + public static IconInfo OpenConsoleIcon { get; } = new("\ue838"); + + public static IconInfo OpenPathIcon { get; } = new("\ue838"); + + public static IconInfo AllAppsIcon { get; } = IconHelpers.FromRelativePath("Assets\\AllApps.svg"); +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs new file mode 100644 index 0000000000..5a11d9c135 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/JsonSerializationContext.cs @@ -0,0 +1,21 @@ +// 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; +using Microsoft.CmdPal.Ext.Apps.State; + +namespace Microsoft.CmdPal.Ext.Apps; + +[JsonSerializable(typeof(string))] +[JsonSerializable(typeof(PinnedApps))] +[JsonSerializable(typeof(List), TypeInfoPropertyName = "StringList")] +[JsonSourceGenerationOptions(UseStringEnumConverter = true, WriteIndented = true, IncludeFields = true, PropertyNameCaseInsensitive = true, AllowTrailingCommas = true)] +internal sealed partial class JsonSerializationContext : JsonSerializerContext +{ +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs index 1cf4bc4463..f0f8de8a35 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/UWPApplication.cs @@ -10,7 +10,9 @@ using System.Xml; using ManagedCommon; using Microsoft.CmdPal.Ext.Apps.Commands; using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CmdPal.Ext.Apps.Utils; +using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Windows.System; using Windows.Win32; @@ -70,9 +72,15 @@ public class UWPApplication : IProgram return Resources.packaged_application; } - public List GetCommands() + public string GetAppIdentifier() { - List commands = []; + // Use UserModelId for UWP apps as it's unique + return UserModelId; + } + + public List GetCommands() + { + List commands = []; if (CanRunElevated) { @@ -511,6 +519,25 @@ public class UWPApplication : IProgram } } + internal AppItem ToAppItem() + { + var app = this; + var iconPath = app.LogoType != LogoType.Error ? app.LogoPath : string.Empty; + var item = new AppItem() + { + Name = app.Name, + Subtitle = app.Description, + Type = UWPApplication.Type(), + IcoPath = iconPath, + DirPath = app.Location, + UserModelId = app.UserModelId, + IsPackaged = true, + Commands = app.GetCommands(), + AppIdentifier = app.GetAppIdentifier(), + }; + return item; + } + /* public ImageSource Logo() { diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs index dd6bdd875d..d8bebcd9a4 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Programs/Win32Program.cs @@ -19,7 +19,9 @@ using System.Windows.Input; using ManagedCommon; using Microsoft.CmdPal.Ext.Apps.Commands; using Microsoft.CmdPal.Ext.Apps.Properties; +using Microsoft.CmdPal.Ext.Apps.State; using Microsoft.CmdPal.Ext.Apps.Utils; +using Microsoft.CommandPalette.Extensions; using Microsoft.CommandPalette.Extensions.Toolkit; using Microsoft.Win32; using Windows.System; @@ -186,9 +188,9 @@ public class Win32Program : IProgram return true; } - public List GetCommands() + public List GetCommands() { - List commands = new List(); + List commands = new List(); if (AppType != ApplicationType.InternetShortcutApplication && AppType != ApplicationType.Folder && AppType != ApplicationType.GenericFile) { @@ -231,6 +233,12 @@ public class Win32Program : IProgram return ExecutableName; } + public string GetAppIdentifier() + { + // Use a combination of name and path to create a unique identifier + return $"{Name}|{FullPath}"; + } + private static Win32Program CreateWin32Program(string path) { try @@ -933,4 +941,29 @@ public class Win32Program : IProgram return Array.Empty(); } } + + internal AppItem ToAppItem() + { + var app = this; + var icoPath = string.IsNullOrEmpty(app.IcoPath) ? + (app.AppType == Win32Program.ApplicationType.InternetShortcutApplication ? + app.IcoPath : + app.FullPath) : + app.IcoPath; + + icoPath = icoPath.EndsWith(".lnk", System.StringComparison.InvariantCultureIgnoreCase) ? + app.FullPath : + icoPath; + return new AppItem() + { + Name = app.Name, + Subtitle = app.Description, + Type = app.Type(), + IcoPath = icoPath, + ExePath = !string.IsNullOrEmpty(app.LnkFilePath) ? app.LnkFilePath : app.FullPath, + DirPath = app.Location, + Commands = app.GetCommands(), + AppIdentifier = app.GetAppIdentifier(), + }; + } } diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs index cc07ac86b4..419bba3580 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.Designer.cs @@ -213,6 +213,15 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties { } } + /// + /// Looks up a localized string similar to Pin. + /// + internal static string pin_app { + get { + return ResourceManager.GetString("pin_app", resourceCulture); + } + } + /// /// Looks up a localized string similar to Run as administrator. /// @@ -267,6 +276,15 @@ namespace Microsoft.CmdPal.Ext.Apps.Properties { } } + /// + /// Looks up a localized string similar to Unpin. + /// + internal static string unpin_app { + get { + return ResourceManager.GetString("unpin_app", resourceCulture); + } + } + /// /// Looks up a localized string similar to Experimental: When enabled, Command Palette will load thumbnails from the Windows Shell. Using thumbnails may cause the app to crash on launch. /// diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx index ce4fb79689..1e4c8fa4bd 100644 --- a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/Properties/Resources.resx @@ -199,4 +199,10 @@ Experimental: When enabled, Command Palette will load thumbnails from the Windows Shell. Using thumbnails may cause the app to crash on launch A description for "use_thumbnails_setting_label" + + Pin + + + Unpin + \ No newline at end of file diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinStateChangedEventArgs.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinStateChangedEventArgs.cs new file mode 100644 index 0000000000..75cbbde56d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinStateChangedEventArgs.cs @@ -0,0 +1,34 @@ +// 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.Threading.Tasks; + +namespace Microsoft.CmdPal.Ext.Apps.State; + +public class PinStateChangedEventArgs : EventArgs +{ + /// + /// Gets the identifier of the application whose pin state has changed. + /// + public string AppIdentifier { get; } + + /// + /// Gets a value indicating whether the specified app identifier was pinned or not. + /// + public bool IsPinned { get; } + + /// + /// Initializes a new instance of the class. + /// + /// The identifier of the application whose pin state has changed. + public PinStateChangedEventArgs(string appIdentifier, bool isPinned) + { + AppIdentifier = appIdentifier ?? throw new ArgumentNullException(nameof(appIdentifier)); + IsPinned = isPinned; + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedApps.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedApps.cs new file mode 100644 index 0000000000..ff76043bf1 --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedApps.cs @@ -0,0 +1,47 @@ +// 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.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text.Json; + +namespace Microsoft.CmdPal.Ext.Apps.State; + +public sealed class PinnedApps +{ + public List PinnedAppIdentifiers { get; set; } = []; + + public static PinnedApps ReadFromFile(string path) + { + if (!File.Exists(path)) + { + return new PinnedApps(); + } + + try + { + var jsonString = File.ReadAllText(path); + var result = JsonSerializer.Deserialize(jsonString, JsonSerializationContext.Default.PinnedApps); + return result ?? new PinnedApps(); + } + catch + { + return new PinnedApps(); + } + } + + public static void WriteToFile(string path, PinnedApps data) + { + try + { + var jsonString = JsonSerializer.Serialize(data, JsonSerializationContext.Default.PinnedApps); + File.WriteAllText(path, jsonString); + } + catch + { + // Silently fail - we don't want pinning issues to crash the extension + } + } +} diff --git a/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedAppsManager.cs b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedAppsManager.cs new file mode 100644 index 0000000000..4540caf78d --- /dev/null +++ b/src/modules/cmdpal/ext/Microsoft.CmdPal.Ext.Apps/State/PinnedAppsManager.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; +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Linq; +using ManagedCommon; +using Microsoft.CommandPalette.Extensions.Toolkit; + +namespace Microsoft.CmdPal.Ext.Apps.State; + +public sealed class PinnedAppsManager +{ + private static readonly Lazy _instance = new(() => new PinnedAppsManager()); + private readonly string _pinnedAppsFilePath; + + public static PinnedAppsManager Instance => _instance.Value; + + private PinnedApps _pinnedApps = new(); + + // Add event for when pinning state changes + public event EventHandler? PinStateChanged; + + private PinnedAppsManager() + { + _pinnedAppsFilePath = GetPinnedAppsFilePath(); + LoadPinnedApps(); + } + + public bool IsAppPinned(string appIdentifier) + { + return _pinnedApps.PinnedAppIdentifiers.Contains(appIdentifier, StringComparer.OrdinalIgnoreCase); + } + + public void PinApp(string appIdentifier) + { + if (!IsAppPinned(appIdentifier)) + { + _pinnedApps.PinnedAppIdentifiers.Add(appIdentifier); + SavePinnedApps(); + Logger.LogTrace($"Pinned app: {appIdentifier}"); + PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, true)); + } + } + + public string[] GetPinnedAppIdentifiers() + { + return _pinnedApps.PinnedAppIdentifiers.ToArray(); + } + + public void UnpinApp(string appIdentifier) + { + var removed = _pinnedApps.PinnedAppIdentifiers.RemoveAll(id => + string.Equals(id, appIdentifier, StringComparison.OrdinalIgnoreCase)); + + if (removed > 0) + { + SavePinnedApps(); + Logger.LogTrace($"Unpinned app: {appIdentifier}"); + PinStateChanged?.Invoke(this, new PinStateChangedEventArgs(appIdentifier, false)); + } + } + + private void LoadPinnedApps() + { + _pinnedApps = PinnedApps.ReadFromFile(_pinnedAppsFilePath); + } + + private void SavePinnedApps() + { + PinnedApps.WriteToFile(_pinnedAppsFilePath, _pinnedApps); + } + + private static string GetPinnedAppsFilePath() + { + var directory = Utilities.BaseSettingsPath("Microsoft.CmdPal"); + Directory.CreateDirectory(directory); + return Path.Combine(directory, "apps.pinned.json"); + } +} From 0783763dd080baa43f2736acaabe35550ecde9d9 Mon Sep 17 00:00:00 2001 From: Yu Leng <42196638+moooyo@users.noreply.github.com> Date: Tue, 15 Jul 2025 22:15:18 +0800 Subject: [PATCH 11/12] [AOT] Enable AOT for CmdPal (#40551) ## Summary of the Pull Request Base on https://github.com/microsoft/PowerToys/pull/40486 we can easily use lab package. So all blocker has been resolved. 1. Replace CommunityToolkit.WinUI.UI.Controls.Markdown with CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock 2. Add default markdown style config to align some configuration with the original one. (but still have some gap) 3. Add new configuration in pipeline to control the AOT enable/disable. ## PR Checklist - [x] **Closes:** #38279 - [x] **Communication:** I've discussed this with core contributors already. If the work hasn't been agreed, this work might be rejected - [x] **Tests:** Added/updated and all pass - [x] **Localization:** All end-user-facing strings can be localized - [ ] **Dev docs:** Added/updated - [ ] **New binaries:** Added on the required places - [ ] [JSON for signing](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ESRPSigning_core.json) for new binaries - [ ] [WXS for installer](https://github.com/microsoft/PowerToys/blob/main/installer/PowerToysSetup/Product.wxs) for new binaries and localization folder - [ ] [YML for CI pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/ci/templates/build-powertoys-steps.yml) for new test projects - [ ] [YML for signed pipeline](https://github.com/microsoft/PowerToys/blob/main/.pipelines/release.yml) - [ ] **Documentation updated:** If checked, please file a pull request on [our docs repo](https://github.com/MicrosoftDocs/windows-uwp/tree/docs/hub/powertoys) and link it here: #xxx ## Detailed Description of the Pull Request / Additional comments ## Validation Steps Performed --------- Co-authored-by: Yu Leng (from Dev Box) Co-authored-by: Mike Griese --- .github/actions/spell-check/expect.txt | 1 + .pipelines/v2/release.yml | 7 +++++- .pipelines/verifyDepsJsonLibraryVersions.ps1 | 2 +- Directory.Packages.props | 1 + NOTICE.md | 2 +- src/Common.Dotnet.AotCompatibility.props | 3 ++- .../ShortcutDialogContentControl.xaml | 4 ++-- .../ShortcutWithTextLabelControl.xaml | 4 ++-- .../ExtViews/ContentPage.xaml | 22 ++++++++++--------- .../Microsoft.CmdPal.UI.csproj | 10 ++++++++- .../Microsoft.CmdPal.UI/NativeMethods.txt | 1 - .../Microsoft.CmdPal.UI/Pages/ShellPage.xaml | 16 +++++++++----- .../PublishProfiles/win-arm64.pubxml | 4 ++-- .../Properties/PublishProfiles/win-x64.pubxml | 4 ++-- 14 files changed, 51 insertions(+), 30 deletions(-) diff --git a/.github/actions/spell-check/expect.txt b/.github/actions/spell-check/expect.txt index cd6e9b0a2b..3d22a06313 100644 --- a/.github/actions/spell-check/expect.txt +++ b/.github/actions/spell-check/expect.txt @@ -1383,6 +1383,7 @@ RIGHTSCROLLBAR riid RKey RNumber +Rns rop ROUNDSMALL ROWSETEXT diff --git a/.pipelines/v2/release.yml b/.pipelines/v2/release.yml index 227dd1cf71..18163e899a 100644 --- a/.pipelines/v2/release.yml +++ b/.pipelines/v2/release.yml @@ -38,6 +38,11 @@ parameters: displayName: "Build Using Visual Studio Preview" default: false + - name: enableAOT + type: boolean + displayName: "Enable AOT (Ahead-of-Time) Compilation for CmdPal" + default: true + name: $(BuildDefinitionName)_$(date:yyMM).$(date:dd)$(rev:rrr) variables: @@ -95,7 +100,7 @@ extends: useManagedIdentity: $(SigningUseManagedIdentity) clientId: $(SigningOriginalClientId) # Have msbuild use the release nuget config profile - additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" + additionalBuildOptions: /p:RestoreConfigFile="$(Build.SourcesDirectory)\.pipelines\release-nuget.config" /p:EnableCmdPalAOT=${{ parameters.enableAOT }} beforeBuildSteps: # Sets versions for all PowerToy created DLLs - pwsh: |- diff --git a/.pipelines/verifyDepsJsonLibraryVersions.ps1 b/.pipelines/verifyDepsJsonLibraryVersions.ps1 index 085e1e439a..6123316b5f 100644 --- a/.pipelines/verifyDepsJsonLibraryVersions.ps1 +++ b/.pipelines/verifyDepsJsonLibraryVersions.ps1 @@ -19,7 +19,7 @@ Get-ChildItem $targetDir -Recurse -Filter *.deps.json -Exclude *UITest*,MouseJum # Temporarily exclude All UI-Test, Fuzzer-Test projects because of Appium.WebDriver dependencies $depsJsonFullFileName = $_.FullName - if ($depsJsonFullFileName -like "*CmdPal*") { + if ($depsJsonFullFileName -like "*CmdPal*" -or $depsJsonFullFileName -like "*CommandPalette*") { return } diff --git a/Directory.Packages.props b/Directory.Packages.props index 5ebced2715..f29669d9e7 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -21,6 +21,7 @@ + diff --git a/NOTICE.md b/NOTICE.md index 2b1201bc50..b2232e4984 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1497,6 +1497,7 @@ SOFTWARE. - Appium.WebDriver 4.4.5 - Azure.AI.OpenAI 1.0.0-beta.17 - CommunityToolkit.Common 8.4.0 +- CommunityToolkit.Labs.WinUI.Controls.MarkdownTextBlock 0.1.250703-build.2173 - CommunityToolkit.Mvvm 8.4.0 - CommunityToolkit.WinUI.Animations 8.2.250402 - CommunityToolkit.WinUI.Collections 8.2.250402 @@ -1579,4 +1580,3 @@ SOFTWARE. - WinUIEx 2.2.0 - WPF-UI 3.0.5 - WyHash 1.0.5 - diff --git a/src/Common.Dotnet.AotCompatibility.props b/src/Common.Dotnet.AotCompatibility.props index 82988104dd..bebb88428c 100644 --- a/src/Common.Dotnet.AotCompatibility.props +++ b/src/Common.Dotnet.AotCompatibility.props @@ -7,6 +7,7 @@ 2 - IL2081;CsWinRT1028;$(WarningsNotAsErrors) + + IL2081;CsWinRT1028;CA1416;$(WarningsNotAsErrors) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml index 7932a27e91..56ae0bfca6 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ShortcutControl/ShortcutDialogContentControl.xaml @@ -4,8 +4,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:controls="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:tk7controls="using:CommunityToolkit.WinUI.UI.Controls" x:Name="ShortcutContentControl" mc:Ignorable="d"> @@ -66,7 +66,7 @@ IsTabStop="{Binding ElementName=ShortcutContentControl, Path=IsWarningAltGr, Mode=OneWay}" Severity="Warning" /> - @@ -36,7 +36,7 @@ - + + + - @@ -61,12 +66,9 @@ - diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj index 75e49fd695..44ef3483e2 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -1,5 +1,6 @@  + @@ -23,6 +24,13 @@ false + + true + false + false + true + + true @@ -71,7 +79,7 @@ - + diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt index 165933fe93..a432d9a808 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/NativeMethods.txt @@ -22,7 +22,6 @@ SetActiveWindow MonitorFromWindow GetMonitorInfo GetDpiForMonitor -CoAllowSetForegroundWindow WM_HOTKEY WM_NCLBUTTONDBLCLK diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml index 63877834e9..9df81e1513 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Pages/ShellPage.xaml @@ -9,8 +9,10 @@ xmlns:cpcontrols="using:Microsoft.CmdPal.UI.Controls" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:help="using:Microsoft.CmdPal.UI.Helpers" + xmlns:labToolkit="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock" + xmlns:markdownTextBlockRns="using:CommunityToolkit.WinUI.Controls.MarkdownTextBlockRns" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:toolkit="using:CommunityToolkit.WinUI.UI.Controls" + xmlns:toolkit="using:CommunityToolkit.WinUI.Controls" xmlns:ui="using:CommunityToolkit.WinUI" xmlns:viewModels="using:Microsoft.CmdPal.UI.ViewModels" Background="Transparent" @@ -144,6 +146,11 @@ + + @@ -406,14 +413,11 @@ TextWrapping="WrapWholeWords" Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" /> - False False True - False - False + True + False \ No newline at end of file diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml index 5ff16b291b..7f6d14d1ad 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-x64.pubxml @@ -13,7 +13,7 @@ https://go.microsoft.com/fwlink/?LinkID=208121. False False True - False - False + True + False \ No newline at end of file From 53bb471449ba0f6379a3d8997c5ad3505efe1146 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 15 Jul 2025 09:33:28 -0500 Subject: [PATCH 12/12] CmdPal: Add a viewmodel factory for pages (#40504) _targets #40482_ ref #40113 A smaller refactor, to be sure. This just moves the instantiation of PageViewModel objects out of the ShellViewModel, and into its own class. The idea being that other page types could be added, just by extending that factory (or implementing your own), and then also handling those new VMs in your ShellPage.xaml.cs equivalent. --- .../PageViewModel.cs | 16 +++++++++-- .../PageViewModelFactory.cs | 27 +++++++++++++++++++ .../ShellViewModel.cs | 19 +++---------- .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 1 + 4 files changed, 46 insertions(+), 17 deletions(-) create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModelFactory.cs diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs index 126efa83ca..681f7bc7f1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs @@ -249,7 +249,19 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext public interface IPageContext { - public void ShowException(Exception ex, string? extensionHint = null); + void ShowException(Exception ex, string? extensionHint = null); - public TaskScheduler Scheduler { get; } + TaskScheduler Scheduler { get; } +} + +public interface IPageViewModelFactoryService +{ + /// + /// Creates a new instance of the page view model for the given page type. + /// + /// The page for which to create the view model. + /// Indicates whether the page is not the top-level page. + /// The command palette host that will host the page (for status messages) + /// A new instance of the page view model. + PageViewModel? TryCreatePageViewModel(IPage page, bool nested, CommandPaletteHost host); } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModelFactory.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModelFactory.cs new file mode 100644 index 0000000000..83dbe51624 --- /dev/null +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModelFactory.cs @@ -0,0 +1,27 @@ +// 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; + +namespace Microsoft.CmdPal.UI.ViewModels; + +public class PageViewModelFactory : IPageViewModelFactoryService +{ + private readonly TaskScheduler _scheduler; + + public PageViewModelFactory(TaskScheduler scheduler) + { + _scheduler = scheduler; + } + + public PageViewModel? TryCreatePageViewModel(IPage page, bool nested, CommandPaletteHost host) + { + return page switch + { + IListPage listPage => new ListViewModel(listPage, _scheduler, host) { IsNested = nested }, + IContentPage contentPage => new ContentPageViewModel(contentPage, _scheduler, host), + _ => null, + }; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs index 112c2b67df..294b0021c7 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ShellViewModel.cs @@ -21,6 +21,7 @@ public partial class ShellViewModel : ObservableObject, { private readonly IRootPageService _rootPageService; private readonly TaskScheduler _scheduler; + private readonly IPageViewModelFactoryService _pageViewModelFactory; private readonly Lock _invokeLock = new(); private Task? _handleInvokeTask; @@ -65,8 +66,9 @@ public partial class ShellViewModel : ObservableObject, public bool IsNested { get => _isNested; } - public ShellViewModel(TaskScheduler scheduler, IRootPageService rootPageService) + public ShellViewModel(TaskScheduler scheduler, IRootPageService rootPageService, IPageViewModelFactoryService pageViewModelFactory) { + _pageViewModelFactory = pageViewModelFactory; _scheduler = scheduler; _rootPageService = rootPageService; _currentPage = new LoadingPageViewModel(null, _scheduler); @@ -252,7 +254,7 @@ public partial class ShellViewModel : ObservableObject, _isNested = !isMainPage; // Construct our ViewModel of the appropriate type and pass it the UI Thread context. - var pageViewModel = GetViewModelForPage(page, _isNested, host); + var pageViewModel = _pageViewModelFactory.TryCreatePageViewModel(page, _isNested, host); if (pageViewModel == null) { Logger.LogError($"Failed to create ViewModel for page {page.GetType().Name}"); @@ -397,19 +399,6 @@ public partial class ShellViewModel : ObservableObject, } } - private PageViewModel? GetViewModelForPage(IPage page, bool nested, CommandPaletteHost host) - { - return page switch - { - IListPage listPage => new ListViewModel(listPage, _scheduler, host) - { - IsNested = nested, - }, - IContentPage contentPage => new ContentPageViewModel(contentPage, _scheduler, host), - _ => null, - }; - } - public void SetActiveExtension(IExtensionWrapper? extension) { if (extension != _activeExtension) diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index a9f416483a..1007998f7c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -149,6 +149,7 @@ public partial class App : Application // ViewModels services.AddSingleton(); + services.AddSingleton(); return services.BuildServiceProvider(); }