From e4c6ff581052444473fa70fb0ac3ce196b298498 Mon Sep 17 00:00:00 2001 From: iksi4prs <65832579+iksi4prs@users.noreply.github.com> Date: Sun, 12 Nov 2023 11:40:02 +0200 Subject: [PATCH 1/5] Feature/quick search wip (#7) * 1st version of adding system icon for files * added SystemIconsService * continued implementation * changed reminder tag * added support for extension who use uwp apps * added support also to indexing using , * added wip * added support to resolve lnk files * fixed some lnk issues * moved some code to new service IShellLinksService * added fallback to builtin icons, when there is no shell icon * updated comments * renamed enum value * added ShellIconsCacheService * implemented cache * added todos * added ImageModel and csproj to implement it * cleanup * moved System.Drawing.Common to windows project * started adding filter * started adding settings for icons * added comment * some imporvments * fix * added caching of value * added some notes in settings dialog * renamed IconsService to IconsSettingsService * continued rename * added check of Windows * cleanup * cleanup * renamed 'SystemIcon' to 'ShellIcon' * moved strings to resources * fixed comment * cleanup and made code more clear * changed code to be more clear * cleanup * fixed - dont crash on linux * some cleanup and comments * added key down on filters also selects next item * moved code to new service 'quick-search * added stubs for TextInput event * cleanup * add usage of TextInput instead of KeyToChar * added settings dialogs * renamed folder * renamed namespace * renamed file * renamed interface * renmaed enum * renamed member * renamed arg * renamed file * renamed class * added comment * fixed text on label * added explenation * fixed comment * fixed todo * added todo * removed comment * updated comments * updated comment * fixed: make sure to scroll to item * refactoring in GetSelected * fixed going to next row, using same letter repeatedly * refactoring and fix of select of first item * also reset selected index on clear of search * cleanup * fixed icon of keyboard in settings (before was copy of some existing icon) * added help and fixed look of settings, almost no need for groupbox now * added binding to disable row * cleanup * cleanup from unused style classes * also go back when shift pressed * cleanup * moved code to static function so will be more clear * some fixes * more fix * added clear search when switcing folders * added comment * added comment * added comment and cleanup * fixed all build error after sync from fork * removed duplicate of file which was moved to other folder * cleanup * cleanup 2 * cleanup and comments * added comments * removed un-needed file * added comments + cleanup * cleanup 3 * added comments + cleanup 2 * remove unused file * added comment * cleanup --- .../IQuickSearchService.cs | 25 ++ .../Models/Enums/Input/KeyModifiers.cs | 19 ++ .../Models/Enums/QuickSearchMode.cs | 9 + .../Models/QuickSearchFileModel.cs | 10 + .../Models/QuickSearchModel.cs | 13 + src/Camelot.Services/QuickSearchService.cs | 243 ++++++++++++++++++ .../Dialogs/SettingsDialogViewModel.cs | 10 +- .../FilePanels/FilesPanelViewModel.cs | 118 ++++++++- .../Nodes/FileSystemNodeViewModelBase.cs | 3 +- .../Settings/KeyboardSettingsViewModel.cs | 58 +++++ .../FilePanels/IFilesPanelViewModel.cs | 6 + .../Nodes/IFileSystemNodeViewModel.cs | 1 + .../ServicesBootstrapper.cs | 3 + .../ViewModelsBootstrapper.cs | 10 +- src/Camelot/Properties/Resources.Designer.cs | 23 ++ src/Camelot/Properties/Resources.resx | 12 + .../Settings/KeyboardSettingsView.xaml | 61 +++++ .../Settings/KeyboardSettingsView.xaml.cs | 13 + src/Camelot/Views/Dialogs/SettingsDialog.xaml | 33 ++- src/Camelot/Views/Main/FilesPanelView.xaml | 25 +- src/Camelot/Views/Main/FilesPanelView.xaml.cs | 40 ++- .../Dialogs/SettingsDialogViewModelTests.cs | 39 ++- .../FilePanels/FilesPanelViewModelTests.cs | 5 +- 23 files changed, 751 insertions(+), 28 deletions(-) create mode 100644 src/Camelot.Services.Abstractions/IQuickSearchService.cs create mode 100644 src/Camelot.Services.Abstractions/Models/Enums/Input/KeyModifiers.cs create mode 100644 src/Camelot.Services.Abstractions/Models/Enums/QuickSearchMode.cs create mode 100644 src/Camelot.Services.Abstractions/Models/QuickSearchFileModel.cs create mode 100644 src/Camelot.Services.Abstractions/Models/QuickSearchModel.cs create mode 100644 src/Camelot.Services/QuickSearchService.cs create mode 100644 src/Camelot.ViewModels/Implementations/Settings/KeyboardSettingsViewModel.cs create mode 100644 src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml create mode 100644 src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml.cs diff --git a/src/Camelot.Services.Abstractions/IQuickSearchService.cs b/src/Camelot.Services.Abstractions/IQuickSearchService.cs new file mode 100644 index 00000000..d73f42f4 --- /dev/null +++ b/src/Camelot.Services.Abstractions/IQuickSearchService.cs @@ -0,0 +1,25 @@ +using Camelot.Services.Abstractions.Models; +using System.Collections.Generic; + +namespace Camelot.Services.Abstractions; + +public interface IQuickSearchService +{ + QuickSearchModel GetQuickSearchSettings(); + + void SaveQuickSearchSettings(QuickSearchModel quickSearchModel); + + /// + /// + /// + /// This arg is is of type 'char' and not 'Key', since translation from Key to char + // is language/keyboard dependent, and should be done in caller level by Avalonia. + /// + /// + /// + void OnCharDown(char c, bool isShiftDown, List files, out bool handled); + + void ClearSearch(); + + bool Enabled(); +} \ No newline at end of file diff --git a/src/Camelot.Services.Abstractions/Models/Enums/Input/KeyModifiers.cs b/src/Camelot.Services.Abstractions/Models/Enums/Input/KeyModifiers.cs new file mode 100644 index 00000000..b92dac12 --- /dev/null +++ b/src/Camelot.Services.Abstractions/Models/Enums/Input/KeyModifiers.cs @@ -0,0 +1,19 @@ +using System; + +namespace Camelot.Services.Abstractions.Models.Enums.Input; + +/// +/// Needed to be duplicated here, since nor Avalonia, nor Windows.Forms, etc +/// are referenced in "Camelot.Services.Abstractions" +/// Used in feature of 'quick search' to determine if shift key is down. +/// + +[Flags] +public enum KeyModifiers +{ + None = 0, + Alt = 1, + Control = 2, + Shift = 4, + Meta = 8, +} diff --git a/src/Camelot.Services.Abstractions/Models/Enums/QuickSearchMode.cs b/src/Camelot.Services.Abstractions/Models/Enums/QuickSearchMode.cs new file mode 100644 index 00000000..fe513f22 --- /dev/null +++ b/src/Camelot.Services.Abstractions/Models/Enums/QuickSearchMode.cs @@ -0,0 +1,9 @@ + +namespace Camelot.Services.Abstractions.Models.Enums; + +public enum QuickSearchMode : byte +{ + Disabled, + Letter, + Word +} \ No newline at end of file diff --git a/src/Camelot.Services.Abstractions/Models/QuickSearchFileModel.cs b/src/Camelot.Services.Abstractions/Models/QuickSearchFileModel.cs new file mode 100644 index 00000000..fcf42989 --- /dev/null +++ b/src/Camelot.Services.Abstractions/Models/QuickSearchFileModel.cs @@ -0,0 +1,10 @@ + +namespace Camelot.Services.Abstractions.Models; + +public record QuickSearchFileModel +{ + public string Name { get; init; } + public object Tag { get; init; } + public bool Found { get; set; } + public bool Selected { get; set; } +} diff --git a/src/Camelot.Services.Abstractions/Models/QuickSearchModel.cs b/src/Camelot.Services.Abstractions/Models/QuickSearchModel.cs new file mode 100644 index 00000000..c9f44aaf --- /dev/null +++ b/src/Camelot.Services.Abstractions/Models/QuickSearchModel.cs @@ -0,0 +1,13 @@ +using Camelot.Services.Abstractions.Models.Enums; + +namespace Camelot.Services.Abstractions.Models; + +public class QuickSearchModel +{ + public QuickSearchMode SelectedMode { get; } + + public QuickSearchModel(QuickSearchMode selectedMode) + { + SelectedMode = selectedMode; + } +} \ No newline at end of file diff --git a/src/Camelot.Services/QuickSearchService.cs b/src/Camelot.Services/QuickSearchService.cs new file mode 100644 index 00000000..680bcdf8 --- /dev/null +++ b/src/Camelot.Services/QuickSearchService.cs @@ -0,0 +1,243 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Camelot.DataAccess.UnitOfWork; +using Camelot.Services.Abstractions; +using Camelot.Services.Abstractions.Models; +using Camelot.Services.Abstractions.Models.Enums; + +namespace Camelot.Services; + +// Name of feature as "Quick search" is based on same name used by Total-Commander. +// Changing opacity of filtered items is based on muCommander. +public class QuickSearchService : IQuickSearchService +{ + private const string SettingsId = "QuickSearchSettings"; + private readonly QuickSearchModel _default; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; + private QuickSearchModel _cachedSettingsValue = null; + private string _searchWord = string.Empty; + private char _searchLetter = Char.MinValue; + private int _selectedIndex = -1; + public QuickSearchService(IUnitOfWorkFactory unitOfWorkFactory) + { + _unitOfWorkFactory = unitOfWorkFactory; + _default = new QuickSearchModel(QuickSearchMode.Letter); + GetQuickSearchSettings(); + } + + public bool Enabled() + { + return _cachedSettingsValue.SelectedMode != QuickSearchMode.Disabled; + } + + public QuickSearchModel GetQuickSearchSettings() + { + if (_cachedSettingsValue == null) + { + using var uow = _unitOfWorkFactory.Create(); + var repository = uow.GetRepository(); + var dbModel = repository.GetById(SettingsId); + if (dbModel != null) + _cachedSettingsValue = dbModel; + else + _cachedSettingsValue = _default; + } + else + { + // we set value of _cachedValue in 'save', + // so no need to read from the repository every time. + } + return _cachedSettingsValue; + } + + public void OnCharDown(char c, + bool isShiftDown, + List files, + out bool handled) + { + if (!Enabled()) + { + handled = false; + return; + } + + if (files == null) + throw new ArgumentNullException(nameof(files)); + + c = Char.ToLower(c); + switch(_cachedSettingsValue.SelectedMode) + { + case QuickSearchMode.Letter: + { + if (_searchLetter != c) + { + _selectedIndex = -1; + } + _searchLetter = c; + break; + } + case QuickSearchMode.Word: + { + _searchWord += c; + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + ResetSelectedItem(files); + var countFound = SearchFilesAndSetFound(files); + if (countFound > 0) + SetSelectedItem(files, isShiftDown); + handled = true; + } + + /// + /// Set value of + /// which indicates whether file was found in quick search, + /// namely start with the typed letter/word + /// + private int SearchFilesAndSetFound(List files) + { + if (files == null) + throw new ArgumentNullException(nameof(files)); + + int found = 0; + for (int i = 0; i < files.Count; i++) + { + var file = files[i]; + if (IncludeInSearchResults(file)) + { + file.Found = true; + found++; + } + else + { + file.Found = false; + } + } + return found; + } + + /// + /// Set value of + /// which indicates to UI which item should be selected. + /// + + private void SetSelectedItem(List files, + bool isShiftDown) + { + if (files == null) + throw new ArgumentNullException(nameof(files)); + if (files.Where(x => x.Selected).Any()) + throw new ArgumentOutOfRangeException(nameof(files)); + + _selectedIndex = ComputeNewSelectedIndex(files, _selectedIndex, isShiftDown); + if (_selectedIndex >= 0) + { + var file = files[_selectedIndex]; + file.Selected = true; + } + } + + private void ResetSelectedItem(List files) + { + if (files == null) + throw new ArgumentNullException(nameof(files)); + + files.ForEach(x => x.Selected = false); + } + + static private int ComputeNewSelectedIndex( + List files, + int selectedIndex, + bool isShiftDown) + { + int start, end, jump; + if (!isShiftDown) + { + start = selectedIndex > -1 ? selectedIndex + 1 : 0; + end = files.Count; + jump = 1; + } + else + { + start = selectedIndex > -1 ? selectedIndex - 1 : 0; + end = -1; + jump = -1; + } + for (int i = start; i != end; i = i + jump) + { + var file = files[i]; + if (file.Found) + { + return i; + } + } + + // if not found yet, need to start search again from 'start' + // E.g. in case 'Shift' not down: + // "cycle from last to first" + // reset, so and start again from first + // Done in 2 'half' loops, in sake of effiency. + if (!isShiftDown) + { + start = 0; + end = selectedIndex > -1 ? selectedIndex : 0; + jump = 1; + } + else + { + start = files.Count - 1; + end = selectedIndex > -1 ? selectedIndex : 0; + jump = -1; + } + for (int i = start; i != end; i = i + jump) + { + var file = files[i]; + if (file.Found) + { + return i; + } + } + + return -1; + } + + private bool IncludeInSearchResults(QuickSearchFileModel file) + { + switch (_cachedSettingsValue.SelectedMode) + { + case QuickSearchMode.Letter: + return char.ToLower(file.Name[0]) == _searchLetter; + case QuickSearchMode.Word: + return file.Name.StartsWith(_searchWord, StringComparison.OrdinalIgnoreCase); + default: + throw new ArgumentOutOfRangeException(); + } + } + + public void ClearSearch() + { + if (!Enabled()) + { + return; + } + + _searchWord = string.Empty; + _searchLetter = Char.MinValue; + _selectedIndex = -1; + } + + public void SaveQuickSearchSettings(QuickSearchModel quickSearchModel) + { + if (quickSearchModel == null) + throw new ArgumentNullException(nameof(quickSearchModel)); + + using var uow = _unitOfWorkFactory.Create(); + var repository = uow.GetRepository(); + repository.Upsert(SettingsId, quickSearchModel); + _cachedSettingsValue = quickSearchModel; + } +} diff --git a/src/Camelot.ViewModels/Implementations/Dialogs/SettingsDialogViewModel.cs b/src/Camelot.ViewModels/Implementations/Dialogs/SettingsDialogViewModel.cs index a2ca2d87..33b9e1e0 100644 --- a/src/Camelot.ViewModels/Implementations/Dialogs/SettingsDialogViewModel.cs +++ b/src/Camelot.ViewModels/Implementations/Dialogs/SettingsDialogViewModel.cs @@ -15,8 +15,11 @@ public class SettingsDialogViewModel : DialogViewModelBase public ISettingsViewModel TerminalSettingsViewModel { get; set; } public ISettingsViewModel GeneralSettingsViewModel { get; set; } + public ISettingsViewModel IconsSettingsViewModel { get; set; } + public ISettingsViewModel KeyboardSettingsViewModel { get; set; } + public int SelectedIndex { get => _selectedIndex; @@ -32,17 +35,20 @@ public int SelectedIndex public SettingsDialogViewModel( ISettingsViewModel generalSettingsViewModel, ISettingsViewModel terminalSettingsViewModel, - ISettingsViewModel iconsSettingsViewModel) + ISettingsViewModel iconsSettingsViewModel, + ISettingsViewModel keyboardSettingsViewModel) { TerminalSettingsViewModel = terminalSettingsViewModel; GeneralSettingsViewModel = generalSettingsViewModel; IconsSettingsViewModel = iconsSettingsViewModel; + KeyboardSettingsViewModel = keyboardSettingsViewModel; _settingsViewModels = new[] { generalSettingsViewModel, terminalSettingsViewModel, - iconsSettingsViewModel + iconsSettingsViewModel, + keyboardSettingsViewModel }; Activate(_settingsViewModels.First()); diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs index 15f9137a..755de868 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Input; +using Avalonia.Input; using Camelot.Avalonia.Interfaces; using Camelot.Extensions; using Camelot.Services.Abstractions; @@ -22,6 +23,7 @@ using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Tabs; using Camelot.ViewModels.Interfaces.MainWindow.Operations; using Camelot.ViewModels.Services.Interfaces; + using DynamicData; using ReactiveUI; using ReactiveUI.Fody.Helpers; @@ -43,6 +45,7 @@ public class FilesPanelViewModel : ViewModelBase, IFilesPanelViewModel private readonly IFilePanelDirectoryObserver _filePanelDirectoryObserver; private readonly IPermissionsService _permissionsService; private readonly IDialogService _dialogService; + private readonly IQuickSearchService _quickSearchService; private readonly ObservableCollection _fileSystemNodes; private readonly ObservableCollection _selectedFileSystemNodes; @@ -79,6 +82,7 @@ public class FilesPanelViewModel : ViewModelBase, IFilesPanelViewModel public IList SelectedFileSystemNodes => _selectedFileSystemNodes; + [Reactive] public IFileSystemNodeViewModel CurrentNode { get; private set; } @@ -113,7 +117,6 @@ public string CurrentDirectory public ICommand GoToParentDirectoryCommand { get; } public ICommand SortFilesCommand { get; } - public FilesPanelViewModel( IFileService fileService, IDirectoryService directoryService, @@ -133,7 +136,8 @@ public FilesPanelViewModel( IOperationsViewModel operationsViewModel, IDirectorySelectorViewModel directorySelectorViewModel, IDragAndDropOperationsMediator dragAndDropOperationsMediator, - IClipboardOperationsViewModel clipboardOperationsViewModel) + IClipboardOperationsViewModel clipboardOperationsViewModel, + IQuickSearchService quickSearchService) { _fileService = fileService; _directoryService = directoryService; @@ -148,6 +152,7 @@ public FilesPanelViewModel( _filePanelDirectoryObserver = filePanelDirectoryObserver; _permissionsService = permissionsService; _dialogService = dialogService; + _quickSearchService = quickSearchService; SearchViewModel = searchViewModel; TabsListViewModel = tabsListViewModel; @@ -165,7 +170,6 @@ public FilesPanelViewModel( (DirectoryModel dm) => dm is not null); GoToParentDirectoryCommand = ReactiveCommand.Create(GoToParentDirectory, canGoToParentDirectory); SortFilesCommand = ReactiveCommand.Create(SortFiles); - SubscribeToEvents(); UpdateStateAsync().Forget(); } @@ -257,6 +261,8 @@ private async Task UpdateStateAsync() CurrentDirectoryChanged.Raise(this, EventArgs.Empty); this.RaisePropertyChanged(nameof(ParentDirectory)); + + _quickSearchService.ClearSearch(); } private void UpdateNode(string nodePath) => RecreateNode(nodePath, nodePath); @@ -438,4 +444,108 @@ private IFileSystemNodeViewModel GetViewModel(string nodePath) => private bool CheckIfShouldShowNode(string nodePath) => _specification?.IsSatisfiedBy(_nodeService.GetNode(nodePath)) ?? true; -} \ No newline at end of file + + private int GetSelectedIndex() + { + if (SelectedFileSystemNodes.Count == 0) + return -1; + var oldSelected = SelectedFileSystemNodes.First(); + var nodes = FileSystemNodes.ToList(); + for (int i = 0; i < nodes.Count; i++) + { + var curr = nodes[i]; + if (curr.FullPath == oldSelected.FullPath) + { + return i; + } + } + return -1; + } + + private IFileSystemNodeViewModel GetSelected() + { + int selectedIndex = GetSelectedIndex(); + if (selectedIndex < 0) + return null; + + var nodes = FileSystemNodes.ToList(); + var selected = nodes[selectedIndex]; + return selected; + } + + private void SelectNodeEx(string newSelected, IFileSystemNodeViewModel oldSelected) + { + if (newSelected != null) + { + if (oldSelected != null) + { + UnselectNode(oldSelected.FullPath); + } + SelectNode(newSelected); + } + } + + public void OnDataGridKeyDownCallback(Key key) + { + if (_quickSearchService.Enabled()) + { + if (key == Key.Escape) + { + _quickSearchService.ClearSearch(); + FileSystemNodes.ForEach(x => x.IsFilteredOut = false); + } + } + } + + /// + /// We use specific handler for TextInput, and not reuse KeyDown, + /// since translation from Key to Char is language/keyboard dependent, + /// and should be done in caller level by Avalonia + /// + /// + /// + /// + public void OnDataGridTextInputCallback(string text, bool isShiftDown) + { + if (_quickSearchService.Enabled()) + { + if (string.IsNullOrEmpty(text) || text.Length != 1) + throw new ArgumentOutOfRangeException(nameof(text)); + + char c = text[0]; + var files = CreateQuickSearchFiles(); + _quickSearchService.OnCharDown(c, isShiftDown, files, out bool handled); + if (handled) + UpdateFilterAfterQuickSearch(files); + } + } + + private void UpdateFilterAfterQuickSearch(List files) + { + if (!_quickSearchService.Enabled()) + throw new InvalidOperationException(); + + string newSelected = null; + foreach (var file in files) + { + var node = (IFileSystemNodeViewModel)file.Tag; + node.IsFilteredOut = !file.Found; + if (file.Selected) + newSelected = node.FullPath; + } + + var oldSelected = GetSelected(); + SelectNodeEx(newSelected, oldSelected); + } + + private List CreateQuickSearchFiles() + { + if (!_quickSearchService.Enabled()) + throw new InvalidOperationException(); + + var result = FileSystemNodes + .Select(x => new QuickSearchFileModel() { Name = x.Name, Tag = x }) + .ToList(); + return result; + } +} diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs index f038273a..0885e9c0 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs @@ -27,7 +27,8 @@ public abstract class FileSystemNodeViewModelBase : ViewModelBase, IFileSystemNo [Reactive] public bool IsEditing { get; set; } - + [Reactive] + public bool IsFilteredOut { get; set; } public bool IsArchive => _fileSystemNodeFacade.CheckIfNodeIsArchive(FullPath); public bool ShouldShowOpenSubmenu { get; } diff --git a/src/Camelot.ViewModels/Implementations/Settings/KeyboardSettingsViewModel.cs b/src/Camelot.ViewModels/Implementations/Settings/KeyboardSettingsViewModel.cs new file mode 100644 index 00000000..8a79067b --- /dev/null +++ b/src/Camelot.ViewModels/Implementations/Settings/KeyboardSettingsViewModel.cs @@ -0,0 +1,58 @@ +using System.Collections.Generic; +using Camelot.Services.Abstractions; +using Camelot.Services.Abstractions.Models; +using Camelot.Services.Abstractions.Models.Enums; +using Camelot.ViewModels.Interfaces.Settings; +using ReactiveUI.Fody.Helpers; + +namespace Camelot.ViewModels.Implementations.Settings; + +public class KeyboardSettingsViewModel : ViewModelBase, ISettingsViewModel +{ + private readonly IQuickSearchService _quickSearchService; + private QuickSearchMode _initialMode; + + private bool _isActivated; + + [Reactive] + public QuickSearchMode CurrentQuickSearchMode { get; set; } + + public IEnumerable QuickSearchModeOptions + { + get + { + return new []{ + QuickSearchMode.Disabled, + QuickSearchMode.Letter, + QuickSearchMode.Word }; + } + } + + public bool IsChanged => _initialMode != CurrentQuickSearchMode; + + public KeyboardSettingsViewModel( + IQuickSearchService quickSearchService) + { + _quickSearchService = quickSearchService; + } + + public void Activate() + { + if (_isActivated) + { + return; + } + + _isActivated = true; + + var model = _quickSearchService.GetQuickSearchSettings(); + _initialMode = model.SelectedMode; + CurrentQuickSearchMode = _initialMode; + } + + public void SaveChanges() + { + var model = new QuickSearchModel(CurrentQuickSearchMode); + _quickSearchService.SaveQuickSearchSettings(model); + } +} \ No newline at end of file diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs index ef208125..6c36f518 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Windows.Input; +using Avalonia.Input; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Tabs; using Camelot.ViewModels.Interfaces.MainWindow.Operations; @@ -23,6 +24,11 @@ public interface IFilesPanelViewModel IClipboardOperationsViewModel ClipboardOperationsViewModel { get; } IList SelectedFileSystemNodes { get; } + + IEnumerable FileSystemNodes { get; } + + void OnDataGridTextInputCallback(string text, bool isShiftDown); + void OnDataGridKeyDownCallback(Key key); bool IsActive { get; } diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs index 74df546a..a93bf84e 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs @@ -9,6 +9,7 @@ public interface IFileSystemNodeViewModel string Name { get; set; } bool IsEditing { get; set; } + bool IsFilteredOut { get; set; } ICommand OpenCommand { get; } } \ No newline at end of file diff --git a/src/Camelot/DependencyInjection/ServicesBootstrapper.cs b/src/Camelot/DependencyInjection/ServicesBootstrapper.cs index 8108f688..ca5fd6ec 100644 --- a/src/Camelot/DependencyInjection/ServicesBootstrapper.cs +++ b/src/Camelot/DependencyInjection/ServicesBootstrapper.cs @@ -177,6 +177,9 @@ private static void RegisterCommonServices(IMutableDependencyResolver services, resolver.GetRequiredService(), resolver.GetRequiredService() )); + services.RegisterLazySingleton(() => new QuickSearchService( + resolver.GetRequiredService() + )); } private static void RegisterPlatformSpecificServices(IMutableDependencyResolver services, IReadonlyDependencyResolver resolver) diff --git a/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs b/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs index 593ae04e..af14bd40 100644 --- a/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs +++ b/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs @@ -214,11 +214,16 @@ private static void RegisterCommonViewModels(IMutableDependencyResolver services services.Register(() => new SettingsDialogViewModel( resolver.GetRequiredService(), resolver.GetRequiredService(), - resolver.GetRequiredService() + resolver.GetRequiredService(), + resolver.GetRequiredService() )); services.Register(() => new IconsSettingsViewModel( resolver.GetRequiredService() )); + services.Register(() => new KeyboardSettingsViewModel( + resolver.GetRequiredService() + + )); services.RegisterLazySingleton(() => new FilePropertiesBehavior( resolver.GetRequiredService() )); @@ -374,7 +379,8 @@ private static IFilesPanelViewModel CreateFilesPanelViewModel( resolver.GetRequiredService(), directorySelectorViewModel, resolver.GetRequiredService(), - resolver.GetRequiredService() + resolver.GetRequiredService(), + resolver.GetRequiredService() ); return filesPanelViewModel; diff --git a/src/Camelot/Properties/Resources.Designer.cs b/src/Camelot/Properties/Resources.Designer.cs index a3568ce2..611f7b85 100644 --- a/src/Camelot/Properties/Resources.Designer.cs +++ b/src/Camelot/Properties/Resources.Designer.cs @@ -914,5 +914,28 @@ public static string SupportedOnWindowsOnly { return ResourceManager.GetString("SupportedOnWindowsOnly", resourceCulture); } } + + public static string Keyboard { + get { + return ResourceManager.GetString("Keyboard", resourceCulture); + } + } + + public static string QuickSearch { + get { + return ResourceManager.GetString("QuickSearch", resourceCulture); + } + } + + public static string QuickSearchModeWithColon { + get { + return ResourceManager.GetString("QuickSearchModeWithColon", resourceCulture); + } + } + public static string QuickSearchHelp { + get { + return ResourceManager.GetString("QuickSearchHelp", resourceCulture); + } + } } } diff --git a/src/Camelot/Properties/Resources.resx b/src/Camelot/Properties/Resources.resx index ad062707..fcf80384 100644 --- a/src/Camelot/Properties/Resources.resx +++ b/src/Camelot/Properties/Resources.resx @@ -552,4 +552,16 @@ * Supported on Windows only + + Keyboard + + + Quick Search + + + Quick search mode: + + + Quick search for items in current directory, as you type. use Esc to clear. + \ No newline at end of file diff --git a/src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml b/src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml new file mode 100644 index 00000000..6605183a --- /dev/null +++ b/src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml @@ -0,0 +1,61 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml.cs b/src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml.cs new file mode 100644 index 00000000..305bb0b5 --- /dev/null +++ b/src/Camelot/Views/Dialogs/Settings/KeyboardSettingsView.xaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Camelot.Views.Dialogs.Settings; +public class KeyboardSettingsView : UserControl +{ + public KeyboardSettingsView() + { + InitializeComponent(); + } + + private void InitializeComponent() => AvaloniaXamlLoader.Load(this); +} \ No newline at end of file diff --git a/src/Camelot/Views/Dialogs/SettingsDialog.xaml b/src/Camelot/Views/Dialogs/SettingsDialog.xaml index ad66df1d..6a43608e 100644 --- a/src/Camelot/Views/Dialogs/SettingsDialog.xaml +++ b/src/Camelot/Views/Dialogs/SettingsDialog.xaml @@ -17,7 +17,7 @@ - + @@ -41,7 +41,7 @@ - + @@ -67,7 +67,7 @@ - + @@ -93,6 +93,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Camelot/Views/Main/FilesPanelView.xaml b/src/Camelot/Views/Main/FilesPanelView.xaml index 245ce6f9..673383b4 100644 --- a/src/Camelot/Views/Main/FilesPanelView.xaml +++ b/src/Camelot/Views/Main/FilesPanelView.xaml @@ -111,6 +111,8 @@ Items="{Binding FileSystemNodes}" DragDrop.AllowDrop="True" KeyDown="OnDataGridKeyDown" + KeyUp="OnDataGridKeyUp" + TextInput="OnDataGridTextInput" DoubleTapped="OnDataGridDoubleTapped" CellPointerPressed="OnDataGridCellPointerPressed" SelectionChanged="OnDataGridSelectionChanged"> @@ -159,8 +161,16 @@ - + + + @@ -298,7 +308,8 @@ - @@ -325,11 +336,13 @@ - + - + @@ -352,7 +365,9 @@ - + + diff --git a/src/Camelot/Views/Main/FilesPanelView.xaml.cs b/src/Camelot/Views/Main/FilesPanelView.xaml.cs index cc625f46..b2cf529a 100644 --- a/src/Camelot/Views/Main/FilesPanelView.xaml.cs +++ b/src/Camelot/Views/Main/FilesPanelView.xaml.cs @@ -1,20 +1,26 @@ using System; using System.ComponentModel; +using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Timers; +using System.Xml.Serialization; using Avalonia; using Avalonia.Controls; +using Avalonia.Data.Converters; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; +using Avalonia.VisualTree; using Camelot.Avalonia.Interfaces; using Camelot.DependencyInjection; using Camelot.Extensions; +using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Nodes; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; using Camelot.Views.Main.Controls; using DynamicData; +using ReactiveUI; using Splat; namespace Camelot.Views.Main; @@ -29,6 +35,7 @@ public class FilesPanelView : UserControl private bool _isCellPressed; private PointerEventArgs _pointerEventArgs; private IDataContextProvider _dataContextProvider; + private bool _shiftDown; private DataGrid FilesDataGrid => this.FindControl("FilesDataGrid"); @@ -77,6 +84,13 @@ private void ViewModelOnSelectionAdded(object sender, SelectionAddedEventArgs e) { FilesDataGrid.SelectedItems.Add(item); } + + // In case event was triggerd by keyboard (up/down arrows, quick-search, etc), + // Need to make sure item is viewable + if (FilesDataGrid.SelectedItems.Count == 1) + { + FilesDataGrid.ScrollIntoView(item, null); + } } private void ViewModelOnSelectionRemoved(object sender, SelectionRemovedEventArgs e) @@ -153,19 +167,34 @@ private void OnDataGridDoubleTapped(object sender, RoutedEventArgs args) StopEditing(nodeViewModel); nodeViewModel.OpenCommand.Execute(null); } - + private void OnDataGridKeyDown(object sender, KeyEventArgs args) { - if (args.Key != Key.Delete && args.Key != Key.Back) + if (args.Key == Key.Delete || args.Key == Key.Back) { + args.Handled = true; + ViewModel.OperationsViewModel.MoveToTrashCommand.Execute(null); return; } - args.Handled = true; + _shiftDown = (args.KeyModifiers & KeyModifiers.Shift) > 0; + ViewModel.OnDataGridKeyDownCallback(args.Key); + } - ViewModel.OperationsViewModel.MoveToTrashCommand.Execute(null); + /// + /// Needed to get state of shift key + /// + /// + /// + private void OnDataGridKeyUp(object sender, KeyEventArgs args) + { + _shiftDown = (args.KeyModifiers & KeyModifiers.Shift) > 0; } + private void OnDataGridTextInput(object sender, TextInputEventArgs args) + { + ViewModel.OnDataGridTextInputCallback(args.Text, _shiftDown); + } private void OnDataGridCellPointerPressed(object sender, DataGridCellPointerPressedEventArgs args) { ActivateViewModel(); @@ -354,4 +383,5 @@ private async void DataGridOnContextMenuOpening(object sender, CancelEventArgs e item.IsVisible = await ViewModel.ClipboardOperationsViewModel.CanPasteAsync(); } } -} \ No newline at end of file +} + diff --git a/tests/Camelot.ViewModels.Tests/Dialogs/SettingsDialogViewModelTests.cs b/tests/Camelot.ViewModels.Tests/Dialogs/SettingsDialogViewModelTests.cs index c4720c42..bc195919 100644 --- a/tests/Camelot.ViewModels.Tests/Dialogs/SettingsDialogViewModelTests.cs +++ b/tests/Camelot.ViewModels.Tests/Dialogs/SettingsDialogViewModelTests.cs @@ -25,14 +25,21 @@ public void TestSettingsViewModel() .Setup(m => m.Activate()) .Verifiable(); + var keyboardSettingsViewModel = new Mock(); + keyboardSettingsViewModel + .Setup(m => m.Activate()) + .Verifiable(); + var dialogViewModel = new SettingsDialogViewModel(generalSettingsViewModel.Object, terminalSettingsViewModel.Object, - iconsSettingsViewModel.Object); + iconsSettingsViewModel.Object, + keyboardSettingsViewModel.Object); Assert.Equal(0, dialogViewModel.SelectedIndex); Assert.Equal(generalSettingsViewModel.Object, dialogViewModel.GeneralSettingsViewModel); Assert.Equal(terminalSettingsViewModel.Object, dialogViewModel.TerminalSettingsViewModel); Assert.Equal(iconsSettingsViewModel.Object, dialogViewModel.IconsSettingsViewModel); + Assert.Equal(keyboardSettingsViewModel.Object, dialogViewModel.KeyboardSettingsViewModel); generalSettingsViewModel.Verify(m => m.Activate(), Times.Once); } @@ -64,10 +71,20 @@ public void TestSaveCommand() .SetupGet(m => m.IsChanged) .Returns(true); + + var keyboardSettingsViewModel = new Mock(); + keyboardSettingsViewModel + .Setup(m => m.SaveChanges()) + .Verifiable(); + keyboardSettingsViewModel + .SetupGet(m => m.IsChanged) + .Returns(true); + var dialogViewModel = new SettingsDialogViewModel( generalSettingsViewModel.Object, terminalSettingsViewModel.Object, - iconsSettingsViewModel.Object); + iconsSettingsViewModel.Object, + keyboardSettingsViewModel.Object); Assert.True(dialogViewModel.SaveCommand.CanExecute(null)); dialogViewModel.SaveCommand.Execute(null); @@ -75,6 +92,7 @@ public void TestSaveCommand() generalSettingsViewModel.Verify(m => m.SaveChanges(), Times.Once); terminalSettingsViewModel.Verify(m => m.SaveChanges(), Times.Once); iconsSettingsViewModel.Verify(m => m.SaveChanges(), Times.Once); + keyboardSettingsViewModel.Verify(m => m.SaveChanges(), Times.Once); } [Fact] @@ -95,10 +113,16 @@ public void TestSaveCommandNoChanges() .Setup(m => m.Activate()) .Verifiable(); + var keyboardSettingsViewModel = new Mock(); + keyboardSettingsViewModel + .Setup(m => m.Activate()) + .Verifiable(); + var dialogViewModel = new SettingsDialogViewModel( generalSettingsViewModel.Object, terminalSettingsViewModel.Object, - iconsSettingsViewModel.Object); + iconsSettingsViewModel.Object, + keyboardSettingsViewModel.Object); Assert.True(dialogViewModel.SaveCommand.CanExecute(null)); dialogViewModel.SaveCommand.Execute(null); @@ -126,10 +150,16 @@ public void TestSettingsViewModelActivation() .Setup(m => m.Activate()) .Verifiable(); + var keyboardSettingsViewModel = new Mock(); + keyboardSettingsViewModel + .Setup(m => m.Activate()) + .Verifiable(); + var dialogViewModel = new SettingsDialogViewModel( generalSettingsViewModel.Object, terminalSettingsViewModel.Object, - iconsSettingsViewModel.Object) + iconsSettingsViewModel.Object, + keyboardSettingsViewModel.Object) { SelectedIndex = 0 }; @@ -138,5 +168,6 @@ public void TestSettingsViewModelActivation() generalSettingsViewModel.Verify(m => m.Activate(), Times.Exactly(2)); terminalSettingsViewModel.Verify(m => m.Activate(), Times.Never); iconsSettingsViewModel.Verify(m => m.Activate(), Times.Never); + keyboardSettingsViewModel.Verify(m => m.Activate(), Times.Never); } } \ No newline at end of file diff --git a/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs b/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs index b3053e0b..16196881 100644 --- a/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs +++ b/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs @@ -62,7 +62,7 @@ public void TestProperties() var directorySelectorViewModel = _autoMocker.GetMock().Object; var dragAndDropOperationsMediator = _autoMocker.GetMock().Object; var clipboardOperationsViewModel = _autoMocker.GetMock().Object; - + var viewModel = new FilesPanelViewModel( _autoMocker.GetMock().Object, _autoMocker.GetMock().Object, @@ -82,7 +82,8 @@ public void TestProperties() operationsViewModel, directorySelectorViewModel, dragAndDropOperationsMediator, - clipboardOperationsViewModel + clipboardOperationsViewModel, + _autoMocker.GetMock().Object ); Assert.Equal(searchViewModel, viewModel.SearchViewModel); From b3074619681098c965107a7336530cf70914dfce Mon Sep 17 00:00:00 2001 From: IngvarX Date: Sun, 12 Nov 2023 17:53:02 -0800 Subject: [PATCH 2/5] Refactored quick search --- .../IQuickSearchService.cs | 17 +- ...chFileModel.cs => QuickSearchNodeModel.cs} | 7 +- src/Camelot.Services/QuickSearchService.cs | 210 ++++++------------ .../FilePanels/FilesPanelViewModel.cs | 127 ++++++----- .../Nodes/FileSystemNodeViewModelBase.cs | 2 + .../FilePanels/IFilesPanelViewModel.cs | 9 +- .../Nodes/IFileSystemNodeViewModel.cs | 1 + src/Camelot/Views/Main/FilesPanelView.xaml.cs | 12 +- 8 files changed, 151 insertions(+), 234 deletions(-) rename src/Camelot.Services.Abstractions/Models/{QuickSearchFileModel.cs => QuickSearchNodeModel.cs} (55%) diff --git a/src/Camelot.Services.Abstractions/IQuickSearchService.cs b/src/Camelot.Services.Abstractions/IQuickSearchService.cs index d73f42f4..94ba0996 100644 --- a/src/Camelot.Services.Abstractions/IQuickSearchService.cs +++ b/src/Camelot.Services.Abstractions/IQuickSearchService.cs @@ -5,21 +5,18 @@ namespace Camelot.Services.Abstractions; public interface IQuickSearchService { + bool IsEnabled { get; } + QuickSearchModel GetQuickSearchSettings(); void SaveQuickSearchSettings(QuickSearchModel quickSearchModel); - /// - /// - /// + /// /// This arg is is of type 'char' and not 'Key', since translation from Key to char - // is language/keyboard dependent, and should be done in caller level by Avalonia. - /// - /// - /// - void OnCharDown(char c, bool isShiftDown, List files, out bool handled); + /// is language/keyboard dependent, and should be done in caller level by Avalonia. + /// + /// + IReadOnlyList FilterNodes(char symbol, bool isBackwardsDirectionEnabled, IReadOnlyList nodes); void ClearSearch(); - - bool Enabled(); } \ No newline at end of file diff --git a/src/Camelot.Services.Abstractions/Models/QuickSearchFileModel.cs b/src/Camelot.Services.Abstractions/Models/QuickSearchNodeModel.cs similarity index 55% rename from src/Camelot.Services.Abstractions/Models/QuickSearchFileModel.cs rename to src/Camelot.Services.Abstractions/Models/QuickSearchNodeModel.cs index fcf42989..271dd4bf 100644 --- a/src/Camelot.Services.Abstractions/Models/QuickSearchFileModel.cs +++ b/src/Camelot.Services.Abstractions/Models/QuickSearchNodeModel.cs @@ -1,10 +1,11 @@  namespace Camelot.Services.Abstractions.Models; -public record QuickSearchFileModel +public record QuickSearchNodeModel { public string Name { get; init; } - public object Tag { get; init; } - public bool Found { get; set; } + + public bool IsFiltered { get; init; } + public bool Selected { get; set; } } diff --git a/src/Camelot.Services/QuickSearchService.cs b/src/Camelot.Services/QuickSearchService.cs index 680bcdf8..3fd2d0e3 100644 --- a/src/Camelot.Services/QuickSearchService.cs +++ b/src/Camelot.Services/QuickSearchService.cs @@ -13,127 +13,81 @@ namespace Camelot.Services; public class QuickSearchService : IQuickSearchService { private const string SettingsId = "QuickSearchSettings"; - private readonly QuickSearchModel _default; + private readonly IUnitOfWorkFactory _unitOfWorkFactory; - private QuickSearchModel _cachedSettingsValue = null; + + private QuickSearchModel _cachedSettingsValue; private string _searchWord = string.Empty; private char _searchLetter = Char.MinValue; private int _selectedIndex = -1; + + public bool IsEnabled => _cachedSettingsValue.SelectedMode != QuickSearchMode.Disabled; + public QuickSearchService(IUnitOfWorkFactory unitOfWorkFactory) { _unitOfWorkFactory = unitOfWorkFactory; - _default = new QuickSearchModel(QuickSearchMode.Letter); - GetQuickSearchSettings(); - } - public bool Enabled() - { - return _cachedSettingsValue.SelectedMode != QuickSearchMode.Disabled; + GetQuickSearchSettings(); } public QuickSearchModel GetQuickSearchSettings() { - if (_cachedSettingsValue == null) + if (_cachedSettingsValue is not null) { - using var uow = _unitOfWorkFactory.Create(); - var repository = uow.GetRepository(); - var dbModel = repository.GetById(SettingsId); - if (dbModel != null) - _cachedSettingsValue = dbModel; - else - _cachedSettingsValue = _default; + return _cachedSettingsValue; } - else - { - // we set value of _cachedValue in 'save', - // so no need to read from the repository every time. - } - return _cachedSettingsValue; + + using var uow = _unitOfWorkFactory.Create(); + var repository = uow.GetRepository(); + var dbModel = repository.GetById(SettingsId) ?? new QuickSearchModel(QuickSearchMode.Disabled); + + return _cachedSettingsValue = dbModel; } - public void OnCharDown(char c, - bool isShiftDown, - List files, - out bool handled) + public IReadOnlyList FilterNodes( + char symbol, bool isBackwardsDirectionEnabled, IReadOnlyList nodes) { - if (!Enabled()) - { - handled = false; - return; - } - - if (files == null) - throw new ArgumentNullException(nameof(files)); - - c = Char.ToLower(c); - switch(_cachedSettingsValue.SelectedMode) + var lowercaseSymbol = Char.ToLower(symbol); + switch (_cachedSettingsValue.SelectedMode) { case QuickSearchMode.Letter: + { + if (_searchLetter != lowercaseSymbol) { - if (_searchLetter != c) - { - _selectedIndex = -1; - } - _searchLetter = c; - break; + _selectedIndex = -1; } + + _searchLetter = lowercaseSymbol; + break; + } case QuickSearchMode.Word: - { - _searchWord += c; - break; - } + { + _searchWord += lowercaseSymbol; + break; + } default: - throw new ArgumentOutOfRangeException(); + throw new ArgumentOutOfRangeException( + nameof(_cachedSettingsValue.SelectedMode), _cachedSettingsValue.SelectedMode, null); } - ResetSelectedItem(files); - var countFound = SearchFilesAndSetFound(files); - if (countFound > 0) - SetSelectedItem(files, isShiftDown); - handled = true; - } - - /// - /// Set value of - /// which indicates whether file was found in quick search, - /// namely start with the typed letter/word - /// - private int SearchFilesAndSetFound(List files) - { - if (files == null) - throw new ArgumentNullException(nameof(files)); - - int found = 0; - for (int i = 0; i < files.Count; i++) + var result = nodes + .Select(n => new QuickSearchNodeModel {Name = n, IsFiltered = CheckIfShouldIncludeInSearchResults(n)}) + .ToList(); + if (result.Any(n => n.IsFiltered)) { - var file = files[i]; - if (IncludeInSearchResults(file)) - { - file.Found = true; - found++; - } - else - { - file.Found = false; - } + SetSelectedItem(result, isBackwardsDirectionEnabled); } - return found; + + return result; } /// - /// Set value of + /// Set value of /// which indicates to UI which item should be selected. /// - - private void SetSelectedItem(List files, - bool isShiftDown) + private void SetSelectedItem(IReadOnlyList files, bool isBackwardsDirectionEnabled) { - if (files == null) - throw new ArgumentNullException(nameof(files)); - if (files.Where(x => x.Selected).Any()) - throw new ArgumentOutOfRangeException(nameof(files)); - - _selectedIndex = ComputeNewSelectedIndex(files, _selectedIndex, isShiftDown); + _selectedIndex = ComputeNewSelectedIndex(files, _selectedIndex, isBackwardsDirectionEnabled); if (_selectedIndex >= 0) { var file = files[_selectedIndex]; @@ -141,62 +95,29 @@ private void SetSelectedItem(List files, } } - private void ResetSelectedItem(List files) - { - if (files == null) - throw new ArgumentNullException(nameof(files)); - - files.ForEach(x => x.Selected = false); - } - - static private int ComputeNewSelectedIndex( - List files, + private static int ComputeNewSelectedIndex( + IReadOnlyList files, int selectedIndex, - bool isShiftDown) + bool isBackwardsDirectionEnabled) { - int start, end, jump; - if (!isShiftDown) - { - start = selectedIndex > -1 ? selectedIndex + 1 : 0; - end = files.Count; - jump = 1; - } - else + int start, jump; + if (isBackwardsDirectionEnabled) { start = selectedIndex > -1 ? selectedIndex - 1 : 0; - end = -1; jump = -1; } - for (int i = start; i != end; i = i + jump) - { - var file = files[i]; - if (file.Found) - { - return i; - } - } - - // if not found yet, need to start search again from 'start' - // E.g. in case 'Shift' not down: - // "cycle from last to first" - // reset, so and start again from first - // Done in 2 'half' loops, in sake of effiency. - if (!isShiftDown) - { - start = 0; - end = selectedIndex > -1 ? selectedIndex : 0; - jump = 1; - } else { - start = files.Count - 1; - end = selectedIndex > -1 ? selectedIndex : 0; - jump = -1; + start = selectedIndex > -1 ? selectedIndex + 1 : 0; + jump = 1; } - for (int i = start; i != end; i = i + jump) + + start = (start + files.Count) % files.Count; + + for (var i = start; i != start - jump; i = (i + jump + files.Count) % files.Count) { var file = files[i]; - if (file.Found) + if (file.IsFiltered) { return i; } @@ -205,22 +126,17 @@ static private int ComputeNewSelectedIndex( return -1; } - private bool IncludeInSearchResults(QuickSearchFileModel file) - { - switch (_cachedSettingsValue.SelectedMode) + private bool CheckIfShouldIncludeInSearchResults(string nodeName) => + _cachedSettingsValue.SelectedMode switch { - case QuickSearchMode.Letter: - return char.ToLower(file.Name[0]) == _searchLetter; - case QuickSearchMode.Word: - return file.Name.StartsWith(_searchWord, StringComparison.OrdinalIgnoreCase); - default: - throw new ArgumentOutOfRangeException(); - } - } + QuickSearchMode.Letter => char.ToLower(nodeName[0]) == _searchLetter, + QuickSearchMode.Word => nodeName.StartsWith(_searchWord, StringComparison.OrdinalIgnoreCase), + _ => throw new ArgumentOutOfRangeException(nameof(_cachedSettingsValue.SelectedMode), _cachedSettingsValue, null) + }; public void ClearSearch() { - if (!Enabled()) + if (!IsEnabled) { return; } @@ -232,11 +148,9 @@ public void ClearSearch() public void SaveQuickSearchSettings(QuickSearchModel quickSearchModel) { - if (quickSearchModel == null) - throw new ArgumentNullException(nameof(quickSearchModel)); - using var uow = _unitOfWorkFactory.Create(); var repository = uow.GetRepository(); + repository.Upsert(SettingsId, quickSearchModel); _cachedSettingsValue = quickSearchModel; } diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs index 755de868..f8adb4d9 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs @@ -117,6 +117,7 @@ public string CurrentDirectory public ICommand GoToParentDirectoryCommand { get; } public ICommand SortFilesCommand { get; } + public FilesPanelViewModel( IFileService fileService, IDirectoryService directoryService, @@ -192,6 +193,38 @@ public void Deactivate() SelectedTab.IsGloballyActive = false; } + + public void OnDataGridKeyDownCallback(Key key) + { + if (!_quickSearchService.IsEnabled) + { + return; + } + + if (key == Key.Escape) + { + _quickSearchService.ClearSearch(); + FileSystemNodes.ForEach(x => x.IsFilteredOut = false); + } + } + + /// + /// We use specific handler for TextInput, and not reuse KeyDown, + /// since translation from Key to Char is language/keyboard dependent, + /// and should be done in caller level by Avalonia + /// + public void OnDataGridTextInputCallback(char symbol, bool isBackwardsDirectionEnabled) + { + if (!_quickSearchService.IsEnabled) + { + return; + } + + var nodes = CreateQuickSearchNodes(); + var filteredNodes = _quickSearchService.FilterNodes(symbol, isBackwardsDirectionEnabled, nodes); + UpdateFilterAfterQuickSearch(filteredNodes); + } + private void SortFiles(SortingMode sortingMode) { var sortingViewModel = SelectedTab.SortingViewModel; @@ -448,10 +481,13 @@ private bool CheckIfShouldShowNode(string nodePath) => private int GetSelectedIndex() { if (SelectedFileSystemNodes.Count == 0) + { return -1; + } + var oldSelected = SelectedFileSystemNodes.First(); var nodes = FileSystemNodes.ToList(); - for (int i = 0; i < nodes.Count; i++) + for (var i = 0; i < nodes.Count; i++) { var curr = nodes[i]; if (curr.FullPath == oldSelected.FullPath) @@ -459,93 +495,54 @@ private int GetSelectedIndex() return i; } } + return -1; } - private IFileSystemNodeViewModel GetSelected() + private IFileSystemNodeViewModel GetSelectedNode() { - int selectedIndex = GetSelectedIndex(); - if (selectedIndex < 0) - return null; + var selectedIndex = GetSelectedIndex(); - var nodes = FileSystemNodes.ToList(); - var selected = nodes[selectedIndex]; - return selected; + return selectedIndex < 0 ? null : _fileSystemNodes[selectedIndex]; } - private void SelectNodeEx(string newSelected, IFileSystemNodeViewModel oldSelected) + private void ChangeSelectedNode(string newSelected, IFileSystemNodeViewModel oldSelected) { - if (newSelected != null) + if (newSelected is null) { - if (oldSelected != null) - { - UnselectNode(oldSelected.FullPath); - } - SelectNode(newSelected); + return; } - } - public void OnDataGridKeyDownCallback(Key key) - { - if (_quickSearchService.Enabled()) + if (oldSelected is not null) { - if (key == Key.Escape) - { - _quickSearchService.ClearSearch(); - FileSystemNodes.ForEach(x => x.IsFilteredOut = false); - } + UnselectNode(oldSelected.FullPath); } - } - /// - /// We use specific handler for TextInput, and not reuse KeyDown, - /// since translation from Key to Char is language/keyboard dependent, - /// and should be done in caller level by Avalonia - /// - /// - /// - /// - public void OnDataGridTextInputCallback(string text, bool isShiftDown) - { - if (_quickSearchService.Enabled()) - { - if (string.IsNullOrEmpty(text) || text.Length != 1) - throw new ArgumentOutOfRangeException(nameof(text)); - - char c = text[0]; - var files = CreateQuickSearchFiles(); - _quickSearchService.OnCharDown(c, isShiftDown, files, out bool handled); - if (handled) - UpdateFilterAfterQuickSearch(files); - } + SelectNode(newSelected); } - private void UpdateFilterAfterQuickSearch(List files) + private void UpdateFilterAfterQuickSearch(IReadOnlyList nodes) { - if (!_quickSearchService.Enabled()) - throw new InvalidOperationException(); - string newSelected = null; - foreach (var file in files) + + var startIndex = ParentDirectory is null ? 0 : 1; + for (var i = startIndex; i < _fileSystemNodes.Count; i++) { - var node = (IFileSystemNodeViewModel)file.Tag; - node.IsFilteredOut = !file.Found; - if (file.Selected) + var node = _fileSystemNodes[i]; + var quickSearchNode = nodes[i]; + node.IsFilteredOut = !quickSearchNode.IsFiltered; + if (quickSearchNode.Selected) + { newSelected = node.FullPath; + } } - var oldSelected = GetSelected(); - SelectNodeEx(newSelected, oldSelected); + var oldSelected = GetSelectedNode(); + ChangeSelectedNode(newSelected, oldSelected); } - private List CreateQuickSearchFiles() - { - if (!_quickSearchService.Enabled()) - throw new InvalidOperationException(); - - var result = FileSystemNodes - .Select(x => new QuickSearchFileModel() { Name = x.Name, Tag = x }) + private IReadOnlyList CreateQuickSearchNodes() => + FileSystemNodes + .Select(n => n.Name) .ToList(); - return result; - } } diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs index 0885e9c0..6cf9f79b 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Nodes/FileSystemNodeViewModelBase.cs @@ -27,8 +27,10 @@ public abstract class FileSystemNodeViewModelBase : ViewModelBase, IFileSystemNo [Reactive] public bool IsEditing { get; set; } + [Reactive] public bool IsFilteredOut { get; set; } + public bool IsArchive => _fileSystemNodeFacade.CheckIfNodeIsArchive(FullPath); public bool ShouldShowOpenSubmenu { get; } diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs index 6c36f518..9c1e3ba9 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs @@ -24,11 +24,8 @@ public interface IFilesPanelViewModel IClipboardOperationsViewModel ClipboardOperationsViewModel { get; } IList SelectedFileSystemNodes { get; } - - IEnumerable FileSystemNodes { get; } - void OnDataGridTextInputCallback(string text, bool isShiftDown); - void OnDataGridKeyDownCallback(Key key); + IEnumerable FileSystemNodes { get; } bool IsActive { get; } @@ -49,4 +46,8 @@ public interface IFilesPanelViewModel void Activate(); void Deactivate(); + + void OnDataGridTextInputCallback(char symbol, bool isShiftDown); + + void OnDataGridKeyDownCallback(Key key); } \ No newline at end of file diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs index a93bf84e..9ec53974 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Nodes/IFileSystemNodeViewModel.cs @@ -9,6 +9,7 @@ public interface IFileSystemNodeViewModel string Name { get; set; } bool IsEditing { get; set; } + bool IsFilteredOut { get; set; } ICommand OpenCommand { get; } diff --git a/src/Camelot/Views/Main/FilesPanelView.xaml.cs b/src/Camelot/Views/Main/FilesPanelView.xaml.cs index b2cf529a..f379b01f 100644 --- a/src/Camelot/Views/Main/FilesPanelView.xaml.cs +++ b/src/Camelot/Views/Main/FilesPanelView.xaml.cs @@ -167,7 +167,7 @@ private void OnDataGridDoubleTapped(object sender, RoutedEventArgs args) StopEditing(nodeViewModel); nodeViewModel.OpenCommand.Execute(null); } - + private void OnDataGridKeyDown(object sender, KeyEventArgs args) { if (args.Key == Key.Delete || args.Key == Key.Back) @@ -182,7 +182,7 @@ private void OnDataGridKeyDown(object sender, KeyEventArgs args) } /// - /// Needed to get state of shift key + /// Needed to get state of shift key /// /// /// @@ -193,8 +193,12 @@ private void OnDataGridKeyUp(object sender, KeyEventArgs args) private void OnDataGridTextInput(object sender, TextInputEventArgs args) { - ViewModel.OnDataGridTextInputCallback(args.Text, _shiftDown); + if (args.Text is not null && args.Text.Length == 1) + { + ViewModel.OnDataGridTextInputCallback(args.Text[0], _shiftDown); + } } + private void OnDataGridCellPointerPressed(object sender, DataGridCellPointerPressedEventArgs args) { ActivateViewModel(); @@ -209,7 +213,7 @@ private void OnDataGridCellPointerPressed(object sender, DataGridCellPointerPres private void ProcessPointerClickInCell(PointerEventArgs args, IDataContextProvider cell) { var point = args.GetCurrentPoint(this); - if (point.Properties.IsMiddleButtonPressed + if (point.Properties.IsMiddleButtonPressed && cell.DataContext is IDirectoryViewModel directoryViewModel) { args.Handled = true; From 0493d50771f48a7f10f4b9e94a4239837096b3ee Mon Sep 17 00:00:00 2001 From: IngvarX Date: Sun, 12 Nov 2023 21:04:17 -0800 Subject: [PATCH 3/5] Used commands to call quick search in files panel vm --- .../WinApi/ShellIcon.cs | 1 - .../FilePanels/FilesPanelViewModel.cs | 60 ++++++++----------- .../FilePanels/IFilesPanelViewModel.cs | 9 ++- .../FilePanels/QuickSearchCommandModel.cs | 14 +++++ src/Camelot/Views/Main/FilesPanelView.xaml.cs | 31 +++++----- 5 files changed, 60 insertions(+), 55 deletions(-) create mode 100644 src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/QuickSearchCommandModel.cs diff --git a/src/Camelot.ViewModels.Windows/WinApi/ShellIcon.cs b/src/Camelot.ViewModels.Windows/WinApi/ShellIcon.cs index bc3a6d9a..dcf74f76 100644 --- a/src/Camelot.ViewModels.Windows/WinApi/ShellIcon.cs +++ b/src/Camelot.ViewModels.Windows/WinApi/ShellIcon.cs @@ -1,5 +1,4 @@ using Camelot.Services.Windows.WinApi; -using System.Diagnostics; namespace Camelot.ViewModels.Windows.WinApi; diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs index f8adb4d9..64e6211f 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs @@ -6,7 +6,6 @@ using System.Threading; using System.Threading.Tasks; using System.Windows.Input; -using Avalonia.Input; using Camelot.Avalonia.Interfaces; using Camelot.Extensions; using Camelot.Services.Abstractions; @@ -82,7 +81,6 @@ public class FilesPanelViewModel : ViewModelBase, IFilesPanelViewModel public IList SelectedFileSystemNodes => _selectedFileSystemNodes; - [Reactive] public IFileSystemNodeViewModel CurrentNode { get; private set; } @@ -112,12 +110,17 @@ public string CurrentDirectory public ICommand ActivateCommand { get; } + public ICommand QuickSearchCommand { get; } + + public ICommand ClearQuickSearchCommand { get; } + public ICommand RefreshCommand { get; } public ICommand GoToParentDirectoryCommand { get; } public ICommand SortFilesCommand { get; } + public FilesPanelViewModel( IFileService fileService, IDirectoryService directoryService, @@ -166,6 +169,8 @@ public FilesPanelViewModel( _selectedFileSystemNodes = new ObservableCollection(); ActivateCommand = ReactiveCommand.Create(Activate); + QuickSearchCommand = ReactiveCommand.Create(QuickSearch); + ClearQuickSearchCommand = ReactiveCommand.Create(ClearQuickSearch); RefreshCommand = ReactiveCommand.Create(ReloadFiles); var canGoToParentDirectory = this.WhenAnyValue(vm => vm.ParentDirectory, (DirectoryModel dm) => dm is not null); @@ -193,38 +198,6 @@ public void Deactivate() SelectedTab.IsGloballyActive = false; } - - public void OnDataGridKeyDownCallback(Key key) - { - if (!_quickSearchService.IsEnabled) - { - return; - } - - if (key == Key.Escape) - { - _quickSearchService.ClearSearch(); - FileSystemNodes.ForEach(x => x.IsFilteredOut = false); - } - } - - /// - /// We use specific handler for TextInput, and not reuse KeyDown, - /// since translation from Key to Char is language/keyboard dependent, - /// and should be done in caller level by Avalonia - /// - public void OnDataGridTextInputCallback(char symbol, bool isBackwardsDirectionEnabled) - { - if (!_quickSearchService.IsEnabled) - { - return; - } - - var nodes = CreateQuickSearchNodes(); - var filteredNodes = _quickSearchService.FilterNodes(symbol, isBackwardsDirectionEnabled, nodes); - UpdateFilterAfterQuickSearch(filteredNodes); - } - private void SortFiles(SortingMode sortingMode) { var sortingViewModel = SelectedTab.SortingViewModel; @@ -541,6 +514,25 @@ private void UpdateFilterAfterQuickSearch(IReadOnlyList no ChangeSelectedNode(newSelected, oldSelected); } + private void QuickSearch(QuickSearchCommandModel parameter) + { + if (!_quickSearchService.IsEnabled) + { + return; + } + + var nodes = CreateQuickSearchNodes(); + var filteredNodes = _quickSearchService.FilterNodes( + parameter.Symbol, parameter.IsBackwardsDirectionEnabled, nodes); + UpdateFilterAfterQuickSearch(filteredNodes); + } + + private void ClearQuickSearch() + { + _quickSearchService.ClearSearch(); + FileSystemNodes.ForEach(x => x.IsFilteredOut = false); + } + private IReadOnlyList CreateQuickSearchNodes() => FileSystemNodes .Select(n => n.Name) diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs index 9c1e3ba9..f0f4671a 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Windows.Input; -using Avalonia.Input; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Tabs; using Camelot.ViewModels.Interfaces.MainWindow.Operations; @@ -43,11 +42,11 @@ public interface IFilesPanelViewModel ICommand ActivateCommand { get; } - void Activate(); + ICommand QuickSearchCommand { get; } - void Deactivate(); + ICommand ClearQuickSearchCommand { get; } - void OnDataGridTextInputCallback(char symbol, bool isShiftDown); + void Activate(); - void OnDataGridKeyDownCallback(Key key); + void Deactivate(); } \ No newline at end of file diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/QuickSearchCommandModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/QuickSearchCommandModel.cs new file mode 100644 index 00000000..93e39a3b --- /dev/null +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/QuickSearchCommandModel.cs @@ -0,0 +1,14 @@ +namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels; + +public class QuickSearchCommandModel +{ + public char Symbol { get; init; } + + public bool IsBackwardsDirectionEnabled { get; init; } + + public QuickSearchCommandModel(char symbol, bool isBackwardsDirectionEnabled) + { + Symbol = symbol; + IsBackwardsDirectionEnabled = isBackwardsDirectionEnabled; + } +} \ No newline at end of file diff --git a/src/Camelot/Views/Main/FilesPanelView.xaml.cs b/src/Camelot/Views/Main/FilesPanelView.xaml.cs index f379b01f..e2fd19f8 100644 --- a/src/Camelot/Views/Main/FilesPanelView.xaml.cs +++ b/src/Camelot/Views/Main/FilesPanelView.xaml.cs @@ -1,26 +1,20 @@ using System; using System.ComponentModel; -using System.Globalization; using System.Linq; using System.Threading.Tasks; using System.Timers; -using System.Xml.Serialization; using Avalonia; using Avalonia.Controls; -using Avalonia.Data.Converters; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Markup.Xaml; -using Avalonia.VisualTree; using Camelot.Avalonia.Interfaces; using Camelot.DependencyInjection; using Camelot.Extensions; -using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Nodes; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; using Camelot.Views.Main.Controls; using DynamicData; -using ReactiveUI; using Splat; namespace Camelot.Views.Main; @@ -85,7 +79,7 @@ private void ViewModelOnSelectionAdded(object sender, SelectionAddedEventArgs e) FilesDataGrid.SelectedItems.Add(item); } - // In case event was triggerd by keyboard (up/down arrows, quick-search, etc), + // In case event was triggered by keyboard (up/down arrows, quick-search, etc), // Need to make sure item is viewable if (FilesDataGrid.SelectedItems.Count == 1) { @@ -170,15 +164,20 @@ private void OnDataGridDoubleTapped(object sender, RoutedEventArgs args) private void OnDataGridKeyDown(object sender, KeyEventArgs args) { - if (args.Key == Key.Delete || args.Key == Key.Back) + if (args.Key is Key.Delete or Key.Back) { args.Handled = true; ViewModel.OperationsViewModel.MoveToTrashCommand.Execute(null); + return; } - _shiftDown = (args.KeyModifiers & KeyModifiers.Shift) > 0; - ViewModel.OnDataGridKeyDownCallback(args.Key); + UpdateShiftKeyStatus(args); + + if (args.Key == Key.Escape) + { + ViewModel.ClearQuickSearchCommand.Execute(null); + } } /// @@ -186,16 +185,15 @@ private void OnDataGridKeyDown(object sender, KeyEventArgs args) /// /// /// - private void OnDataGridKeyUp(object sender, KeyEventArgs args) - { - _shiftDown = (args.KeyModifiers & KeyModifiers.Shift) > 0; - } + private void OnDataGridKeyUp(object sender, KeyEventArgs args) => UpdateShiftKeyStatus(args); private void OnDataGridTextInput(object sender, TextInputEventArgs args) { if (args.Text is not null && args.Text.Length == 1) { - ViewModel.OnDataGridTextInputCallback(args.Text[0], _shiftDown); + var parameter = new QuickSearchCommandModel(args.Text[0], _shiftDown); + + ViewModel.QuickSearchCommand.Execute(parameter); } } @@ -387,5 +385,8 @@ private async void DataGridOnContextMenuOpening(object sender, CancelEventArgs e item.IsVisible = await ViewModel.ClipboardOperationsViewModel.CanPasteAsync(); } } + + private void UpdateShiftKeyStatus(KeyEventArgs args) => + _shiftDown = (args.KeyModifiers & KeyModifiers.Shift) > 0; } From 90bcd52ad4de64badb5644d910355ef3511aeefe Mon Sep 17 00:00:00 2001 From: IngvarX Date: Sun, 26 Nov 2023 23:36:59 -0800 Subject: [PATCH 4/5] Refactored quick search --- .../IQuickSearchService.cs | 13 +- .../Models/QuickSearchNodeModel.cs | 11 -- src/Camelot.Services/QuickSearchService.cs | 116 +------------ .../FilePanels/FilesPanelViewModel.cs | 153 ++++++++---------- .../FilePanels/QuickSearchViewModel.cs | 106 ++++++++++++ .../MainWindow/FilePanels/SearchViewModel.cs | 4 +- .../QuickSearchEmptySpecification.cs | 9 ++ .../QuickSearch/QuickSearchSpecification.cs | 25 +++ .../{ => Search}/EmptySpecification.cs | 2 +- .../NodeNameRegexSpecification.cs | 2 +- .../{ => Search}/NodeNameTextSpecification.cs | 2 +- .../{ => Search}/SpecificationBase.cs | 2 +- .../QuickSearchFilterChangedEventArgs.cs | 11 ++ .../SelectionAddedEventArgs.cs | 6 +- .../EventArgs/SelectionChangeDirection.cs | 8 + .../SelectionRemovedEventArgs.cs | 6 +- .../FilePanels/IDirectorySelectorViewModel.cs | 2 +- .../FilePanels/IFilesPanelViewModel.cs | 13 +- .../FilePanels/IQuickSearchViewModel.cs | 20 +++ .../MainWindow/FilePanels/ISearchViewModel.cs | 2 +- .../Tabs/IFileSystemNodesSortingViewModel.cs | 2 +- .../FilePanels/Tabs/ITabViewModel.cs | 14 +- .../FilePanels/Tabs/ITabsListViewModel.cs | 2 +- .../Tabs/TabMoveRequestedEventArgs.cs | 4 +- .../ViewModelsBootstrapper.cs | 8 +- src/Camelot/Views/Main/FilesPanelView.xaml.cs | 10 +- .../FilePanels/SearchViewModelTests.cs | 2 +- .../NodeNameRegexSpecificationTests.cs | 2 +- .../NodeNameTextSpecificationTests.cs | 2 +- 29 files changed, 300 insertions(+), 259 deletions(-) delete mode 100644 src/Camelot.Services.Abstractions/Models/QuickSearchNodeModel.cs create mode 100644 src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/QuickSearchViewModel.cs create mode 100644 src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchEmptySpecification.cs create mode 100644 src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchSpecification.cs rename src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/{ => Search}/EmptySpecification.cs (92%) rename src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/{ => Search}/NodeNameRegexSpecification.cs (97%) rename src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/{ => Search}/NodeNameTextSpecification.cs (96%) rename src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/{ => Search}/SpecificationBase.cs (94%) create mode 100644 src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/QuickSearchFilterChangedEventArgs.cs rename src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/{ => EventArgs}/SelectionAddedEventArgs.cs (50%) create mode 100644 src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionChangeDirection.cs rename src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/{ => EventArgs}/SelectionRemovedEventArgs.cs (50%) create mode 100644 src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IQuickSearchViewModel.cs diff --git a/src/Camelot.Services.Abstractions/IQuickSearchService.cs b/src/Camelot.Services.Abstractions/IQuickSearchService.cs index 94ba0996..31bc45af 100644 --- a/src/Camelot.Services.Abstractions/IQuickSearchService.cs +++ b/src/Camelot.Services.Abstractions/IQuickSearchService.cs @@ -1,5 +1,5 @@ +using System; using Camelot.Services.Abstractions.Models; -using System.Collections.Generic; namespace Camelot.Services.Abstractions; @@ -7,16 +7,9 @@ public interface IQuickSearchService { bool IsEnabled { get; } + event EventHandler QuickSearchModeChanged; + QuickSearchModel GetQuickSearchSettings(); void SaveQuickSearchSettings(QuickSearchModel quickSearchModel); - - /// - /// This arg is is of type 'char' and not 'Key', since translation from Key to char - /// is language/keyboard dependent, and should be done in caller level by Avalonia. - /// - /// - IReadOnlyList FilterNodes(char symbol, bool isBackwardsDirectionEnabled, IReadOnlyList nodes); - - void ClearSearch(); } \ No newline at end of file diff --git a/src/Camelot.Services.Abstractions/Models/QuickSearchNodeModel.cs b/src/Camelot.Services.Abstractions/Models/QuickSearchNodeModel.cs deleted file mode 100644 index 271dd4bf..00000000 --- a/src/Camelot.Services.Abstractions/Models/QuickSearchNodeModel.cs +++ /dev/null @@ -1,11 +0,0 @@ - -namespace Camelot.Services.Abstractions.Models; - -public record QuickSearchNodeModel -{ - public string Name { get; init; } - - public bool IsFiltered { get; init; } - - public bool Selected { get; set; } -} diff --git a/src/Camelot.Services/QuickSearchService.cs b/src/Camelot.Services/QuickSearchService.cs index 3fd2d0e3..5cecad95 100644 --- a/src/Camelot.Services/QuickSearchService.cs +++ b/src/Camelot.Services/QuickSearchService.cs @@ -1,15 +1,12 @@ using System; -using System.Collections.Generic; -using System.Linq; using Camelot.DataAccess.UnitOfWork; +using Camelot.Extensions; using Camelot.Services.Abstractions; using Camelot.Services.Abstractions.Models; using Camelot.Services.Abstractions.Models.Enums; namespace Camelot.Services; -// Name of feature as "Quick search" is based on same name used by Total-Commander. -// Changing opacity of filtered items is based on muCommander. public class QuickSearchService : IQuickSearchService { private const string SettingsId = "QuickSearchSettings"; @@ -17,12 +14,11 @@ public class QuickSearchService : IQuickSearchService private readonly IUnitOfWorkFactory _unitOfWorkFactory; private QuickSearchModel _cachedSettingsValue; - private string _searchWord = string.Empty; - private char _searchLetter = Char.MinValue; - private int _selectedIndex = -1; public bool IsEnabled => _cachedSettingsValue.SelectedMode != QuickSearchMode.Disabled; + public event EventHandler QuickSearchModeChanged; + public QuickSearchService(IUnitOfWorkFactory unitOfWorkFactory) { _unitOfWorkFactory = unitOfWorkFactory; @@ -44,108 +40,6 @@ public QuickSearchModel GetQuickSearchSettings() return _cachedSettingsValue = dbModel; } - public IReadOnlyList FilterNodes( - char symbol, bool isBackwardsDirectionEnabled, IReadOnlyList nodes) - { - var lowercaseSymbol = Char.ToLower(symbol); - switch (_cachedSettingsValue.SelectedMode) - { - case QuickSearchMode.Letter: - { - if (_searchLetter != lowercaseSymbol) - { - _selectedIndex = -1; - } - - _searchLetter = lowercaseSymbol; - break; - } - case QuickSearchMode.Word: - { - _searchWord += lowercaseSymbol; - break; - } - default: - throw new ArgumentOutOfRangeException( - nameof(_cachedSettingsValue.SelectedMode), _cachedSettingsValue.SelectedMode, null); - } - - var result = nodes - .Select(n => new QuickSearchNodeModel {Name = n, IsFiltered = CheckIfShouldIncludeInSearchResults(n)}) - .ToList(); - if (result.Any(n => n.IsFiltered)) - { - SetSelectedItem(result, isBackwardsDirectionEnabled); - } - - return result; - } - - /// - /// Set value of - /// which indicates to UI which item should be selected. - /// - private void SetSelectedItem(IReadOnlyList files, bool isBackwardsDirectionEnabled) - { - _selectedIndex = ComputeNewSelectedIndex(files, _selectedIndex, isBackwardsDirectionEnabled); - if (_selectedIndex >= 0) - { - var file = files[_selectedIndex]; - file.Selected = true; - } - } - - private static int ComputeNewSelectedIndex( - IReadOnlyList files, - int selectedIndex, - bool isBackwardsDirectionEnabled) - { - int start, jump; - if (isBackwardsDirectionEnabled) - { - start = selectedIndex > -1 ? selectedIndex - 1 : 0; - jump = -1; - } - else - { - start = selectedIndex > -1 ? selectedIndex + 1 : 0; - jump = 1; - } - - start = (start + files.Count) % files.Count; - - for (var i = start; i != start - jump; i = (i + jump + files.Count) % files.Count) - { - var file = files[i]; - if (file.IsFiltered) - { - return i; - } - } - - return -1; - } - - private bool CheckIfShouldIncludeInSearchResults(string nodeName) => - _cachedSettingsValue.SelectedMode switch - { - QuickSearchMode.Letter => char.ToLower(nodeName[0]) == _searchLetter, - QuickSearchMode.Word => nodeName.StartsWith(_searchWord, StringComparison.OrdinalIgnoreCase), - _ => throw new ArgumentOutOfRangeException(nameof(_cachedSettingsValue.SelectedMode), _cachedSettingsValue, null) - }; - - public void ClearSearch() - { - if (!IsEnabled) - { - return; - } - - _searchWord = string.Empty; - _searchLetter = Char.MinValue; - _selectedIndex = -1; - } - public void SaveQuickSearchSettings(QuickSearchModel quickSearchModel) { using var uow = _unitOfWorkFactory.Create(); @@ -153,5 +47,9 @@ public void SaveQuickSearchSettings(QuickSearchModel quickSearchModel) repository.Upsert(SettingsId, quickSearchModel); _cachedSettingsValue = quickSearchModel; + + RaiseQuickSearchModeChangedEvent(); } + + private void RaiseQuickSearchModeChangedEvent() => QuickSearchModeChanged.Raise(this, EventArgs.Empty); } diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs index 64e6211f..594fca4c 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/FilesPanelViewModel.cs @@ -18,6 +18,7 @@ using Camelot.ViewModels.Implementations.Dialogs; using Camelot.ViewModels.Implementations.Dialogs.NavigationParameters; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Tabs; using Camelot.ViewModels.Interfaces.MainWindow.Operations; @@ -44,14 +45,14 @@ public class FilesPanelViewModel : ViewModelBase, IFilesPanelViewModel private readonly IFilePanelDirectoryObserver _filePanelDirectoryObserver; private readonly IPermissionsService _permissionsService; private readonly IDialogService _dialogService; - private readonly IQuickSearchService _quickSearchService; private readonly ObservableCollection _fileSystemNodes; private readonly ObservableCollection _selectedFileSystemNodes; private CancellationTokenSource _cancellationTokenSource; private string _currentDirectory; - private INodeSpecification _specification; + private INodeSpecification _searchSpecification; + private ISpecification _filterSpecification; private IEnumerable SelectedFiles => _selectedFileSystemNodes.OfType(); @@ -65,6 +66,8 @@ public class FilesPanelViewModel : ViewModelBase, IFilesPanelViewModel public ISearchViewModel SearchViewModel { get; } + public IQuickSearchViewModel QuickSearchViewModel { get; } + public ITabsListViewModel TabsListViewModel { get; } public IOperationsViewModel OperationsViewModel { get; } @@ -110,17 +113,12 @@ public string CurrentDirectory public ICommand ActivateCommand { get; } - public ICommand QuickSearchCommand { get; } - - public ICommand ClearQuickSearchCommand { get; } - public ICommand RefreshCommand { get; } public ICommand GoToParentDirectoryCommand { get; } public ICommand SortFilesCommand { get; } - public FilesPanelViewModel( IFileService fileService, IDirectoryService directoryService, @@ -136,12 +134,12 @@ public FilesPanelViewModel( IPermissionsService permissionsService, IDialogService dialogService, ISearchViewModel searchViewModel, + IQuickSearchViewModel quickSearchViewModel, ITabsListViewModel tabsListViewModel, IOperationsViewModel operationsViewModel, IDirectorySelectorViewModel directorySelectorViewModel, IDragAndDropOperationsMediator dragAndDropOperationsMediator, - IClipboardOperationsViewModel clipboardOperationsViewModel, - IQuickSearchService quickSearchService) + IClipboardOperationsViewModel clipboardOperationsViewModel) { _fileService = fileService; _directoryService = directoryService; @@ -156,9 +154,9 @@ public FilesPanelViewModel( _filePanelDirectoryObserver = filePanelDirectoryObserver; _permissionsService = permissionsService; _dialogService = dialogService; - _quickSearchService = quickSearchService; SearchViewModel = searchViewModel; + QuickSearchViewModel = quickSearchViewModel; TabsListViewModel = tabsListViewModel; OperationsViewModel = operationsViewModel; DirectorySelectorViewModel = directorySelectorViewModel; @@ -169,8 +167,6 @@ public FilesPanelViewModel( _selectedFileSystemNodes = new ObservableCollection(); ActivateCommand = ReactiveCommand.Create(Activate); - QuickSearchCommand = ReactiveCommand.Create(QuickSearch); - ClearQuickSearchCommand = ReactiveCommand.Create(ClearQuickSearch); RefreshCommand = ReactiveCommand.Create(ReloadFiles); var canGoToParentDirectory = this.WhenAnyValue(vm => vm.ParentDirectory, (DirectoryModel dm) => dm is not null); @@ -219,6 +215,7 @@ private void SubscribeToEvents() SearchViewModel.SearchSettingsChanged += SearchViewModelOnSearchSettingsChanged; _filePanelDirectoryObserver.CurrentDirectoryChanged += async (_, _) => await UpdateStateAsync(); _selectedFileSystemNodes.CollectionChanged += SelectedFileSystemNodesOnCollectionChanged; + QuickSearchViewModel.QuickSearchFilterChanged += QuickSearchViewModelOnQuickSearchFilterChanged; _fileSystemWatchingService.NodeCreated += (_, args) => ExecuteInUiThread(() => InsertNode(args.Node)); @@ -268,7 +265,7 @@ private async Task UpdateStateAsync() CurrentDirectoryChanged.Raise(this, EventArgs.Empty); this.RaisePropertyChanged(nameof(ParentDirectory)); - _quickSearchService.ClearSearch(); + QuickSearchViewModel.ClearQuickSearch(); } private void UpdateNode(string nodePath) => RecreateNode(nodePath, nodePath); @@ -295,6 +292,7 @@ private void InsertNode(string nodePath, bool isSelected = false) var index = GetInsertIndex(newNodeModel); _fileSystemNodes.Insert(index, newNodeModel); + UpdateNodeFiltering(newNodeModel); if (isSelected) { @@ -340,14 +338,14 @@ private void ReloadFiles() { CancelPreviousSearchIfNeeded(); - _specification = SearchViewModel.GetSpecification(); - if (_specification.IsRecursive) + _searchSpecification = SearchViewModel.GetSpecification(); + if (_searchSpecification.IsRecursive) { - RecursiveSearch(_specification); + RecursiveSearch(_searchSpecification); } else { - Search(_specification); + Search(_searchSpecification); } InsertParentDirectory(); @@ -432,6 +430,27 @@ private void SelectedFileSystemNodesOnCollectionChanged(object sender, NotifyCol this.RaisePropertyChanged(nameof(AreAnyFileSystemNodesSelected)); } + private void QuickSearchViewModelOnQuickSearchFilterChanged(object sender, QuickSearchFilterChangedEventArgs e) + { + _filterSpecification = QuickSearchViewModel.GetSpecification(); + _fileSystemNodes.ForEach(UpdateNodeFiltering); + + MoveSelection(e.Direction); + } + + private void MoveSelection(SelectionChangeDirection direction) + { + switch (direction) + { + case SelectionChangeDirection.Backward: + MoveSelection(-1); + break; + case SelectionChangeDirection.Forward: + MoveSelection(1); + break; + } + } + private int GetInsertIndex(IFileSystemNodeViewModel newNodeViewModel) { var comparer = GetComparer(); @@ -440,8 +459,7 @@ private int GetInsertIndex(IFileSystemNodeViewModel newNodeViewModel) return index < 0 ? index ^ -1 : index; } - private IComparer GetComparer() => - _comparerFactory.Create(SelectedTab.SortingViewModel); + private IComparer GetComparer() => _comparerFactory.Create(SelectedTab.SortingViewModel); private IFileSystemNodeViewModel GetViewModel(string nodePath) => _fileSystemNodes.FirstOrDefault(n => n.FullPath == nodePath); @@ -449,92 +467,63 @@ private IFileSystemNodeViewModel GetViewModel(string nodePath) => private void ExecuteInUiThread(Action action) => _applicationDispatcher.Dispatch(action); private bool CheckIfShouldShowNode(string nodePath) => - _specification?.IsSatisfiedBy(_nodeService.GetNode(nodePath)) ?? true; + _searchSpecification?.IsSatisfiedBy(_nodeService.GetNode(nodePath)) ?? true; - private int GetSelectedIndex() + private void MoveSelection(int step) { - if (SelectedFileSystemNodes.Count == 0) - { - return -1; - } - - var oldSelected = SelectedFileSystemNodes.First(); - var nodes = FileSystemNodes.ToList(); - for (var i = 0; i < nodes.Count; i++) - { - var curr = nodes[i]; - if (curr.FullPath == oldSelected.FullPath) - { - return i; - } - } + var (selectedIndex, selectedNode) = GetSelectedNodeWithIndex(); + var newNode = GetNewSelectedNode(selectedIndex, step); - return -1; + ChangeSelectedNode(selectedNode, newNode); } - private IFileSystemNodeViewModel GetSelectedNode() + private IFileSystemNodeViewModel GetNewSelectedNode(int selectedIndex, int step) { - var selectedIndex = GetSelectedIndex(); - - return selectedIndex < 0 ? null : _fileSystemNodes[selectedIndex]; - } + var count = _fileSystemNodes.Count; - private void ChangeSelectedNode(string newSelected, IFileSystemNodeViewModel oldSelected) - { - if (newSelected is null) - { - return; - } + var start = selectedIndex > -1 ? selectedIndex + step : 0; + start = (start + count) % count; - if (oldSelected is not null) + for (var i = start; i != start - step; i = (i + step + count) % count) { - UnselectNode(oldSelected.FullPath); + var file = _fileSystemNodes[i]; + if (!file.IsFilteredOut) + { + return file; + } } - SelectNode(newSelected); + return null; } - private void UpdateFilterAfterQuickSearch(IReadOnlyList nodes) + private (int, IFileSystemNodeViewModel) GetSelectedNodeWithIndex() { - string newSelected = null; + // TODO: check if possible to improve + var currentSelectedNode = _selectedFileSystemNodes.FirstOrDefault(); - var startIndex = ParentDirectory is null ? 0 : 1; - for (var i = startIndex; i < _fileSystemNodes.Count; i++) - { - var node = _fileSystemNodes[i]; - var quickSearchNode = nodes[i]; - node.IsFilteredOut = !quickSearchNode.IsFiltered; - if (quickSearchNode.Selected) - { - newSelected = node.FullPath; - } - } - - var oldSelected = GetSelectedNode(); - ChangeSelectedNode(newSelected, oldSelected); + return currentSelectedNode is null + ? (-1, null) + : (_fileSystemNodes.IndexOf(currentSelectedNode), currentSelectedNode); } - private void QuickSearch(QuickSearchCommandModel parameter) + private void ChangeSelectedNode(IFileSystemNodeViewModel oldNode, IFileSystemNodeViewModel newNode) { - if (!_quickSearchService.IsEnabled) + if (newNode is null) { return; } - var nodes = CreateQuickSearchNodes(); - var filteredNodes = _quickSearchService.FilterNodes( - parameter.Symbol, parameter.IsBackwardsDirectionEnabled, nodes); - UpdateFilterAfterQuickSearch(filteredNodes); - } + if (oldNode is not null) + { + UnselectNode(oldNode.FullPath); + } - private void ClearQuickSearch() - { - _quickSearchService.ClearSearch(); - FileSystemNodes.ForEach(x => x.IsFilteredOut = false); + SelectNode(newNode.FullPath); } - private IReadOnlyList CreateQuickSearchNodes() => - FileSystemNodes - .Select(n => n.Name) - .ToList(); + private void UpdateNodeFiltering(IFileSystemNodeViewModel nodeViewModel) => + nodeViewModel.IsFilteredOut = CheckIfShouldFilterOutNode(nodeViewModel); + + private bool CheckIfShouldFilterOutNode(IFileSystemNodeViewModel nodeViewModel) => + !_filterSpecification?.IsSatisfiedBy(nodeViewModel) ?? false; } diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/QuickSearchViewModel.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/QuickSearchViewModel.cs new file mode 100644 index 00000000..a556b549 --- /dev/null +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/QuickSearchViewModel.cs @@ -0,0 +1,106 @@ +using System; +using System.Windows.Input; +using Camelot.Extensions; +using Camelot.Services.Abstractions; +using Camelot.Services.Abstractions.Models.Enums; +using Camelot.Services.Abstractions.Specifications; +using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.QuickSearch; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; +using ReactiveUI; + +namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels; + +public class QuickSearchViewModel : ViewModelBase, IQuickSearchViewModel +{ + private readonly IQuickSearchService _quickSearchService; + private readonly IPathService _pathService; + + private string _searchTerm; + + private QuickSearchMode QuickSearchMode => _quickSearchService.GetQuickSearchSettings().SelectedMode; + + public event EventHandler QuickSearchFilterChanged; + + public ICommand QuickSearchCommand { get; } + + public ICommand ClearQuickSearchCommand { get; } + + public QuickSearchViewModel( + IQuickSearchService quickSearchService, + IPathService pathService) + { + _quickSearchService = quickSearchService; + _pathService = pathService; + + _searchTerm = String.Empty; + + QuickSearchCommand = ReactiveCommand.Create(QuickSearch); + ClearQuickSearchCommand = ReactiveCommand.Create(ClearQuickSearch); + + SubscribeToEvents(); + } + + public ISpecification GetSpecification() + { + switch (QuickSearchMode) + { + case QuickSearchMode.Disabled: + return new QuickSearchEmptySpecification(); + case QuickSearchMode.Letter: + case QuickSearchMode.Word: + return new QuickSearchSpecification(_pathService, _searchTerm); + default: + throw new ArgumentOutOfRangeException(nameof(QuickSearchMode), QuickSearchMode, null); + } + } + + public void ClearQuickSearch() + { + _searchTerm = string.Empty; + RaiseQuickSearchFilterChangedEvent(); + } + + private void SubscribeToEvents() + { + _quickSearchService.QuickSearchModeChanged += (_, _) => ClearQuickSearch(); + } + + private void QuickSearch(QuickSearchCommandModel parameter) + { + switch (QuickSearchMode) + { + case QuickSearchMode.Disabled: + return; + case QuickSearchMode.Letter: + var newSearchTerm = parameter.Symbol.ToString(); + if (_searchTerm == newSearchTerm) + { + RaiseQuickSearchFilterChangedEvent(parameter.IsBackwardsDirectionEnabled); + } + else + { + _searchTerm = newSearchTerm; + RaiseQuickSearchFilterChangedEvent(parameter.IsBackwardsDirectionEnabled); + } + + break; + case QuickSearchMode.Word: + _searchTerm += parameter.Symbol; + RaiseQuickSearchFilterChangedEvent(parameter.IsBackwardsDirectionEnabled); + break; + default: + throw new ArgumentOutOfRangeException(nameof(QuickSearchMode), QuickSearchMode, null); + } + } + + private void RaiseQuickSearchFilterChangedEvent(bool isBackwardsDirectionEnabled) => + RaiseQuickSearchFilterChangedEvent(isBackwardsDirectionEnabled + ? SelectionChangeDirection.Backward + : SelectionChangeDirection.Forward); + + private void RaiseQuickSearchFilterChangedEvent( + SelectionChangeDirection direction = SelectionChangeDirection.Keep) => + QuickSearchFilterChanged.Raise(this, new QuickSearchFilterChangedEventArgs(direction)); +} \ No newline at end of file diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/SearchViewModel.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/SearchViewModel.cs index e4a7cb34..8390c391 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/SearchViewModel.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/SearchViewModel.cs @@ -5,7 +5,7 @@ using Camelot.Extensions; using Camelot.Services.Environment.Interfaces; using Camelot.ViewModels.Configuration; -using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels; using Camelot.ViewModels.Services.Interfaces; using ReactiveUI; @@ -38,7 +38,7 @@ public class SearchViewModel : ValidatableViewModelBase, ISearchViewModel [Reactive] public bool IsRecursiveSearchEnabled { get; set; } - public event EventHandler SearchSettingsChanged; + public event EventHandler SearchSettingsChanged; public ICommand ToggleSearchCommand { get; } diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchEmptySpecification.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchEmptySpecification.cs new file mode 100644 index 00000000..8bb46299 --- /dev/null +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchEmptySpecification.cs @@ -0,0 +1,9 @@ +using Camelot.Services.Abstractions.Specifications; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; + +namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.QuickSearch; + +public class QuickSearchEmptySpecification : ISpecification +{ + public bool IsSatisfiedBy(IFileSystemNodeViewModel node) => true; +} \ No newline at end of file diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchSpecification.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchSpecification.cs new file mode 100644 index 00000000..e01aa76d --- /dev/null +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/QuickSearch/QuickSearchSpecification.cs @@ -0,0 +1,25 @@ +using System; +using Camelot.Services.Abstractions; +using Camelot.Services.Abstractions.Specifications; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; + +namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.QuickSearch; + +public class QuickSearchSpecification : ISpecification +{ + private readonly IPathService _pathService; + private readonly string _searchTerm; + + public QuickSearchSpecification(IPathService pathService, string searchTerm) + { + _pathService = pathService; + _searchTerm = searchTerm; + } + + public bool IsSatisfiedBy(IFileSystemNodeViewModel node) + { + var name = _pathService.GetFileName(node.FullPath); + + return name.StartsWith(_searchTerm, StringComparison.OrdinalIgnoreCase); + } +} \ No newline at end of file diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/EmptySpecification.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/EmptySpecification.cs similarity index 92% rename from src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/EmptySpecification.cs rename to src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/EmptySpecification.cs index 9565b324..4f168d6e 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/EmptySpecification.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/EmptySpecification.cs @@ -1,6 +1,6 @@ using Camelot.Services.Abstractions.Models; -namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; public class EmptySpecification : SpecificationBase { diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/NodeNameRegexSpecification.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/NodeNameRegexSpecification.cs similarity index 97% rename from src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/NodeNameRegexSpecification.cs rename to src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/NodeNameRegexSpecification.cs index 235c8c6c..cb9e1006 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/NodeNameRegexSpecification.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/NodeNameRegexSpecification.cs @@ -2,7 +2,7 @@ using Camelot.Services.Abstractions.Models; using Camelot.Services.Environment.Interfaces; -namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; public class NodeNameRegexSpecification : SpecificationBase { diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/NodeNameTextSpecification.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/NodeNameTextSpecification.cs similarity index 96% rename from src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/NodeNameTextSpecification.cs rename to src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/NodeNameTextSpecification.cs index a5126387..fb2284b2 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/NodeNameTextSpecification.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/NodeNameTextSpecification.cs @@ -1,7 +1,7 @@ using System; using Camelot.Services.Abstractions.Models; -namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; public class NodeNameTextSpecification : SpecificationBase { diff --git a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/SpecificationBase.cs b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/SpecificationBase.cs similarity index 94% rename from src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/SpecificationBase.cs rename to src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/SpecificationBase.cs index 33de3deb..2e4aaaa3 100644 --- a/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/SpecificationBase.cs +++ b/src/Camelot.ViewModels/Implementations/MainWindow/FilePanels/Specifications/Search/SpecificationBase.cs @@ -1,7 +1,7 @@ using Camelot.Services.Abstractions.Models; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels; -namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +namespace Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; public abstract class SpecificationBase : INodeSpecification { diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/QuickSearchFilterChangedEventArgs.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/QuickSearchFilterChangedEventArgs.cs new file mode 100644 index 00000000..281ca2f2 --- /dev/null +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/QuickSearchFilterChangedEventArgs.cs @@ -0,0 +1,11 @@ +namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; + +public class QuickSearchFilterChangedEventArgs : System.EventArgs +{ + public SelectionChangeDirection Direction { get; } + + public QuickSearchFilterChangedEventArgs(SelectionChangeDirection direction) + { + Direction = direction; + } +} \ No newline at end of file diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/SelectionAddedEventArgs.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionAddedEventArgs.cs similarity index 50% rename from src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/SelectionAddedEventArgs.cs rename to src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionAddedEventArgs.cs index c3c56a4e..6b382031 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/SelectionAddedEventArgs.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionAddedEventArgs.cs @@ -1,8 +1,6 @@ -using System; +namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; -namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels; - -public class SelectionAddedEventArgs : EventArgs +public class SelectionAddedEventArgs : System.EventArgs { public string NodePath { get; } diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionChangeDirection.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionChangeDirection.cs new file mode 100644 index 00000000..3fa126bb --- /dev/null +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionChangeDirection.cs @@ -0,0 +1,8 @@ +namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; + +public enum SelectionChangeDirection +{ + Forward, + Keep, + Backward +} \ No newline at end of file diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/SelectionRemovedEventArgs.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionRemovedEventArgs.cs similarity index 50% rename from src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/SelectionRemovedEventArgs.cs rename to src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionRemovedEventArgs.cs index 2692081c..653e2be1 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/SelectionRemovedEventArgs.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/EventArgs/SelectionRemovedEventArgs.cs @@ -1,8 +1,6 @@ -using System; +namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; -namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels; - -public class SelectionRemovedEventArgs : EventArgs +public class SelectionRemovedEventArgs : System.EventArgs { public string NodePath { get; } diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IDirectorySelectorViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IDirectorySelectorViewModel.cs index 5fec2be9..e5b34c67 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IDirectorySelectorViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IDirectorySelectorViewModel.cs @@ -9,7 +9,7 @@ public interface IDirectorySelectorViewModel bool ShouldShowSuggestions { get; set; } - event EventHandler ActivationRequested; + event EventHandler ActivationRequested; ICommand ToggleFavouriteStatusCommand { get; } diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs index f0f4671a..8ecff8de 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IFilesPanelViewModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Windows.Input; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Tabs; using Camelot.ViewModels.Interfaces.MainWindow.Operations; @@ -14,6 +15,8 @@ public interface IFilesPanelViewModel ISearchViewModel SearchViewModel { get; } + IQuickSearchViewModel QuickSearchViewModel { get; } + IOperationsViewModel OperationsViewModel { get; } IDirectorySelectorViewModel DirectorySelectorViewModel { get; } @@ -30,11 +33,11 @@ public interface IFilesPanelViewModel string CurrentDirectory { get; set; } - event EventHandler Activated; + event EventHandler Activated; - event EventHandler Deactivated; + event EventHandler Deactivated; - event EventHandler CurrentDirectoryChanged; + event EventHandler CurrentDirectoryChanged; event EventHandler SelectionAdded; @@ -42,10 +45,6 @@ public interface IFilesPanelViewModel ICommand ActivateCommand { get; } - ICommand QuickSearchCommand { get; } - - ICommand ClearQuickSearchCommand { get; } - void Activate(); void Deactivate(); diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IQuickSearchViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IQuickSearchViewModel.cs new file mode 100644 index 00000000..fccfa3a2 --- /dev/null +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/IQuickSearchViewModel.cs @@ -0,0 +1,20 @@ +using System; +using System.Windows.Input; +using Camelot.Services.Abstractions.Specifications; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; + +namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels; + +public interface IQuickSearchViewModel +{ + event EventHandler QuickSearchFilterChanged; + + ICommand QuickSearchCommand { get; } + + ICommand ClearQuickSearchCommand { get; } + + ISpecification GetSpecification(); + + void ClearQuickSearch(); +} \ No newline at end of file diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/ISearchViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/ISearchViewModel.cs index 2b2c20df..11f9bfab 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/ISearchViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/ISearchViewModel.cs @@ -6,7 +6,7 @@ public interface ISearchViewModel { bool IsSearchEnabled { get; } - event EventHandler SearchSettingsChanged; + event EventHandler SearchSettingsChanged; INodeSpecification GetSpecification(); diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/IFileSystemNodesSortingViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/IFileSystemNodesSortingViewModel.cs index 277f6543..4c1e7244 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/IFileSystemNodesSortingViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/IFileSystemNodesSortingViewModel.cs @@ -11,5 +11,5 @@ public interface IFileSystemNodesSortingViewModel void ToggleSortingDirection(); - event EventHandler SortingSettingsChanged; + event EventHandler SortingSettingsChanged; } \ No newline at end of file diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabViewModel.cs index 24d9456e..4c05d488 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabViewModel.cs @@ -14,19 +14,19 @@ public interface ITabViewModel IFileSystemNodesSortingViewModel SortingViewModel { get; } - event EventHandler ActivationRequested; + event EventHandler ActivationRequested; - event EventHandler NewTabRequested; + event EventHandler NewTabRequested; - event EventHandler NewTabOnOppositePanelRequested; + event EventHandler NewTabOnOppositePanelRequested; - event EventHandler CloseRequested; + event EventHandler CloseRequested; - event EventHandler ClosingTabsToTheLeftRequested; + event EventHandler ClosingTabsToTheLeftRequested; - event EventHandler ClosingTabsToTheRightRequested; + event EventHandler ClosingTabsToTheRightRequested; - event EventHandler ClosingAllTabsButThisRequested; + event EventHandler ClosingAllTabsButThisRequested; event EventHandler MoveRequested; diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabsListViewModel.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabsListViewModel.cs index a1d046a8..e89be6ee 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabsListViewModel.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/ITabsListViewModel.cs @@ -10,7 +10,7 @@ public interface ITabsListViewModel IReadOnlyList Tabs { get; } - event EventHandler SelectedTabChanged; + event EventHandler SelectedTabChanged; ICommand SelectTabToTheLeftCommand { get; } diff --git a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/TabMoveRequestedEventArgs.cs b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/TabMoveRequestedEventArgs.cs index fa48404a..4b982dd8 100644 --- a/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/TabMoveRequestedEventArgs.cs +++ b/src/Camelot.ViewModels/Interfaces/MainWindow/FilePanels/Tabs/TabMoveRequestedEventArgs.cs @@ -1,8 +1,6 @@ -using System; - namespace Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Tabs; -public class TabMoveRequestedEventArgs : EventArgs +public class TabMoveRequestedEventArgs : System.EventArgs { public ITabViewModel Target { get; } diff --git a/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs b/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs index af14bd40..c2965298 100644 --- a/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs +++ b/src/Camelot/DependencyInjection/ViewModelsBootstrapper.cs @@ -302,6 +302,10 @@ private static void RegisterCommonViewModels(IMutableDependencyResolver services resolver.GetRequiredService(), resolver.GetRequiredService() )); + services.Register(() => new QuickSearchViewModel( + resolver.GetRequiredService(), + resolver.GetRequiredService() + )); services.RegisterLazySingleton(() => new DrivesListViewModel( resolver.GetRequiredService(), resolver.GetRequiredService(), @@ -375,12 +379,12 @@ private static IFilesPanelViewModel CreateFilesPanelViewModel( resolver.GetRequiredService(), resolver.GetRequiredService(), resolver.GetRequiredService(), + resolver.GetRequiredService(), tabsListViewModel, resolver.GetRequiredService(), directorySelectorViewModel, resolver.GetRequiredService(), - resolver.GetRequiredService(), - resolver.GetRequiredService() + resolver.GetRequiredService() ); return filesPanelViewModel; diff --git a/src/Camelot/Views/Main/FilesPanelView.xaml.cs b/src/Camelot/Views/Main/FilesPanelView.xaml.cs index e2fd19f8..b5b4c41a 100644 --- a/src/Camelot/Views/Main/FilesPanelView.xaml.cs +++ b/src/Camelot/Views/Main/FilesPanelView.xaml.cs @@ -12,6 +12,7 @@ using Camelot.DependencyInjection; using Camelot.Extensions; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels; +using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.EventArgs; using Camelot.ViewModels.Interfaces.MainWindow.FilePanels.Nodes; using Camelot.Views.Main.Controls; using DynamicData; @@ -176,15 +177,10 @@ private void OnDataGridKeyDown(object sender, KeyEventArgs args) if (args.Key == Key.Escape) { - ViewModel.ClearQuickSearchCommand.Execute(null); + ViewModel.QuickSearchViewModel.ClearQuickSearchCommand.Execute(null); } } - /// - /// Needed to get state of shift key - /// - /// - /// private void OnDataGridKeyUp(object sender, KeyEventArgs args) => UpdateShiftKeyStatus(args); private void OnDataGridTextInput(object sender, TextInputEventArgs args) @@ -193,7 +189,7 @@ private void OnDataGridTextInput(object sender, TextInputEventArgs args) { var parameter = new QuickSearchCommandModel(args.Text[0], _shiftDown); - ViewModel.QuickSearchCommand.Execute(parameter); + ViewModel.QuickSearchViewModel.QuickSearchCommand.Execute(parameter); } } diff --git a/tests/Camelot.ViewModels.Tests/FilePanels/SearchViewModelTests.cs b/tests/Camelot.ViewModels.Tests/FilePanels/SearchViewModelTests.cs index de10c475..1669131d 100644 --- a/tests/Camelot.ViewModels.Tests/FilePanels/SearchViewModelTests.cs +++ b/tests/Camelot.ViewModels.Tests/FilePanels/SearchViewModelTests.cs @@ -4,7 +4,7 @@ using Camelot.Services.Environment.Interfaces; using Camelot.ViewModels.Configuration; using Camelot.ViewModels.Implementations.MainWindow.FilePanels; -using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; using Camelot.ViewModels.Services.Interfaces; using Moq; using Moq.AutoMock; diff --git a/tests/Camelot.ViewModels.Tests/Specifications/NodeNameRegexSpecificationTests.cs b/tests/Camelot.ViewModels.Tests/Specifications/NodeNameRegexSpecificationTests.cs index 40e7ccdf..a03a5042 100644 --- a/tests/Camelot.ViewModels.Tests/Specifications/NodeNameRegexSpecificationTests.cs +++ b/tests/Camelot.ViewModels.Tests/Specifications/NodeNameRegexSpecificationTests.cs @@ -1,7 +1,7 @@ using System.Text.RegularExpressions; using Camelot.Services.Abstractions.Models; using Camelot.Services.Environment.Interfaces; -using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; using Moq; using Xunit; diff --git a/tests/Camelot.ViewModels.Tests/Specifications/NodeNameTextSpecificationTests.cs b/tests/Camelot.ViewModels.Tests/Specifications/NodeNameTextSpecificationTests.cs index a81d6548..db741fb0 100644 --- a/tests/Camelot.ViewModels.Tests/Specifications/NodeNameTextSpecificationTests.cs +++ b/tests/Camelot.ViewModels.Tests/Specifications/NodeNameTextSpecificationTests.cs @@ -1,5 +1,5 @@ using Camelot.Services.Abstractions.Models; -using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications; +using Camelot.ViewModels.Implementations.MainWindow.FilePanels.Specifications.Search; using Xunit; namespace Camelot.ViewModels.Tests.Specifications; From 14abb1d539bd2e7a7399565f7b8f7b5c650c3c82 Mon Sep 17 00:00:00 2001 From: IngvarX Date: Sun, 26 Nov 2023 23:45:26 -0800 Subject: [PATCH 5/5] Fixed broken tests build --- .../FilePanels/FilesPanelViewModelTests.cs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs b/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs index 16196881..6cbf9d92 100644 --- a/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs +++ b/tests/Camelot.ViewModels.Tests/FilePanels/FilesPanelViewModelTests.cs @@ -57,12 +57,13 @@ public FilesPanelViewModelTests() public void TestProperties() { var searchViewModel = _autoMocker.GetMock().Object; + var quickSearchViewModel = _autoMocker.GetMock().Object; var tabsListViewModel = _autoMocker.GetMock().Object; var operationsViewModel = _autoMocker.GetMock().Object; var directorySelectorViewModel = _autoMocker.GetMock().Object; var dragAndDropOperationsMediator = _autoMocker.GetMock().Object; var clipboardOperationsViewModel = _autoMocker.GetMock().Object; - + var viewModel = new FilesPanelViewModel( _autoMocker.GetMock().Object, _autoMocker.GetMock().Object, @@ -78,12 +79,12 @@ public void TestProperties() _autoMocker.GetMock().Object, _autoMocker.GetMock().Object, searchViewModel, + quickSearchViewModel, tabsListViewModel, operationsViewModel, directorySelectorViewModel, dragAndDropOperationsMediator, - clipboardOperationsViewModel, - _autoMocker.GetMock().Object + clipboardOperationsViewModel ); Assert.Equal(searchViewModel, viewModel.SearchViewModel);