Skip to content

Commit dd95dd6

Browse files
authored
feat(panzerfaust): add search support allowing user to find projet by name (#399) (#447)
* fix(buildengine): update glm cmake minimum version to 3.5 * docs: update with some dependencies some might find useful to build the engine * docs: update setup windows machine section * feat(panzerfaust): add search support allowing user to find projet by name * docs: update with some dependencies some might find useful to build the engine * docs: update setup windows machine section * feat(panzerfaust): add search support allowing user to find projet by name * chore: replace naive pattern matching implementation with kmp algorithm * chore: remove throttle() and make kmp functions static
1 parent 64a58fb commit dd95dd6

File tree

2 files changed

+109
-7
lines changed

2 files changed

+109
-7
lines changed

Panzerfaust/ViewModels/MainWindowViewModel.cs

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using DynamicData;
22
using Microsoft.Extensions.DependencyInjection;
3+
using Microsoft.Extensions.Logging;
34
using Panzerfaust.Models;
45
using ReactiveUI;
56
using System;
@@ -18,27 +19,122 @@ namespace Panzerfaust.ViewModels
1819
{
1920
internal class MainWindowViewModel : ViewModelBase
2021
{
21-
public ObservableCollection<ProjectViewModel> Projects { get; set; } = new();
22+
23+
// Field which keep updated with the user inputs in the search bar
24+
private string searchText = string.Empty;
25+
public string SearchText
26+
{
27+
get => searchText;
28+
set => this.RaiseAndSetIfChanged(ref searchText, value);
29+
}
30+
31+
// Use of DynamicData's SourceList for batch updates later on,
32+
// reduces UI refreshes
33+
private readonly SourceList<ProjectViewModel> projects = new();
34+
public ReadOnlyObservableCollection<ProjectViewModel> FilteredProjects { get; set; }
35+
2236
public ReactiveCommand<Unit, Unit> CreateProjectCommand { get; }
2337
public Interaction<ProjectWindowViewModel, ProjectViewModel?> NewProjectDialog { get; } = new();
2438
public Interaction<MessageBoxWindowViewModel, bool> DeleteProjectInteraction { get; } = new();
2539

40+
2641
public MainWindowViewModel()
2742
{
43+
var filterPredicate = this.WhenAnyValue(x => x.SearchText)
44+
.Select(searchText => CreateFilterPredicate(searchText.Trim()))
45+
.DistinctUntilChanged();
46+
47+
// The connect() method links the project list to the filtered view while
48+
// bind() propagates the changes to FilteredProjects on the UI thread.
49+
// Subscribe() is required to activate the "pipeline".
50+
projects.Connect()
51+
.Filter(filterPredicate)
52+
.Bind(out var filteredProjects)
53+
.Subscribe();
54+
55+
FilteredProjects = filteredProjects;
56+
2857
RxApp.MainThreadScheduler.Schedule(LoadProjectsAsync);
2958

3059
CreateProjectCommand = ReactiveCommand.CreateFromTask(OnCreateProjectCommand);
3160

3261
MessageBus.Current.Listen<(string, ProjectViewModel)>().Subscribe(OnReceiveMessage);
3362
}
63+
private static int[] BuildKMPTable(string pattern)
64+
{
65+
int j = 0; // Position in the pattern
66+
67+
int m = pattern.Length;
68+
int[] lps = new int[m]; // longest prefix which is also a suffix
69+
70+
lps[0] = 0;
71+
for (int i = 1; i < m; i++)
72+
{
73+
while (j > 0 && char.ToLower(pattern[i]) != char.ToLower(pattern[j]))
74+
{
75+
j = lps[j - 1];
76+
}
77+
if (char.ToLower(pattern[i]) == char.ToLower(pattern[j]))
78+
{
79+
j++;
80+
}
81+
lps[i] = j;
82+
}
83+
return lps;
84+
}
85+
86+
private static bool KMPSearch(string searchText, string pattern)
87+
{
88+
89+
int j;
90+
int[] lps;
91+
92+
// Edge cases
93+
if (string.IsNullOrEmpty(pattern))
94+
return true;
95+
96+
if (string.IsNullOrEmpty(searchText))
97+
return false;
98+
99+
if (pattern.Length > searchText.Length)
100+
return false;
101+
102+
j = 0;
103+
lps = BuildKMPTable(pattern);
104+
105+
for (int i = 0; i < searchText.Length; i++)
106+
{
107+
while (j > 0 && char.ToLower(searchText[i]) != char.ToLower(pattern[j]))
108+
{
109+
j = lps[j - 1];
110+
}
111+
if (char.ToLower(searchText[i]) == char.ToLower(pattern[j]))
112+
{
113+
j++;
114+
}
115+
if (j == pattern.Length)
116+
{
117+
return true;
118+
}
119+
}
120+
121+
return false;
122+
}
123+
124+
private Func<ProjectViewModel, bool> CreateFilterPredicate(string searchTerm)
125+
{
126+
return string.IsNullOrWhiteSpace(searchTerm)
127+
? _ => true
128+
: p => KMPSearch(p.Name, searchTerm);
129+
}
34130

35131
private void OnReceiveMessage((string, ProjectViewModel) message)
36132
{
37133
var (action, data) = message;
38134

39135
if (action == Message.DeleteAction)
40136
{
41-
Projects.Remove(data);
137+
projects.Remove(data);
42138
}
43139
}
44140

@@ -49,7 +145,7 @@ private async Task OnCreateProjectCommand()
49145
if (result != null)
50146
{
51147
result.SetRemovalInteraction(DeleteProjectInteraction);
52-
Projects.Add(result);
148+
projects.Add(result);
53149
}
54150
}
55151

@@ -58,8 +154,14 @@ private async void LoadProjectsAsync()
58154
var projectService = App.Current?.ServiceProvider?.GetService<Service.IProjectService>();
59155
if (projectService == null) return;
60156

61-
var projects = await projectService.LoadProjectsAsync();
62-
Projects.AddRange(projects.Select(project => new ProjectViewModel(project, DeleteProjectInteraction)));
157+
var loadedProjects = await projectService.LoadProjectsAsync();
158+
159+
projects.Edit(innerList =>
160+
{
161+
innerList.AddRange(
162+
loadedProjects.Select(project => new ProjectViewModel(project, DeleteProjectInteraction))
163+
);
164+
});
63165
}
64166
}
65167
}

Panzerfaust/Views/MainWindow.axaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,15 @@
4646
<TextBlock Grid.Row="0" Text="Projects" Margin="30, 30, 0, 0" FontSize="25"/>
4747
<StackPanel Orientation="Vertical" Grid.Row="1" VerticalAlignment="Stretch" HorizontalAlignment="Stretch">
4848
<Button Content="New project" HorizontalAlignment="Right" Background="Green" Margin="0, 20, 5, 20" FontSize="15" CornerRadius="6" Command="{Binding CreateProjectCommand}"/>
49-
<TextBox Watermark="Search..." BorderBrush="Transparent" BorderThickness="0" Foreground="White" MinWidth="200" Width="210" HorizontalAlignment="Right" Margin="0, 5, 5, 5" CornerRadius="6"/>
49+
<TextBox Text="{Binding SearchText, UpdateSourceTrigger=PropertyChanged}" Watermark="Search..." BorderBrush="Transparent" BorderThickness="0" Foreground="White" MinWidth="200" Width="210" HorizontalAlignment="Right" Margin="0, 5, 5, 5" CornerRadius="6"/>
5050
<ProgressBar Foreground="WhiteSmoke" VerticalAlignment="Stretch" HorizontalAlignment="Stretch"/>
5151
<DataGrid VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
5252
IsReadOnly="True"
5353
CanUserReorderColumns="False"
5454
CanUserSortColumns="False"
5555
VerticalScrollBarVisibility="Auto"
5656
CanUserResizeColumns="False"
57-
BorderThickness="0" BorderBrush="Gray" ItemsSource="{Binding Projects}" SelectionMode="Single">
57+
BorderThickness="0" BorderBrush="Gray" ItemsSource="{Binding FilteredProjects}" SelectionMode="Single">
5858
<DataGrid.Columns>
5959
<DataGridTextColumn Header="NAME" Width="*" MinWidth="256" Binding="{Binding Name, Mode=OneWay}"/>
6060
<DataGridTextColumn Header="PATH" Width="*" MinWidth="256" Binding="{Binding Path, Mode=OneWay}"/>

0 commit comments

Comments
 (0)