CommandPalette use FuzzyPanel, Focus search textbox

This commit is contained in:
2023-06-30 16:25:48 +02:00
parent 71606a57f9
commit 4f7c576b87
13 changed files with 122 additions and 68 deletions

View File

@@ -9,6 +9,7 @@
<ItemGroup>
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.App.FuzzyPanel.Abstraction\FileTime.App.FuzzyPanel.Abstraction.csproj" />
</ItemGroup>
<ItemGroup>

View File

@@ -7,6 +7,7 @@ public interface ICommandPaletteService
{
IObservable<bool> ShowWindow { get; }
void OpenCommandPalette();
void CloseCommandPalette();
IReadOnlyList<ICommandPaletteEntry> GetCommands();
ICommandPaletteViewModel? CurrentModal { get; }
}

View File

@@ -1,14 +1,10 @@
using Avalonia.Input;
using FileTime.App.Core.ViewModels;
using FileTime.App.Core.ViewModels;
using FileTime.App.FuzzyPanel;
namespace FileTime.App.CommandPalette.ViewModels;
public interface ICommandPaletteViewModel : IModalViewModel
public interface ICommandPaletteViewModel : IFuzzyPanelViewModel<ICommandPaletteEntryViewModel>, IModalViewModel
{
IObservable<bool> ShowWindow { get; }
List<ICommandPaletteEntryViewModel> FilteredMatches { get; }
string SearchText { get; set; }
ICommandPaletteEntryViewModel SelectedItem { get; set; }
void Close();
void HandleKeyDown(KeyEventArgs keyEventArgs);
}

View File

@@ -9,10 +9,12 @@
<ItemGroup>
<ProjectReference Include="..\FileTime.App.CommandPalette.Abstractions\FileTime.App.CommandPalette.Abstractions.csproj" />
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.App.FuzzyPanel\FileTime.App.FuzzyPanel.csproj" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyInjection.Abstractions" Version="7.0.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Abstractions" Version="7.0.0" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.0.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

View File

@@ -28,6 +28,16 @@ public partial class CommandPaletteService : ICommandPaletteService
CurrentModal = _modalService.OpenModal<ICommandPaletteViewModel>();
}
public void CloseCommandPalette()
{
_showWindow.OnNext(false);
if (_currentModal is not null)
{
_modalService.CloseModal(_currentModal);
CurrentModal = null;
}
}
public IReadOnlyList<ICommandPaletteEntry> GetCommands() =>
_identifiableUserCommandService
.GetCommandIdentifiers()

View File

@@ -1,59 +1,85 @@
using Avalonia.Input;
using FileTime.App.CommandPalette.Services;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels;
using MvvmGen;
using FileTime.App.FuzzyPanel;
using Microsoft.Extensions.Logging;
namespace FileTime.App.CommandPalette.ViewModels;
[ViewModel]
[Inject(typeof(ICommandPaletteService), "_commandPaletteService")]
public partial class CommandPaletteViewModel : ICommandPaletteViewModel
public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryViewModel>, ICommandPaletteViewModel
{
private string _searchText;
[Property] private IObservable<bool> _showWindow;
[Property] private List<ICommandPaletteEntryViewModel> _filteredMatches;
[Property] private ICommandPaletteEntryViewModel? _selectedItem;
private readonly ICommandPaletteService _commandPaletteService;
private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
private readonly IUserCommandHandlerService _userCommandHandlerService;
private readonly ILogger<CommandPaletteViewModel> _logger;
string IModalViewModel.Name => "CommandPalette";
public string SearchText
{
get => _searchText;
set
{
if (_searchText == value) return;
_searchText = value;
OnPropertyChanged();
UpdateFilteredMatches();
}
}
public void Close() => throw new NotImplementedException();
public void HandleKeyDown(KeyEventArgs keyEventArgs) => throw new NotImplementedException();
partial void OnInitialize()
public CommandPaletteViewModel(
ICommandPaletteService commandPaletteService,
IIdentifiableUserCommandService identifiableUserCommandService,
IUserCommandHandlerService userCommandHandlerService,
ILogger<CommandPaletteViewModel> logger)
{
_commandPaletteService = commandPaletteService;
_identifiableUserCommandService = identifiableUserCommandService;
_userCommandHandlerService = userCommandHandlerService;
_logger = logger;
ShowWindow = _commandPaletteService.ShowWindow;
UpdateFilteredMatches();
UpdateFilteredMatchesInternal();
}
private void UpdateFilteredMatches()
{
public void Close() => _commandPaletteService.CloseCommandPalette();
public override void UpdateFilteredMatches() => UpdateFilteredMatchesInternal();
private void UpdateFilteredMatchesInternal() =>
FilteredMatches = _commandPaletteService
.GetCommands()
.Where(c =>
c.Title.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| c.Identifier.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
)
.Select(c =>
(ICommandPaletteEntryViewModel) new CommandPaletteEntryViewModel(c.Identifier, c.Title))
.Take(30) // TODO remove magic number
.OrderBy(c => c.Title)
.ToList();
if (SelectedItem != null && FilteredMatches.Contains(SelectedItem)) return;
public override async Task<bool> HandleKeyDown(KeyEventArgs keyEventArgs)
{
var handled = await base.HandleKeyDown(keyEventArgs);
if (handled)
{
return true;
}
SelectedItem = FilteredMatches.Count > 0
? FilteredMatches[0]
: null;
if (keyEventArgs.Key == Key.Escape)
{
Close();
return true;
}
if (keyEventArgs.Key == Key.Enter)
{
if (SelectedItem is null) return false;
var command = _identifiableUserCommandService.GetCommand(SelectedItem.Identifier);
if (command is null) return false;
try
{
await _userCommandHandlerService.HandleCommandAsync(command);
}
catch (Exception e)
{
_logger.LogError(e, "Unknown error while running command. {Command} {Error}", command.GetType().Name, e);
}
Close();
return true;
}
return false;
}
}

View File

@@ -5,6 +5,6 @@ namespace FileTime.App.Core.Services;
public interface IIdentifiableUserCommandService
{
void AddIdentifiableUserCommandFactory(string identifier, Func<IIdentifiableUserCommand> commandFactory);
IIdentifiableUserCommand GetCommand(string identifier);
IIdentifiableUserCommand? GetCommand(string identifier);
IReadOnlyCollection<string> GetCommandIdentifiers();
}

View File

@@ -9,8 +9,9 @@ public class IdentifiableUserCommandService : IIdentifiableUserCommandService
public void AddIdentifiableUserCommandFactory(string identifier, Func<IIdentifiableUserCommand> commandFactory)
=> _identifiableUserCommands.Add(identifier, commandFactory);
public IIdentifiableUserCommand GetCommand(string identifier)
public IIdentifiableUserCommand? GetCommand(string identifier)
{
//TODO: refactor to not throw an exception
if (!_identifiableUserCommands.ContainsKey(identifier))
throw new IndexOutOfRangeException($"No command factory is registered for command {identifier}");

View File

@@ -12,9 +12,9 @@
</UserControl.Styles>
<Grid RowDefinitions="Auto,*">
<TextBox
Focusable="True"
KeyDown="Search_OnKeyDown"
Text="{Binding SearchText, Mode=TwoWay}" />
Text="{Binding SearchText, Mode=TwoWay}"
x:Name="SearchTextBox" />
<ListBox
Classes="CommandPalette"
Grid.Row="1"
@@ -29,4 +29,4 @@
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</UserControl>
</UserControl>

View File

@@ -1,7 +1,9 @@
using Avalonia.Controls;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using FileTime.App.FrequencyNavigation.ViewModels;
using Avalonia.Threading;
using FileTime.App.CommandPalette.ViewModels;
namespace FileTime.GuiApp.Views;
@@ -10,17 +12,22 @@ public partial class CommandPalette : UserControl
public CommandPalette()
{
InitializeComponent();
PropertyChanged += CommandPalette_PropertyChanged;
}
private void InitializeComponent()
private async void CommandPalette_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
AvaloniaXamlLoader.Load(this);
if (e.Property.Name == nameof(IsVisible) && IsVisible)
{
await Task.Delay(10);
SearchTextBox.Focus();
}
}
private void Search_OnKeyDown(object? sender, KeyEventArgs e)
{
if (DataContext is not IFrequencyNavigationViewModel viewModel) return;
if (DataContext is not ICommandPaletteViewModel viewModel) return;
if (e.Key == Key.Escape)
{
viewModel.Close();
@@ -30,4 +37,9 @@ public partial class CommandPalette : UserControl
viewModel.HandleKeyDown(e);
}
}
public void Test()
{
;
}
}

View File

@@ -1,27 +1,27 @@
<UserControl
Background="{DynamicResource ContainerBackgroundColor}"
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="FileTime.GuiApp.Views.FrequencyNavigation"
x:CompileBindings="True"
x:DataType="viewModels:IFrequencyNavigationViewModel"
xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:viewModels="clr-namespace:FileTime.App.FrequencyNavigation.ViewModels;assembly=FileTime.App.FrequencyNavigation.Abstractions"
d:DesignHeight="450"
d:DesignWidth="800"
x:CompileBindings="True"
x:DataType="viewModels:IFrequencyNavigationViewModel"
Background="{DynamicResource ContainerBackgroundColor}"
mc:Ignorable="d">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<UserControl.Styles>
<StyleInclude Source="avares://FileTime.GuiApp/Resources/Styles.axaml" />
</UserControl.Styles>
<Grid RowDefinitions="Auto,*">
<TextBox
Focusable="True"
KeyDown="Search_OnKeyDown"
Text="{Binding SearchText, Mode=TwoWay}" />
Text="{Binding SearchText, Mode=TwoWay}"
x:Name="SearchTextBox" />
<ListBox
Grid.Row="1"
Classes="CommandPalette"
Grid.Row="1"
ItemsSource="{Binding FilteredMatches}"
SelectedItem="{Binding SelectedItem}">
<ListBox.ItemTemplate>

View File

@@ -1,6 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using FileTime.App.FrequencyNavigation.ViewModels;
namespace FileTime.GuiApp.Views;
@@ -10,11 +10,16 @@ public partial class FrequencyNavigation : UserControl
public FrequencyNavigation()
{
InitializeComponent();
PropertyChanged += FrequencyNavigation_PropertyChanged;
}
private void InitializeComponent()
private async void FrequencyNavigation_PropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
AvaloniaXamlLoader.Load(this);
if (e.Property.Name == nameof(IsVisible) && IsVisible)
{
await Task.Delay(100);
SearchTextBox.Focus();
}
}
private void Search_OnKeyDown(object? sender, KeyEventArgs e)

View File

@@ -672,7 +672,7 @@
IsVisible="{Binding ShowWindow^, FallbackValue=False}"
VerticalAlignment="Stretch">
<Grid Margin="100">
<local:FrequencyNavigation />
<local:FrequencyNavigation IsVisible="{Binding ShowWindow^, FallbackValue=False}" />
</Grid>
</Border>
@@ -683,7 +683,7 @@
IsVisible="{Binding ShowWindow^, FallbackValue=False}"
VerticalAlignment="Stretch">
<Grid Margin="100">
<local:CommandPalette />
<local:CommandPalette IsVisible="{Binding ShowWindow^, FallbackValue=False}" />
</Grid>
</Border>
</Grid>