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 7cee21e2d4..d09281b5a6 100644 --- a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs +++ b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/CommandItemViewModel.cs @@ -24,7 +24,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); @@ -34,6 +34,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 @@ -47,7 +49,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 }; @@ -77,7 +79,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)!) @@ -127,6 +129,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 d931f65834..29851c2e85 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, IPageContext context) : // 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,8 +33,11 @@ public partial class DetailsViewModel(IDetails _details, IPageContext context) : Title = model.Title; Body = model.Body; + HeroImage = model.HeroImage; UpdateProperty(nameof(Title)); UpdateProperty(nameof(Body)); + UpdateProperty(nameof(HeroImage)); + UpdateProperty(nameof(HasHeroImage)); } } diff --git a/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs b/src/modules/cmdpal/Microsoft.CmdPal.UI.ViewModels/PageViewModel.cs index 2774ced27c..5cb88d51a2 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, IPageContext public bool IsLoading { get; private set; } = true; + public IconDataType Icon { get; private set; } = new(string.Empty); + public PageViewModel(IPage model, TaskScheduler scheduler) : base(null) { @@ -78,11 +80,13 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext Name = page.Name; IsLoading = page.IsLoading; Title = page.Title; + Icon = page.Icon; // Let the UI know about our initial properties too. UpdateProperty(nameof(Name)); UpdateProperty(nameof(Title)); UpdateProperty(nameof(IsLoading)); + UpdateProperty(nameof(Icon)); page.PropChanged += Model_PropChanged; } @@ -128,6 +132,9 @@ public partial class PageViewModel : ExtensionObjectViewModel, IPageContext 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 8e59f18b2d..a2ef741f0d 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, IPageContext context) : ExtensionOb 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,9 +37,11 @@ public partial class TagViewModel(ITag _tag, IPageContext context) : ExtensionOb Text = model.Text; Color = model.Color; Tooltip = model.ToolTip; + Icon = model.Icon; UpdateProperty(nameof(Text)); UpdateProperty(nameof(Color)); UpdateProperty(nameof(Tooltip)); + UpdateProperty(nameof(Icon)); } } 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 aeacdf4bc4..5be5b27fb5 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/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() 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 @@ - + + + + +