CommandPalette use FuzzyPanel, Focus search textbox
This commit is contained in:
@@ -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>
|
||||||
|
|||||||
@@ -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; }
|
||||||
}
|
}
|
||||||
@@ -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);
|
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -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();
|
||||||
}
|
}
|
||||||
@@ -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}");
|
||||||
|
|
||||||
|
|||||||
@@ -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>
|
||||||
@@ -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()
|
||||||
|
{
|
||||||
|
;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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>
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user