diff --git a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/FileTime.App.CommandPalette.Abstractions.csproj b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/FileTime.App.CommandPalette.Abstractions.csproj index 2da1ed9..0433a7a 100644 --- a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/FileTime.App.CommandPalette.Abstractions.csproj +++ b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/FileTime.App.CommandPalette.Abstractions.csproj @@ -9,6 +9,7 @@ + diff --git a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs index 861f4a6..b1d476f 100644 --- a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs +++ b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/Services/ICommandPaletteService.cs @@ -7,6 +7,7 @@ public interface ICommandPaletteService { IObservable ShowWindow { get; } void OpenCommandPalette(); + void CloseCommandPalette(); IReadOnlyList GetCommands(); ICommandPaletteViewModel? CurrentModal { get; } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs index 3e8b3f0..738732d 100644 --- a/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette.Abstractions/ViewModels/ICommandPaletteViewModel.cs @@ -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, IModalViewModel { IObservable ShowWindow { get; } - List FilteredMatches { get; } - string SearchText { get; set; } - ICommandPaletteEntryViewModel SelectedItem { get; set; } void Close(); - void HandleKeyDown(KeyEventArgs keyEventArgs); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.CommandPalette/FileTime.App.CommandPalette.csproj b/src/AppCommon/FileTime.App.CommandPalette/FileTime.App.CommandPalette.csproj index 36e5f38..4a0a6a7 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/FileTime.App.CommandPalette.csproj +++ b/src/AppCommon/FileTime.App.CommandPalette/FileTime.App.CommandPalette.csproj @@ -9,10 +9,12 @@ + + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs b/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs index 36995b5..c6a09a4 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs +++ b/src/AppCommon/FileTime.App.CommandPalette/Services/CommandPaletteService.cs @@ -28,6 +28,16 @@ public partial class CommandPaletteService : ICommandPaletteService CurrentModal = _modalService.OpenModal(); } + public void CloseCommandPalette() + { + _showWindow.OnNext(false); + if (_currentModal is not null) + { + _modalService.CloseModal(_currentModal); + CurrentModal = null; + } + } + public IReadOnlyList GetCommands() => _identifiableUserCommandService .GetCommandIdentifiers() diff --git a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs index 079d53e..967dce4 100644 --- a/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs +++ b/src/AppCommon/FileTime.App.CommandPalette/ViewModels/CommandPaletteViewModel.cs @@ -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, ICommandPaletteViewModel { - private string _searchText; - - [Property] private IObservable _showWindow; - [Property] private List _filteredMatches; - [Property] private ICommandPaletteEntryViewModel? _selectedItem; + private readonly ICommandPaletteService _commandPaletteService; + private readonly IIdentifiableUserCommandService _identifiableUserCommandService; + private readonly IUserCommandHandlerService _userCommandHandlerService; + private readonly ILogger _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 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 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; } } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IIdentifiableUserCommandService.cs b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IIdentifiableUserCommandService.cs index 48d53c5..c925895 100644 --- a/src/AppCommon/FileTime.App.Core.Abstraction/Services/IIdentifiableUserCommandService.cs +++ b/src/AppCommon/FileTime.App.Core.Abstraction/Services/IIdentifiableUserCommandService.cs @@ -5,6 +5,6 @@ namespace FileTime.App.Core.Services; public interface IIdentifiableUserCommandService { void AddIdentifiableUserCommandFactory(string identifier, Func commandFactory); - IIdentifiableUserCommand GetCommand(string identifier); + IIdentifiableUserCommand? GetCommand(string identifier); IReadOnlyCollection GetCommandIdentifiers(); } \ No newline at end of file diff --git a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/IdentifiableUserCommandService.cs b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/IdentifiableUserCommandService.cs index aa37ac7..9d65eb0 100644 --- a/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/IdentifiableUserCommandService.cs +++ b/src/AppCommon/FileTime.App.Core/Services/UserCommandHandler/IdentifiableUserCommandService.cs @@ -9,8 +9,9 @@ public class IdentifiableUserCommandService : IIdentifiableUserCommandService public void AddIdentifiableUserCommandFactory(string identifier, Func 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}"); diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml index b6098e9..47e3916 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml @@ -12,9 +12,9 @@ + Text="{Binding SearchText, Mode=TwoWay}" + x:Name="SearchTextBox" /> - + \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs index 1b849e2..2a809d8 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/CommandPalette.axaml.cs @@ -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() + { + ; + } } \ No newline at end of file diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml index 962746e..16297d9 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml @@ -1,27 +1,27 @@ + xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> + Text="{Binding SearchText, Mode=TwoWay}" + x:Name="SearchTextBox" /> diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs index de21d8e..f1e3b7e 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/FrequencyNavigation.axaml.cs @@ -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) diff --git a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml index f714c93..1c0142d 100644 --- a/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml +++ b/src/GuiApp/Avalonia/FileTime.GuiApp/Views/MainWindow.axaml @@ -672,7 +672,7 @@ IsVisible="{Binding ShowWindow^, FallbackValue=False}" VerticalAlignment="Stretch"> - + @@ -683,7 +683,7 @@ IsVisible="{Binding ShowWindow^, FallbackValue=False}" VerticalAlignment="Stretch"> - +