Skip to content

Commit

Permalink
Added support for filtering to the new file bundle tree.
Browse files Browse the repository at this point in the history
  • Loading branch information
MeltyPlayer committed Sep 19, 2024
1 parent 5a08db6 commit 0bebd97
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 92 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
<PackageReference Include="MessageBox.Avalonia" Version="3.1.5.1" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.9.2" />
<PackageReference Include="NaturalSort.Extension" Version="4.3.0" />
<PackageReference Include="ObservableCollections" Version="3.0.4" />
<PackageReference Include="OxyPlot.Avalonia" Version="2.1.0-Avalonia11" />
<PackageReference Include="TextMateSharp" Version="1.0.58" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.58" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ public MainViewModel() {
var rootDirectory
= new RootFileBundleGatherer().GatherAllFiles(loadingProgress);
var totalNodeCount = this.GetTotalNodeCountWithinDirectory_(rootDirectory);
var totalNodeCount
= this.GetTotalNodeCountWithinDirectory_(rootDirectory);
var counterProgress = new CounterPercentageProgress(totalNodeCount);
counterProgress.OnProgressChanged += (_, progress)
=> fileTreeProgress.ReportProgress(progress);
Expand Down Expand Up @@ -129,9 +130,7 @@ var animationPlaybackManager
};

AudioPlaylistService.OnPlaylistUpdated
+= playlist => {
this.AudioPlayerPanel.AudioFileBundles = playlist;
};
+= playlist => { this.AudioPlayerPanel.AudioFileBundles = playlist; };
}

public ProgressPanelViewModel FileBundleTreeAsyncPanelViewModel {
Expand Down Expand Up @@ -160,22 +159,22 @@ private int GetTotalNodeCountWithinDirectory_(
directoryRoot.Subdirs.Sum(this.GetTotalNodeCountWithinDirectory_) +
directoryRoot.FileBundles.Count;

private FileBundleTreeViewModel<IAnnotatedFileBundle> GetFileTreeViewModel_(
private FileBundleTreeViewModel GetFileTreeViewModel_(
IFileBundleDirectory directoryRoot,
CounterPercentageProgress counterPercentageProgress) {
var viewModel = new FileBundleTreeViewModel<IAnnotatedFileBundle> {
Nodes = new ObservableCollection<INode<IAnnotatedFileBundle>>(
var viewModel = new FileBundleTreeViewModel(
new ObservableCollection<INode<IAnnotatedFileBundle>>(
directoryRoot
.Subdirs
.Select(subdir => this.CreateDirectoryNode_(
subdir,
counterPercentageProgress)))
};
);

viewModel.NodeSelected
+= (_, node) => {
if (node is FileBundleLeafNode leafNode) {
FileBundleService.OpenFileBundle(null, leafNode.Data.FileBundle);
FileBundleService.OpenFileBundle(null, leafNode.Value.FileBundle);
}
};

Expand Down Expand Up @@ -217,10 +216,16 @@ private INode<IAnnotatedFileBundle> CreateDirectoryNode_(
text,
new ObservableCollection<INode<IAnnotatedFileBundle>>(
directory
.Subdirs.Select(d => this.CreateDirectoryNode_(d, counterPercentageProgress))
.Subdirs
.Select(
d => this.CreateDirectoryNode_(
d,
counterPercentageProgress))
.Concat(
directory.FileBundles.Select(
f => this.CreateFileNode_(f, counterPercentageProgress)))));
f => this.CreateFileNode_(
f,
counterPercentageProgress)))));
}

private INode<IAnnotatedFileBundle> CreateFileNode_(
Expand All @@ -235,7 +240,7 @@ private INode<IAnnotatedFileBundle> CreateFileNode_(
text = Path.Join(parts.ToArray());
}

return new FileBundleLeafNode(text ?? fileBundle.FileBundle.DisplayName,
fileBundle);
var label = text ?? fileBundle.FileBundle.DisplayName;
return new FileBundleLeafNode(label, fileBundle);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.ObjectModel;
using System.Collections.Generic;
using System.Linq;

using fin.audio.io;
using fin.audio.io.importers.ogg;
Expand All @@ -14,62 +15,195 @@

using Material.Icons;

using ObservableCollections;

using uni.ui.avalonia.ViewModels;

namespace uni.ui.avalonia.common.treeViews {
namespace uni.ui.avalonia.common.treeViews;

using IFileBundleNode = INode<IAnnotatedFileBundle>;

// Top-level view model types
public class FileBundleTreeViewModel
: ViewModelBase, IFilterTreeViewViewModel<IAnnotatedFileBundle> {
private readonly IReadOnlyList<IFileBundleNode> nodes_;

// Top-level view model types
public class FileBundleTreeViewModel<T>
: ViewModelBase, IFilterTreeViewViewModel<T> {
public ObservableCollection<INode<T>> Nodes { get; init; }
private readonly ISynchronizedView<IFileBundleNode, IFileBundleNode>
filteredNodes_;

public event EventHandler<INode<T>>? NodeSelected;
public FileBundleTreeViewModel(IReadOnlyList<IFileBundleNode> nodes) {
this.nodes_ = nodes;

var obsList = new ObservableList<IFileBundleNode>(nodes);
this.filteredNodes_ = obsList.CreateView(t => t);
this.FilteredNodes = this.filteredNodes_.ToNotifyCollectionChanged();
}

public void ChangeSelection(INode node)
=> this.NodeSelected?.Invoke(this, Asserts.AsA<INode<T>>(node));
public INotifyCollectionChangedSynchronizedViewList<INode> FilteredNodes {
get;
}

public class FileBundleTreeViewModelForDesigner : FileBundleTreeViewModel<IAnnotatedFileBundle> {
public FileBundleTreeViewModelForDesigner() {
this.Nodes = [
new FileBundleDirectoryNode("Animals",
[
new FileBundleDirectoryNode("Mammals",
[
new FileBundleLeafNode("Lion", new CmbModelFileBundle("foo", new FinFile()).Annotate(null)),
new FileBundleLeafNode("Cat", new OggAudioFileBundle(new FinFile()).Annotate(null))
])
])
];
public event EventHandler<IFileBundleNode>? NodeSelected;

public void ChangeSelection(INode node)
=> this.NodeSelected?.Invoke(this, Asserts.AsA<IFileBundleNode>(node));

public void UpdateFilter(string? text) {
var filter = FileBundleFilter.FromText(text);

foreach (var node in this.nodes_) {
node.Filter = filter;
}

if (filter == null) {
this.filteredNodes_.ResetFilter();
return;
}

this.filteredNodes_.AttachFilter(n => n.InFilter);
}
}

public class FileBundleTreeViewModelForDesigner()
: FileBundleTreeViewModel([
new FileBundleDirectoryNode("Animals",
[
new FileBundleDirectoryNode("Mammals",
[
new FileBundleLeafNode("Lion",
new CmbModelFileBundle(
"foo",
new FinFile()).Annotate(null)),
new FileBundleLeafNode("Cat",
new OggAudioFileBundle(new FinFile())
.Annotate(null))
])
])
]);

// Node types
public class FileBundleDirectoryNode
: ViewModelBase, IFileBundleNode {
private readonly IReadOnlyList<IFileBundleNode>? subNodes_;

private readonly
ISynchronizedView<IFileBundleNode,
IFileBundleNode>? filteredSubNodes_;

public FileBundleDirectoryNode(
string label,
IReadOnlyList<IFileBundleNode>? subNodes) : this(label,
subNodes,
new HashSet<string>([label])) { }

// Node types
public class FileBundleDirectoryNode(
public FileBundleDirectoryNode(
string label,
ObservableCollection<INode<IAnnotatedFileBundle>>? subNodes = null)
: ViewModelBase, INode<IAnnotatedFileBundle> {
public ObservableCollection<INode<IAnnotatedFileBundle>>? SubNodes { get; }
= subNodes;
IReadOnlyList<IFileBundleNode>? subNodes,
IReadOnlySet<string> filterTerms) {
this.subNodes_ = subNodes;
this.Label = label;
this.FilterTerms = filterTerms;

var obsList = subNodes != null
? new ObservableList<IFileBundleNode>(subNodes)
: null;
this.filteredSubNodes_ = obsList?.CreateView(t => t);
this.FilteredSubNodes = this.filteredSubNodes_?.ToNotifyCollectionChanged();
}

public IAnnotatedFileBundle? Value => null;

public INotifyCollectionChangedSynchronizedViewList<
IFileBundleNode>? FilteredSubNodes { get; }

public MaterialIconKind? Icon => null;
public string Label { get; }
public IReadOnlySet<string> FilterTerms { get; }

public MaterialIconKind? Icon => null;
public IFilter<IAnnotatedFileBundle>? Filter {
set {
if (this.subNodes_ == null || this.FilteredSubNodes == null) {
return;
}

public string Label { get; } = label;
foreach (var node in this.subNodes_) {
node.Filter = value;
}

if (value == null) {
this.filteredSubNodes_?.ResetFilter();
this.InFilter = true;
return;
}

this.filteredSubNodes_?.AttachFilter(n => n.InFilter);
this.InFilter = this.subNodes_.Any(n => n.InFilter);
}
}

public class FileBundleLeafNode(string label, IAnnotatedFileBundle data)
: ViewModelBase, INode<IAnnotatedFileBundle> {
public ObservableCollection<INode<IAnnotatedFileBundle>>? SubNodes => null;
public bool InFilter { get; private set; } = true;
}

public class FileBundleLeafNode(string label, IAnnotatedFileBundle data)
: ViewModelBase, IFileBundleNode {
public INotifyCollectionChangedSynchronizedViewList<
IFileBundleNode>? FilteredSubNodes => null;

public MaterialIconKind? Icon => data.FileBundle switch {
public MaterialIconKind? Icon => data.FileBundle switch {
IAudioFileBundle => MaterialIconKind.VolumeHigh,
IImageFileBundle => MaterialIconKind.ImageOutline,
IModelFileBundle => MaterialIconKind.CubeOutline,
ISceneFileBundle => MaterialIconKind.Web,
};
};

public string Label { get; } = label;
public IAnnotatedFileBundle Value => data;
public string Label { get; } = label;

public IAnnotatedFileBundle Data => data;
public IFilter<IAnnotatedFileBundle>? Filter {
set => this.InFilter = value?.MatchesNode(this) ?? true;
}

public bool InFilter { get; private set; } = true;
}

public class FileBundleFilter(IReadOnlySet<string> tokens)
: IFilter<IAnnotatedFileBundle> {
public static FileBundleFilter? FromText(string? text) {
var tokens = text?.Split(new[] { ' ', '\t', '\n' },
StringSplitOptions.RemoveEmptyEntries |
StringSplitOptions.TrimEntries);
return tokens?.Length > 0
? new FileBundleFilter(new HashSet<string>(tokens))
: null;
}

public bool MatchesNode(IFileBundleNode node) {
foreach (var token in tokens) {
var fileBundle = node.Value.FileBundle;

if (this.ContainsToken_(node.Label, token)) {
goto FoundMatch;
}

if (fileBundle.GameName != null &&
this.ContainsToken_(fileBundle.GameName, token)) {
goto FoundMatch;
}

foreach (var file in fileBundle.Files) {
if (this.ContainsToken_(file.DisplayFullPath, token)) {
goto FoundMatch;
}
}

return false;

FoundMatch: ;
}

return true;
}

private bool ContainsToken_(string text, string token)
=> text.Contains(token, StringComparison.OrdinalIgnoreCase);
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,27 +45,28 @@
<!-- Renders tree view. -->
<DockPanel>
<textboxes:AutocompleteTextbox
x:Name="autocompleteTextbox_"
DockPanel.Dock="Top"
Icon="Search"
Placeholder="Search file bundles..." />

<controls:If Condition="{Binding Nodes, Converter={x:Static ObjectConverters.IsNotNull}}">
<TreeView ItemsSource="{Binding Nodes}"
<controls:If Condition="{Binding FilteredNodes, Converter={x:Static ObjectConverters.IsNotNull}}">
<TreeView ItemsSource="{Binding FilteredNodes}"
SelectionChanged="TreeView_OnSelectionChanged_"
Margin="-4 0">
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding SubNodes}">
<TreeDataTemplate ItemsSource="{Binding FilteredSubNodes}">
<StackPanel Orientation="Horizontal">
<controls:If Condition="{Binding Icon, Converter={x:Static ObjectConverters.IsNotNull}}">
<avalonia:MaterialIcon
Kind="{Binding Icon}"
Margin="-24 0 4 0"
Height="16"
Width="16" />
Kind="{Binding Icon}"
Margin="-24 0 4 0"
Height="16"
Width="16" />
</controls:If>
<TextBlock
Classes="regular"
Text="{Binding Label}" />
Classes="regular"
Text="{Binding Label}" />
</StackPanel>
</TreeDataTemplate>
</TreeView.ItemTemplate>
Expand Down
Loading

0 comments on commit 0bebd97

Please sign in to comment.