From c208d4b842821b016f459998e5b943eff964205a Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 10 Dec 2024 11:58:54 -0600 Subject: [PATCH 1/2] Terminate extensions on exit (again) --- .../cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs | 15 ++++++++++++++- 1 file changed, 14 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 d8a61ce2bf..c375898dca 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/MainWindow.xaml.cs @@ -3,7 +3,10 @@ // See the LICENSE file in the project root for more information. using CommunityToolkit.Mvvm.Messaging; +using Microsoft.CmdPal.Common.Extensions; +using Microsoft.CmdPal.Common.Services; using Microsoft.CmdPal.UI.ViewModels.Messages; +using Microsoft.Extensions.DependencyInjection; using Microsoft.UI.Composition; using Microsoft.UI.Composition.SystemBackdrops; using Microsoft.UI.Input; @@ -130,7 +133,17 @@ public sealed partial class MainWindow : Window, public void Receive(QuitMessage message) => Close(); - private void MainWindow_Closed(object sender, WindowEventArgs args) => DisposeAcrylic(); + private void MainWindow_Closed(object sender, WindowEventArgs args) + { + var serviceProvider = App.Current.Services; + var extensionService = serviceProvider.GetService()!; + extensionService.SignalStopExtensionsAsync(); + + // WinUI bug is causing a crash on shutdown when FailFastOnErrors is set to true (#51773592). + // Workaround by turning it off before shutdown. + App.Current.DebugSettings.FailFastOnErrors = false; + DisposeAcrylic(); + } // Updates our window s.t. the top of the window is dragable. private void UpdateRegionsForCustomTitleBar() From 6a7a2442d91f4163f0f82df892a1d86bbb0e8807 Mon Sep 17 00:00:00 2001 From: Mike Griese Date: Tue, 10 Dec 2024 17:34:55 -0600 Subject: [PATCH 2/2] Load icons again (#208) This adds a magic helper to load icons for us. Any time you want an icon, just do this: ```xaml ``` And that'll magically give us a border filled with the icon, and updating with the binding. I believe it'll also work with `IRandomAccessStreamReference`s, but I didn't actually test that with #151 yet. I didn't actually implement the "caching" bit of this yet. That'll involve doing some locking per-key inside the factory and I didn't want to futz with that in this initial PR to restore icons --------- Co-authored-by: Mike Griese --- .../ActionBarViewModel.cs | 18 +++-- .../CommandItemViewModel.cs | 11 ++- .../DetailsPaneViewModel.cs | 17 ----- .../DetailsViewModel.cs | 7 +- .../PageViewModel.cs | 7 ++ .../TagViewModel.cs | 7 +- .../cmdpal/Microsoft.CmdPal.UI/App.xaml.cs | 1 - .../Controls/ActionBar.xaml | 71 +++++++++++++------ .../Controls/ActionBar.xaml.cs | 2 + .../ExtViews/IconCacheService.xaml.cs | 66 +++++++++++++++++ .../ExtViews/ListPage.xaml | 46 ++++++------ .../LoadIconBehavior.xaml.cs | 62 ++++++++++++++++ .../Microsoft.CmdPal.UI.csproj | 7 ++ .../cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml | 14 ++-- .../Microsoft.CmdPal.UI/ShellPage.xaml.cs | 1 + 15 files changed, 261 insertions(+), 76 deletions(-) delete mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsPaneViewModel.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/IconCacheService.xaml.cs create mode 100644 src/modules/cmdpal/Microsoft.CmdPal.UI/LoadIconBehavior.xaml.cs diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs index a23c60cc95..2681f0f863 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/ActionBarViewModel.cs @@ -24,14 +24,20 @@ public partial class ActionBarViewModel : ObservableObject, } [ObservableProperty] - public partial string PrimaryActionName { get; set; } = string.Empty; + public partial CommandItemViewModel? PrimaryAction { get; set; } [ObservableProperty] - public partial string SecondaryActionName { get; set; } = string.Empty; + [NotifyPropertyChangedFor(nameof(HasSecondaryCommand))] + public partial CommandItemViewModel? SecondaryAction { get; set; } + + public bool HasSecondaryCommand => SecondaryAction != null; [ObservableProperty] public partial bool ShouldShowContextMenu { get; set; } = false; + [ObservableProperty] + public partial PageViewModel? CurrentPage { get; set; } + [ObservableProperty] public partial ObservableCollection ContextActions { get; set; } = []; @@ -46,8 +52,8 @@ public partial class ActionBarViewModel : ObservableObject, { if (value != null) { - PrimaryActionName = value.Name; - SecondaryActionName = value.SecondaryCommandName; + PrimaryAction = value; + SecondaryAction = value.SecondaryCommand; if (value.MoreCommands.Count > 1) { @@ -61,8 +67,8 @@ public partial class ActionBarViewModel : ObservableObject, } else { - PrimaryActionName = string.Empty; - SecondaryActionName = string.Empty; + PrimaryAction = null; + SecondaryAction = null; ShouldShowContextMenu = false; } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs index a1d6b46ae1..6bb0a56586 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -26,7 +26,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel public string Subtitle { get; private set; } = string.Empty; - public string IconUri { get; private set; } = string.Empty; + public IconDataType Icon { get; private set; } = new(string.Empty); public ExtensionObject Command { get; private set; } = new(null); @@ -36,6 +36,8 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel public string SecondaryCommandName => HasMoreCommands ? MoreCommands[0].Name : string.Empty; + public CommandItemViewModel? SecondaryCommand => HasMoreCommands ? MoreCommands[0] : null; + public List AllCommands { get @@ -49,7 +51,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel Name = Name, Title = Name, Subtitle = Subtitle, - IconUri = IconUri, + Icon = Icon, // TODO this probably should just be a CommandContextItemViewModel(CommandItemViewModel) ctor, or a copy ctor or whatever }; @@ -79,7 +81,7 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel Name = model.Command?.Name ?? string.Empty; Title = model.Title; Subtitle = model.Subtitle; - IconUri = model.Icon.Icon; + Icon = model.Icon; MoreCommands = model.MoreCommands .Where(contextItem => contextItem is ICommandContextItem) .Select(contextItem => (contextItem as ICommandContextItem)!) @@ -129,6 +131,9 @@ public partial class CommandItemViewModel : ExtensionObjectViewModel case nameof(Subtitle): this.Subtitle = model.Subtitle; break; + case nameof(Icon): + this.Icon = model.Icon; + break; // TODO! Icon // TODO! MoreCommands array, which needs to also raise HasMoreCommands diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsPaneViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsPaneViewModel.cs deleted file mode 100644 index 29fcff4ef4..0000000000 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsPaneViewModel.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) Microsoft Corporation -// The Microsoft Corporation licenses this file to you under the MIT license. -// See the LICENSE file in the project root for more information. - -using CommunityToolkit.Mvvm.ComponentModel; - -namespace Microsoft.CmdPal.UI.ViewModels; - -public partial class DetailsPaneViewModel : ObservableObject -{ - [ObservableProperty] - public partial DetailsViewModel? Details { get; set; } - - public DetailsPaneViewModel() - { - } -} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs index 4257be9ab8..386899cb2c 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/DetailsViewModel.cs @@ -13,8 +13,10 @@ public partial class DetailsViewModel(IDetails _details, TaskScheduler Scheduler // Remember - "observable" properties from the model (via PropChanged) // cannot be marked [ObservableProperty] + public IconDataType HeroImage { get; private set; } = new(string.Empty); + + public bool HasHeroImage => !string.IsNullOrEmpty(HeroImage.Icon) || HeroImage.Data != null; - // TODO: Icon // TODO: Metadata is an array of IDetailsElement, // where IDetailsElement = {IDetailsTags, IDetailsLink, IDetailsSeparator} public string Title { get; private set; } = string.Empty; @@ -31,9 +33,12 @@ public partial class DetailsViewModel(IDetails _details, TaskScheduler Scheduler Title = model.Title; Body = model.Body; + HeroImage = model.HeroImage; UpdateProperty(nameof(Title)); UpdateProperty(nameof(Body)); + UpdateProperty(nameof(HeroImage)); + UpdateProperty(nameof(HasHeroImage)); } protected void UpdateProperty(string propertyName) => Task.Factory.StartNew(() => { OnPropertyChanged(propertyName); }, CancellationToken.None, TaskCreationOptions.None, Scheduler); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs index 3da00a85e4..ef22c082a1 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs @@ -37,6 +37,8 @@ public partial class PageViewModel : ExtensionObjectViewModel public bool IsLoading { get; private set; } = true; + public IconDataType Icon { get; private set; } = new(string.Empty); + public PageViewModel(IPage model, TaskScheduler scheduler) { _pageModel = new(model); @@ -74,10 +76,12 @@ public partial class PageViewModel : ExtensionObjectViewModel Name = page.Name; IsLoading = page.IsLoading; + Icon = page.Icon; // Let the UI know about our initial properties too. UpdateProperty(nameof(Name)); UpdateProperty(nameof(IsLoading)); + UpdateProperty(nameof(Icon)); page.PropChanged += Model_PropChanged; } @@ -119,6 +123,9 @@ public partial class PageViewModel : ExtensionObjectViewModel case nameof(IsLoading): this.IsLoading = model.IsLoading; break; + case nameof(Icon): + this.Icon = model.Icon; + break; } UpdateProperty(propertyName); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TagViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TagViewModel.cs index ec5b2da7ee..bf71521a0b 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TagViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/TagViewModel.cs @@ -19,7 +19,10 @@ public partial class TagViewModel(ITag _tag, TaskScheduler Scheduler) : Extensio public OptionalColor Color { get; private set; } - // TODO Icon + public IconDataType Icon { get; private set; } = new(string.Empty); + + public bool HasIcon => !string.IsNullOrEmpty(Icon.Icon); + public ExtensionObject Command { get; private set; } = new(null); public override void InitializeProperties() @@ -34,10 +37,12 @@ public partial class TagViewModel(ITag _tag, TaskScheduler Scheduler) : Extensio Text = model.Text; Color = model.Color; Tooltip = model.ToolTip; + Icon = model.Icon; UpdateProperty(nameof(Text)); UpdateProperty(nameof(Color)); UpdateProperty(nameof(Tooltip)); + UpdateProperty(nameof(Icon)); } protected void UpdateProperty(string propertyName) => Task.Factory.StartNew(() => { OnPropertyChanged(propertyName); }, CancellationToken.None, TaskCreationOptions.None, Scheduler); diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs index 30f7f5f829..ae53039251 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/App.xaml.cs @@ -12,7 +12,6 @@ using Microsoft.CmdPal.Ext.WindowsServices; using Microsoft.CmdPal.Ext.WindowsSettings; using Microsoft.CmdPal.Ext.WindowsTerminal; using Microsoft.CmdPal.Extensions; -using Microsoft.CmdPal.UI.Pages; using Microsoft.CmdPal.UI.ViewModels; using Microsoft.CmdPal.UI.ViewModels.BuiltinCommands; using Microsoft.CmdPal.UI.ViewModels.Models; diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml index 701b56222a..037179f1e8 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Controls/ActionBar.xaml @@ -5,10 +5,12 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:converters="using:CommunityToolkit.WinUI.Converters" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" + xmlns:cmdpalUI="using:Microsoft.CmdPal.UI" xmlns:local="using:Microsoft.CmdPal.UI.Controls" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:ui="using:CommunityToolkit.WinUI" xmlns:viewmodels="using:Microsoft.CmdPal.UI.ViewModels" + xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" Background="Transparent" mc:Ignorable="d"> @@ -24,19 +26,21 @@ - + - - - - - - + Width="16" + Height="16" + Margin="0,0,0,0"> + + + + + + @@ -51,13 +55,17 @@ - - + + + + + + + + Visibility="{x:Bind ViewModel.PrimaryAction.Name, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}"> - + + + + + + + + + Text="{x:Bind ViewModel.PrimaryAction.Name, Mode=OneWay}" /> + Visibility="{x:Bind ViewModel.HasSecondaryCommand, Mode=OneWay}"> - + + + + + + + + + + Text="{x:Bind ViewModel.SecondaryAction.Name, Mode=OneWay}" /> GetIconSource(IconDataType icon) => + + // todo: actually implement a cache of some sort + IconToSource(icon); + + private async Task IconToSource(IconDataType icon) + { + if (!string.IsNullOrEmpty(icon.Icon)) + { + var source = IconPathConverter.IconSourceMUX(icon.Icon, false); + + return source; + } + else if (icon.Data != null) + { + return await StreamToIconSource(icon.Data); + } + + return null; + } + + private async Task StreamToIconSource(IRandomAccessStreamReference iconStreamRef) + { + if (iconStreamRef == null) + { + return null; + } + + var bitmap = await IconStreamToBitmapImageAsync(iconStreamRef); + var icon = new ImageIconSource() { ImageSource = bitmap }; + return icon; + } + + private async Task IconStreamToBitmapImageAsync(IRandomAccessStreamReference iconStreamRef) + { + // Return the bitmap image via TaskCompletionSource. Using WCT's EnqueueAsync does not suffice here, since if + // we're already on the thread of the DispatcherQueue then it just directly calls the function, with no async involved. + var completionSource = new TaskCompletionSource(); + dispatcherQueue.TryEnqueue(async () => + { + using var bitmapStream = await iconStreamRef.OpenReadAsync(); + var itemImage = new BitmapImage(); + await itemImage.SetSourceAsync(bitmapStream); + completionSource.TrySetResult(itemImage); + }); + + var bitmapImage = await completionSource.Task; + + return bitmapImage; + } +} diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml index 91fa163ea9..ccf38946ca 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ExtViews/ListPage.xaml @@ -34,26 +34,29 @@ - - - - - - + Visibility="{x:Bind HasIcon, Mode=OneWay}"> + + + + + + + + @@ -68,14 +71,17 @@ - - - + + + + + + + (IconDataType)GetValue(SourceProperty); + set + { + SetValue(SourceProperty, value); + OnSourcePropertyChanged(); + } + } + + // Using a DependencyProperty as the backing store for Source. This enables animation, styling, binding, etc... + public static readonly DependencyProperty SourceProperty = + DependencyProperty.Register(nameof(Source), typeof(IconDataType), typeof(LoadIconBehavior), new PropertyMetadata(new IconDataType(string.Empty))); + + public DependencyObject? AssociatedObject { get; private set; } + + public void Attach(DependencyObject associatedObject) => AssociatedObject = associatedObject; + + public void Detach() => AssociatedObject = null; + + public async void OnSourcePropertyChanged() + { + var icoSource = await IconService.GetIconSource(Source ?? new(string.Empty)); + + if (AssociatedObject is Border border) + { + if (icoSource is FontIconSource fontIco) + { + fontIco.FontSize = border.Width; + + // For inexplicable reasons, FontIconSource.CreateIconElement + // doesn't work, so do it ourselves + IconSourceElement elem = new() + { + IconSource = fontIco, + }; + border.Child = elem; + } + else + { + var icoElement = icoSource?.CreateIconElement(); + border.Child = icoElement; + } + } + } +} 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 f848997597..e841e4193f 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/Microsoft.CmdPal.UI.csproj @@ -14,6 +14,13 @@ false + + + + Microsoft.Terminal.UI + $(OutDir) + + DISABLE_XAML_GENERATED_MAIN diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml index d0d0d07d67..7a1ada4998 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI/ShellPage.xaml @@ -9,6 +9,7 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:labs="using:CommunityToolkit.Labs.WinUI.MarkdownTextBlock" xmlns:converters="using:CommunityToolkit.WinUI.Converters" + xmlns:Interactivity="using:Microsoft.Xaml.Interactivity" Background="Transparent" mc:Ignorable="d"> @@ -84,11 +85,14 @@ - + + + + +