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> <ItemGroup>
<ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" /> <ProjectReference Include="..\FileTime.App.Core.Abstraction\FileTime.App.Core.Abstraction.csproj" />
<ProjectReference Include="..\FileTime.App.FuzzyPanel.Abstraction\FileTime.App.FuzzyPanel.Abstraction.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>

View File

@@ -7,6 +7,7 @@ public interface ICommandPaletteService
{ {
IObservable<bool> ShowWindow { get; } IObservable<bool> ShowWindow { get; }
void OpenCommandPalette(); void OpenCommandPalette();
void CloseCommandPalette();
IReadOnlyList<ICommandPaletteEntry> GetCommands(); IReadOnlyList<ICommandPaletteEntry> GetCommands();
ICommandPaletteViewModel? CurrentModal { get; } 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; namespace FileTime.App.CommandPalette.ViewModels;
public interface ICommandPaletteViewModel : IModalViewModel public interface ICommandPaletteViewModel : IFuzzyPanelViewModel<ICommandPaletteEntryViewModel>, IModalViewModel
{ {
IObservable<bool> ShowWindow { get; } IObservable<bool> ShowWindow { get; }
List<ICommandPaletteEntryViewModel> FilteredMatches { get; }
string SearchText { get; set; }
ICommandPaletteEntryViewModel SelectedItem { get; set; }
void Close(); void Close();
void HandleKeyDown(KeyEventArgs keyEventArgs);
} }

View File

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

View File

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

View File

@@ -1,59 +1,85 @@
using Avalonia.Input; using Avalonia.Input;
using FileTime.App.CommandPalette.Services; using FileTime.App.CommandPalette.Services;
using FileTime.App.Core.Services;
using FileTime.App.Core.ViewModels; using FileTime.App.Core.ViewModels;
using MvvmGen; using FileTime.App.FuzzyPanel;
using Microsoft.Extensions.Logging;
namespace FileTime.App.CommandPalette.ViewModels; namespace FileTime.App.CommandPalette.ViewModels;
[ViewModel] public class CommandPaletteViewModel : FuzzyPanelViewModel<ICommandPaletteEntryViewModel>, ICommandPaletteViewModel
[Inject(typeof(ICommandPaletteService), "_commandPaletteService")]
public partial class CommandPaletteViewModel : ICommandPaletteViewModel
{ {
private string _searchText; private readonly ICommandPaletteService _commandPaletteService;
private readonly IIdentifiableUserCommandService _identifiableUserCommandService;
[Property] private IObservable<bool> _showWindow; private readonly IUserCommandHandlerService _userCommandHandlerService;
[Property] private List<ICommandPaletteEntryViewModel> _filteredMatches; private readonly ILogger<CommandPaletteViewModel> _logger;
[Property] private ICommandPaletteEntryViewModel? _selectedItem;
string IModalViewModel.Name => "CommandPalette"; string IModalViewModel.Name => "CommandPalette";
public string SearchText public CommandPaletteViewModel(
{ ICommandPaletteService commandPaletteService,
get => _searchText; IIdentifiableUserCommandService identifiableUserCommandService,
set IUserCommandHandlerService userCommandHandlerService,
{ ILogger<CommandPaletteViewModel> logger)
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()
{ {
_commandPaletteService = commandPaletteService;
_identifiableUserCommandService = identifiableUserCommandService;
_userCommandHandlerService = userCommandHandlerService;
_logger = logger;
ShowWindow = _commandPaletteService.ShowWindow; ShowWindow = _commandPaletteService.ShowWindow;
UpdateFilteredMatches(); UpdateFilteredMatchesInternal();
} }
private void UpdateFilteredMatches() public void Close() => _commandPaletteService.CloseCommandPalette();
{
public override void UpdateFilteredMatches() => UpdateFilteredMatchesInternal();
private void UpdateFilteredMatchesInternal() =>
FilteredMatches = _commandPaletteService FilteredMatches = _commandPaletteService
.GetCommands() .GetCommands()
.Where(c =>
c.Title.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
|| c.Identifier.Contains(SearchText, StringComparison.OrdinalIgnoreCase)
)
.Select(c => .Select(c =>
(ICommandPaletteEntryViewModel) new CommandPaletteEntryViewModel(c.Identifier, c.Title)) (ICommandPaletteEntryViewModel) new CommandPaletteEntryViewModel(c.Identifier, c.Title))
.Take(30) // TODO remove magic number .Take(30) // TODO remove magic number
.OrderBy(c => c.Title) .OrderBy(c => c.Title)
.ToList(); .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 if (keyEventArgs.Key == Key.Escape)
? FilteredMatches[0] {
: null; 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 public interface IIdentifiableUserCommandService
{ {
void AddIdentifiableUserCommandFactory(string identifier, Func<IIdentifiableUserCommand> commandFactory); void AddIdentifiableUserCommandFactory(string identifier, Func<IIdentifiableUserCommand> commandFactory);
IIdentifiableUserCommand GetCommand(string identifier); IIdentifiableUserCommand? GetCommand(string identifier);
IReadOnlyCollection<string> GetCommandIdentifiers(); IReadOnlyCollection<string> GetCommandIdentifiers();
} }

View File

@@ -9,8 +9,9 @@ public class IdentifiableUserCommandService : IIdentifiableUserCommandService
public void AddIdentifiableUserCommandFactory(string identifier, Func<IIdentifiableUserCommand> commandFactory) public void AddIdentifiableUserCommandFactory(string identifier, Func<IIdentifiableUserCommand> commandFactory)
=> _identifiableUserCommands.Add(identifier, 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)) if (!_identifiableUserCommands.ContainsKey(identifier))
throw new IndexOutOfRangeException($"No command factory is registered for command {identifier}"); throw new IndexOutOfRangeException($"No command factory is registered for command {identifier}");

View File

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

View File

@@ -1,7 +1,9 @@
using Avalonia.Controls; using Avalonia;
using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using FileTime.App.FrequencyNavigation.ViewModels; using Avalonia.Threading;
using FileTime.App.CommandPalette.ViewModels;
namespace FileTime.GuiApp.Views; namespace FileTime.GuiApp.Views;
@@ -10,17 +12,22 @@ public partial class CommandPalette : UserControl
public CommandPalette() public CommandPalette()
{ {
InitializeComponent(); 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) 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) if (e.Key == Key.Escape)
{ {
viewModel.Close(); viewModel.Close();
@@ -30,4 +37,9 @@ public partial class CommandPalette : UserControl
viewModel.HandleKeyDown(e); viewModel.HandleKeyDown(e);
} }
} }
public void Test()
{
;
}
} }

View File

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

View File

@@ -1,6 +1,6 @@
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml;
using FileTime.App.FrequencyNavigation.ViewModels; using FileTime.App.FrequencyNavigation.ViewModels;
namespace FileTime.GuiApp.Views; namespace FileTime.GuiApp.Views;
@@ -10,11 +10,16 @@ public partial class FrequencyNavigation : UserControl
public FrequencyNavigation() public FrequencyNavigation()
{ {
InitializeComponent(); 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) private void Search_OnKeyDown(object? sender, KeyEventArgs e)

View File

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