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/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 diff --git a/nuget.config b/nuget.config index e6a17ffdfe..51f9b3b3f7 100644 --- a/nuget.config +++ b/nuget.config @@ -2,10 +2,10 @@ - + - + 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.ViewModels/TopLevelCommandManager.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs index 9c5718e17e..b4f6542c93 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TopLevelCommandManager.cs @@ -105,14 +105,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); } } 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 b03a6ade7b..761c69d724 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/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 @@ - + + + - @@ -62,12 +67,9 @@ - diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs index 2f01873b6b..3d2c9b8c47 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -108,7 +108,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", () => @@ -176,6 +176,8 @@ public sealed partial class MainWindow : WindowEx, private void UpdateAcrylic() { + _acrylicController?.RemoveAllSystemBackdropTargets(); + _acrylicController = GetAcrylicConfig(Content); // Enable the system backdrop. 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 c250662d1e..b35b70496e 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" xmlns:coreViewModels="using:Microsoft.CmdPal.Core.ViewModels" @@ -145,6 +147,11 @@ + + @@ -407,14 +414,11 @@ TextWrapping="WrapWholeWords" Visibility="{x:Bind ViewModel.Details.Title, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}" /> - (this); WeakReferenceMessenger.Default.Register(this); + AddHandler(PreviewKeyDownEvent, new KeyEventHandler(ShellPage_OnPreviewKeyDown), true); AddHandler(PointerPressedEvent, new PointerEventHandler(ShellPage_OnPointerPressed), true); RootFrame.Navigate(typeof(LoadingPage), ViewModel); @@ -449,6 +451,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 diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml index b95afb7240..adf22122d9 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.pubxml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Properties/PublishProfiles/win-arm64.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 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 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"); + } +} 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; 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]; } 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; 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), ];