Merge remote-tracking branch 'origin/main' into dev/migrie/f/error-context-pr

This commit is contained in:
Mike Griese
2024-12-12 05:48:09 -06:00
16 changed files with 275 additions and 77 deletions

View File

@@ -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<CommandContextItemViewModel> 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;
}
}

View File

@@ -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<ICommand> 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<CommandContextItemViewModel> 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

View File

@@ -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()
{
}
}

View File

@@ -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));
}
}

View File

@@ -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);

View File

@@ -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<ICommand> 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));
}
}

View File

@@ -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;

View File

@@ -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 @@
<ListViewItem KeyDown="ActionListViewItem_KeyDown" Tapped="ActionListViewItem_Tapped">
<Grid ColumnSpacing="8">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="24" />
<ColumnDefinition Width="20" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Viewbox Width="16" Height="16">
<!-- TODO bind to icon -->
<ContentControl
<Border x:Name="IconBorder"
Grid.Column="0"
Width="24"
Height="24">
<!--<SymbolIcon Symbol="Emoji" />-->
</ContentControl>
</Viewbox>
Width="16"
Height="16"
Margin="0,0,0,0">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<TextBlock Grid.Column="1" Text="{x:Bind Title}" />
</Grid>
</ListViewItem>
@@ -51,13 +55,17 @@
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- TO DO: Placeholder, needs to be replaced with extension info -->
<Border
Width="20"
Height="20"
Margin="12,0,0,0"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="{StaticResource ControlCornerRadius}" />
<Border x:Name="IconBorder"
Width="20"
Height="20"
Margin="12,0,0,0"
CornerRadius="{StaticResource ControlCornerRadius}">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind ViewModel.CurrentPage.Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<TextBlock
Grid.Column="1"
@@ -73,15 +81,24 @@
<Button
Height="40"
Padding="8,4,8,4"
Visibility="{x:Bind ViewModel.PrimaryActionName, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
Visibility="{x:Bind ViewModel.PrimaryAction.Name, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="&#xEA3A;" />
<!-- <FontIcon Glyph="&#xEA3A;" /> -->
<Border Width="16"
Height="16"
Margin="4,4,4,4">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind ViewModel.PrimaryAction.Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<StackPanel Orientation="Vertical" Spacing="2">
<TextBlock
FontSize="12"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.PrimaryActionName, Mode=OneWay}" />
Text="{x:Bind ViewModel.PrimaryAction.Name, Mode=OneWay}" />
<FontIcon
HorizontalAlignment="Left"
VerticalAlignment="Center"
@@ -94,14 +111,24 @@
<Button
Height="40"
Padding="8,4,8,4"
Visibility="{x:Bind ViewModel.SecondaryActionName, Converter={StaticResource StringNotEmptyToVisibilityConverter}, Mode=OneWay}">
Visibility="{x:Bind ViewModel.HasSecondaryCommand, Mode=OneWay}">
<StackPanel Orientation="Horizontal" Spacing="8">
<FontIcon Glyph="&#xEA3A;" />
<!-- <FontIcon Glyph="&#xEA3A;" /> -->
<Border Width="16"
Height="16"
Margin="4,4,4,4">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<cmdpalUI:LoadIconBehavior Source="{x:Bind ViewModel.SecondaryAction.Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<StackPanel Orientation="Vertical" Spacing="1">
<TextBlock
FontSize="12"
Style="{StaticResource CaptionTextBlockStyle}"
Text="{x:Bind ViewModel.SecondaryActionName, Mode=OneWay}" />
Text="{x:Bind ViewModel.SecondaryAction.Name, Mode=OneWay}" />
<TextBlock
FontSize="10"
Foreground="{ThemeResource TextFillColorSecondaryBrush}"

View File

@@ -29,11 +29,13 @@ public sealed partial class ActionBar : UserControl, ICurrentPageAware
this.InitializeComponent();
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-agressively")]
private void ActionListViewItem_KeyDown(object sender, KeyRoutedEventArgs e)
{
// TODO
}
[System.Diagnostics.CodeAnalysis.SuppressMessage("CodeQuality", "IDE0051:Remove unused private members", Justification = "VS has a tendency to delete XAML bound methods over-agressively")]
private void ActionListViewItem_Tapped(object sender, TappedRoutedEventArgs e)
{
MoreCommandsButton.Flyout.Hide();

View File

@@ -0,0 +1,66 @@
// 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.Extensions;
using Microsoft.Terminal.UI;
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml.Controls;
using Microsoft.UI.Xaml.Media.Imaging;
using Windows.Storage.Streams;
namespace Microsoft.CmdPal.UI.ExtViews;
public sealed class IconCacheService(DispatcherQueue dispatcherQueue)
{
public Task<IconSource?> GetIconSource(IconDataType icon) =>
// todo: actually implement a cache of some sort
IconToSource(icon);
private async Task<IconSource?> 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<IconSource?> StreamToIconSource(IRandomAccessStreamReference iconStreamRef)
{
if (iconStreamRef == null)
{
return null;
}
var bitmap = await IconStreamToBitmapImageAsync(iconStreamRef);
var icon = new ImageIconSource() { ImageSource = bitmap };
return icon;
}
private async Task<BitmapImage> 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<BitmapImage>();
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;
}
}

View File

@@ -34,26 +34,29 @@
<DataTemplate x:Key="TagTemplate" x:DataType="viewmodels:TagViewModel">
<!-- TODO: Actually colorize the tags again -->
<Border
<StackPanel
Padding="4,2,4,2"
VerticalAlignment="Center"
BorderBrush="{ThemeResource TextBoxBorderThemeBrush}"
BorderThickness="1"
Orientation="Horizontal"
CornerRadius="4">
<StackPanel Orientation="Horizontal">
<!-- -TODO: Icon
<ContentControl
<Border x:Name="IconBorder"
Width="12"
Height="12"
Margin="0,0,4,0"
Content="{x:Bind IcoElement, Mode=OneWay}"
Visibility="{x:Bind HasIcon, Mode=OneWay}" /> -->
<TextBlock
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind Text, Mode=OneWay}" />
</StackPanel>
</Border>
Visibility="{x:Bind HasIcon, Mode=OneWay}">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<local:LoadIconBehavior Source="{x:Bind Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<TextBlock
VerticalAlignment="Center"
FontSize="12"
Text="{x:Bind Text, Mode=OneWay}" />
</StackPanel>
</DataTemplate>
<!-- https://learn.microsoft.com/windows/apps/design/controls/itemsview#specify-the-look-of-the-items -->
@@ -68,14 +71,17 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Viewbox
Width="20"
Height="20"
VerticalAlignment="Center">
<!--TODO: Icon
<ContentControl Content="{x:Bind IcoElement, Mode=OneWay}" />
-->
</Viewbox>
<Border x:Name="IconBorder"
Grid.Column="0"
Width="20"
Height="20"
Margin="4,0,4,0">
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<local:LoadIconBehavior Source="{x:Bind Icon, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<StackPanel
Grid.Column="1"

View File

@@ -0,0 +1,62 @@
// 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.Extensions;
using Microsoft.CmdPal.UI.ExtViews;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
using Microsoft.Xaml.Interactivity;
namespace Microsoft.CmdPal.UI;
public partial class LoadIconBehavior : DependencyObject, IBehavior
{
private static readonly IconCacheService IconService = new(Microsoft.UI.Dispatching.DispatcherQueue.GetForCurrentThread());
public IconDataType Source
{
get => (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;
}
}
}
}

View File

@@ -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<IExtensionService>()!;
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()

View File

@@ -14,6 +14,13 @@
<AppendRuntimeIdentifierToOutputPath>false</AppendRuntimeIdentifierToOutputPath>
</PropertyGroup>
<PropertyGroup>
<!-- This lets us actually reference types from Microsoft.Terminal.UI -->
<CsWinRTIncludes>Microsoft.Terminal.UI</CsWinRTIncludes>
<CsWinRTGeneratedFilesDir>$(OutDir)</CsWinRTGeneratedFilesDir>
</PropertyGroup>
<PropertyGroup>
<!-- This disables the auto-generated main, so we can be single-instanced -->
<DefineConstants>DISABLE_XAML_GENERATED_MAIN</DefineConstants>

View File

@@ -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 @@
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border
Width="64"
Height="64"
Background="{ThemeResource AccentFillColorDefaultBrush}"
CornerRadius="{StaticResource ControlCornerRadius}">
<Border x:Name="HeroImageBorder"
Width="64"
Height="64"
Visibility="{x:Bind ViewModel.Details.HasHeroImage, Mode=OneWay}" >
<!-- LoadIconBehavior will magically fill this border up with an icon -->
<Interactivity:Interaction.Behaviors>
<local:LoadIconBehavior Source="{x:Bind ViewModel.Details.HeroImage, Mode=OneWay}"/>
</Interactivity:Interaction.Behaviors>
</Border>
<TextBlock

View File

@@ -54,6 +54,7 @@ public sealed partial class ShellPage :
if (RootFrame.CanGoBack)
{
RootFrame.GoBack();
HideDetails();
RootFrame.ForwardStack.Clear();
SearchBox.Focus(Microsoft.UI.Xaml.FocusState.Programmatic);
}