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);