feature<RevisionFiles>: enable to search file in revision files

This commit is contained in:
leo 2021-11-15 12:20:33 +08:00
parent b452456d9d
commit acc9840830
2 changed files with 159 additions and 94 deletions

View file

@ -21,39 +21,77 @@
<ColumnDefinition Width="*"/> <ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions> </Grid.ColumnDefinitions>
<Border Grid.Column="0" Background="{DynamicResource Brush.Contents}" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1"> <Grid Grid.Column="0">
<controls:Tree x:Name="treeFiles" SelectionChanged="OnFilesSelectionChanged" ContextMenuOpening="OnFilesContextMenuOpening"> <Grid.RowDefinitions>
<controls:Tree.ItemTemplate> <RowDefinition Height="Auto"/>
<HierarchicalDataTemplate ItemsSource="{Binding Children}"> <RowDefinition Height="*"/>
<StackPanel Orientation="Horizontal" Height="24"> </Grid.RowDefinitions>
<Path x:Name="Icon" Width="14" Height="14" Data="{StaticResource Icon.File}"/>
<TextBlock Margin="6,0,0,0" Text="{Binding Path, Converter={StaticResource PureFileName}}"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers> <Grid Grid.Row="0">
<DataTrigger Binding="{Binding IsFolder}" Value="True"> <Grid.ColumnDefinitions>
<Setter TargetName="Icon" Property="Fill" Value="Goldenrod"/> <ColumnDefinition Width="24"/>
<Setter TargetName="Icon" Property="Opacity" Value="1"/> <ColumnDefinition Width="*"/>
</DataTrigger> </Grid.ColumnDefinitions>
<MultiDataTrigger>
<MultiDataTrigger.Conditions> <Border
<Condition Binding="{Binding IsFolder}" Value="True"/> Grid.Column="0" Grid.ColumnSpan="2"
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="False"/> BorderBrush="{DynamicResource Brush.Border2}"
</MultiDataTrigger.Conditions> BorderThickness="1"/>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/> <Path
</MultiDataTrigger> Grid.Column="0"
<MultiDataTrigger> Width="14" Height="14"
<MultiDataTrigger.Conditions> Fill="{DynamicResource Brush.FG2}"
<Condition Binding="{Binding IsFolder}" Value="True"/> Data="{StaticResource Icon.Search}"
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="True"/> IsHitTestVisible="False"/>
</MultiDataTrigger.Conditions> <controls:TextEdit
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/> Grid.Column="1"
</MultiDataTrigger> Height="24"
</HierarchicalDataTemplate.Triggers> Margin="0"
</HierarchicalDataTemplate> Placeholder="{DynamicResource Text.CommitViewer.Changes.Search}"
</controls:Tree.ItemTemplate> BorderThickness="0"
</controls:Tree> TextChanged="OnSearchFilterChanged"/>
</Border> </Grid>
<Border Grid.Row="1" Margin="0,4,0,0" Background="{DynamicResource Brush.Contents}" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1">
<controls:Tree x:Name="treeFiles" SelectionChanged="OnFilesSelectionChanged">
<controls:Tree.ItemContainerStyle>
<Style TargetType="{x:Type controls:TreeItem}" BasedOn="{StaticResource Style.TreeItem}">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<EventSetter Event="ContextMenuOpening" Handler="OnFilesContextMenuOpening"/>
</Style>
</controls:Tree.ItemContainerStyle>
<controls:Tree.ItemTemplate>
<HierarchicalDataTemplate ItemsSource="{Binding Children}">
<StackPanel Orientation="Horizontal" Height="24">
<Path x:Name="Icon" Width="14" Height="14" Data="{StaticResource Icon.File}"/>
<TextBlock Margin="6,0,0,0" Text="{Binding Path, Converter={StaticResource PureFileName}}"/>
</StackPanel>
<HierarchicalDataTemplate.Triggers>
<DataTrigger Binding="{Binding IsFolder}" Value="True">
<Setter TargetName="Icon" Property="Fill" Value="Goldenrod"/>
<Setter TargetName="Icon" Property="Opacity" Value="1"/>
</DataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="False"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Fill}"/>
</MultiDataTrigger>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsFolder}" Value="True"/>
<Condition Binding="{Binding RelativeSource={RelativeSource AncestorType={x:Type controls:TreeItem}}, Path=IsExpanded}" Value="True"/>
</MultiDataTrigger.Conditions>
<Setter TargetName="Icon" Property="Data" Value="{StaticResource Icon.Folder.Open}"/>
</MultiDataTrigger>
</HierarchicalDataTemplate.Triggers>
</HierarchicalDataTemplate>
</controls:Tree.ItemTemplate>
</controls:Tree>
</Border>
</Grid>
<GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Background="Transparent"/> <GridSplitter Grid.Column="1" Width="1" HorizontalAlignment="Center" VerticalAlignment="Stretch" Background="Transparent"/>

View file

@ -18,6 +18,8 @@ namespace SourceGit.Views.Widgets {
private string repo = null; private string repo = null;
private string sha = null; private string sha = null;
private bool isLFSEnabled = false; private bool isLFSEnabled = false;
private List<Models.Object> cached = new List<Models.Object>();
private string filter = null;
/// <summary> /// <summary>
/// 文件列表树节点 /// 文件列表树节点
@ -26,6 +28,7 @@ namespace SourceGit.Views.Widgets {
public Models.ObjectType Type { get; set; } = Models.ObjectType.None; public Models.ObjectType Type { get; set; } = Models.ObjectType.None;
public string Path { get; set; } = ""; public string Path { get; set; } = "";
public string SHA { get; set; } = null; public string SHA { get; set; } = null;
public bool IsExpanded { get; set; } = false;
public bool IsFolder => Type == Models.ObjectType.None; public bool IsFolder => Type == Models.ObjectType.None;
public List<FileNode> Children { get; set; } = new List<FileNode>(); public List<FileNode> Children { get; set; } = new List<FileNode>();
} }
@ -44,72 +47,89 @@ namespace SourceGit.Views.Widgets {
var objects = cmd.Result(); var objects = cmd.Result();
if (cmd.Ctx.IsCancelRequested) return; if (cmd.Ctx.IsCancelRequested) return;
var nodes = new List<FileNode>(); cached = objects;
var folders = new Dictionary<string, FileNode>(); ShowVisibles();
foreach (var obj in objects) {
var sepIdx = obj.Path.IndexOf('/');
if (sepIdx == -1) {
nodes.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
} else {
FileNode lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = obj.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = obj.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
}
obj.Path = null;
}
folders.Clear();
objects.Clear();
SortFileNodes(nodes);
Dispatcher.Invoke(() => {
treeFiles.ItemsSource = nodes;
GC.Collect();
});
}); });
} }
public void Cleanup() { public void Cleanup() {
treeFiles.ItemsSource = new List<FileNode>(); treeFiles.ItemsSource = new List<FileNode>();
cached = new List<Models.Object>();
}
private void ShowVisibles() {
var nodes = new List<FileNode>();
var folders = new Dictionary<string, FileNode>();
var visibles = new List<Models.Object>();
if (string.IsNullOrEmpty(filter)) {
visibles.AddRange(cached);
} else {
foreach (var obj in cached) {
if (obj.Path.ToUpper().Contains(filter)) visibles.Add(obj);
}
}
var expanded = visibles.Count <= 50;
foreach (var obj in visibles) {
var sepIdx = obj.Path.IndexOf('/');
if (sepIdx == -1) {
nodes.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
} else {
FileNode lastFolder = null;
var start = 0;
while (sepIdx != -1) {
var folder = obj.Path.Substring(0, sepIdx);
if (folders.ContainsKey(folder)) {
lastFolder = folders[folder];
} else if (lastFolder == null) {
lastFolder = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
IsExpanded = expanded,
};
nodes.Add(lastFolder);
folders.Add(folder, lastFolder);
} else {
var cur = new FileNode() {
Type = Models.ObjectType.None,
Path = folder,
SHA = null,
IsExpanded = expanded,
};
folders.Add(folder, cur);
lastFolder.Children.Add(cur);
lastFolder = cur;
}
start = sepIdx + 1;
sepIdx = obj.Path.IndexOf('/', start);
}
lastFolder.Children.Add(new FileNode() {
Type = obj.Type,
Path = obj.Path,
SHA = obj.SHA,
});
}
}
folders.Clear();
visibles.Clear();
SortFileNodes(nodes);
Dispatcher.Invoke(() => {
treeFiles.ItemsSource = nodes;
GC.Collect();
});
} }
private void SortFileNodes(List<FileNode> nodes) { private void SortFileNodes(List<FileNode> nodes) {
@ -259,7 +279,7 @@ namespace SourceGit.Views.Widgets {
} }
private void OnFilesContextMenuOpening(object sender, ContextMenuEventArgs e) { private void OnFilesContextMenuOpening(object sender, ContextMenuEventArgs e) {
var item = treeFiles.FindItem(e.OriginalSource as DependencyObject); var item = sender as Controls.TreeItem;
if (item == null) return; if (item == null) return;
var node = item.DataContext as FileNode; var node = item.DataContext as FileNode;
@ -322,6 +342,13 @@ namespace SourceGit.Views.Widgets {
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) { private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e) {
e.Handled = true; e.Handled = true;
} }
private void OnSearchFilterChanged(object sender, TextChangedEventArgs e) {
var edit = sender as Controls.TextEdit;
filter = edit.Text.ToUpper();
Task.Run(() => ShowVisibles());
e.Handled = true;
}
#endregion #endregion
} }
} }