CommandPalette use FuzzyPanel, Focus search textbox
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -7,6 +7,7 @@ public interface ICommandPaletteService
|
||||
{
|
||||
IObservable<bool> ShowWindow { get; }
|
||||
void OpenCommandPalette();
|
||||
void CloseCommandPalette();
|
||||
IReadOnlyList<ICommandPaletteEntry> GetCommands();
|
||||
ICommandPaletteViewModel? CurrentModal { get; }
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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}");
|
||||
|
||||
|
||||
@@ -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>
|
||||
@@ -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()
|
||||
{
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user