2024-07-11 22:54:13 -07:00
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
|
|
|
|
using Avalonia;
|
|
|
|
using Avalonia.Collections;
|
|
|
|
using Avalonia.Controls;
|
|
|
|
using Avalonia.Controls.Primitives;
|
|
|
|
using Avalonia.Input;
|
|
|
|
using Avalonia.Layout;
|
|
|
|
using Avalonia.Media;
|
|
|
|
using Avalonia.VisualTree;
|
|
|
|
|
|
|
|
namespace SourceGit.Views
|
|
|
|
{
|
|
|
|
public class RevisionFileTreeNodeToggleButton : ToggleButton
|
|
|
|
{
|
|
|
|
protected override Type StyleKeyOverride => typeof(ToggleButton);
|
|
|
|
|
|
|
|
protected override void OnPointerPressed(PointerPressedEventArgs e)
|
|
|
|
{
|
|
|
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
|
2024-07-14 00:55:15 -07:00
|
|
|
DataContext is ViewModels.RevisionFileTreeNode { IsFolder: true } node)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
var tree = this.FindAncestorOfType<RevisionFileTreeView>();
|
2024-07-14 00:55:15 -07:00
|
|
|
tree?.ToggleNodeIsExpanded(node);
|
2024-07-11 22:54:13 -07:00
|
|
|
}
|
|
|
|
|
|
|
|
e.Handled = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public class RevisionTreeNodeIcon : UserControl
|
|
|
|
{
|
2024-07-14 00:55:15 -07:00
|
|
|
public static readonly StyledProperty<ViewModels.RevisionFileTreeNode> NodeProperty =
|
|
|
|
AvaloniaProperty.Register<RevisionTreeNodeIcon, ViewModels.RevisionFileTreeNode>(nameof(Node));
|
2024-07-11 22:54:13 -07:00
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
public ViewModels.RevisionFileTreeNode Node
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
get => GetValue(NodeProperty);
|
|
|
|
set => SetValue(NodeProperty, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
public static readonly StyledProperty<bool> IsExpandedProperty =
|
|
|
|
AvaloniaProperty.Register<RevisionTreeNodeIcon, bool>(nameof(IsExpanded));
|
|
|
|
|
|
|
|
public bool IsExpanded
|
|
|
|
{
|
|
|
|
get => GetValue(IsExpandedProperty);
|
|
|
|
set => SetValue(IsExpandedProperty, value);
|
|
|
|
}
|
|
|
|
|
|
|
|
static RevisionTreeNodeIcon()
|
|
|
|
{
|
|
|
|
NodeProperty.Changed.AddClassHandler<RevisionTreeNodeIcon>((icon, _) => icon.UpdateContent());
|
|
|
|
IsExpandedProperty.Changed.AddClassHandler<RevisionTreeNodeIcon>((icon, _) => icon.UpdateContent());
|
|
|
|
}
|
|
|
|
|
|
|
|
private void UpdateContent()
|
|
|
|
{
|
|
|
|
var node = Node;
|
2024-07-14 00:55:15 -07:00
|
|
|
if (node?.Backend == null)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
Content = null;
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var obj = node.Backend;
|
2024-07-14 00:55:15 -07:00
|
|
|
switch (obj.Type)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
2024-07-14 00:55:15 -07:00
|
|
|
case Models.ObjectType.Blob:
|
|
|
|
CreateContent("Icons.File");
|
|
|
|
break;
|
|
|
|
case Models.ObjectType.Commit:
|
|
|
|
CreateContent("Icons.Submodule");
|
|
|
|
break;
|
|
|
|
default:
|
|
|
|
CreateContent(node.IsExpanded ? "Icons.Folder.Open" : "Icons.Folder.Fill", Brushes.Goldenrod);
|
|
|
|
break;
|
2024-07-11 22:54:13 -07:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
private void CreateContent(string iconKey, IBrush fill = null)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
var geo = this.FindResource(iconKey) as StreamGeometry;
|
|
|
|
if (geo == null)
|
|
|
|
return;
|
|
|
|
|
|
|
|
var icon = new Avalonia.Controls.Shapes.Path()
|
|
|
|
{
|
2024-07-14 00:55:15 -07:00
|
|
|
Width = 14,
|
|
|
|
Height = 14,
|
2024-07-11 22:54:13 -07:00
|
|
|
HorizontalAlignment = HorizontalAlignment.Left,
|
|
|
|
VerticalAlignment = VerticalAlignment.Center,
|
|
|
|
Data = geo,
|
|
|
|
};
|
|
|
|
|
|
|
|
if (fill != null)
|
|
|
|
icon.Fill = fill;
|
|
|
|
|
|
|
|
Content = icon;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
public partial class RevisionFileTreeView : UserControl
|
|
|
|
{
|
|
|
|
public static readonly StyledProperty<string> RevisionProperty =
|
2024-07-14 00:55:15 -07:00
|
|
|
AvaloniaProperty.Register<RevisionFileTreeView, string>(nameof(Revision));
|
2024-07-11 22:54:13 -07:00
|
|
|
|
|
|
|
public string Revision
|
|
|
|
{
|
|
|
|
get => GetValue(RevisionProperty);
|
|
|
|
set => SetValue(RevisionProperty, value);
|
|
|
|
}
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
public AvaloniaList<ViewModels.RevisionFileTreeNode> Rows
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
get => _rows;
|
|
|
|
}
|
|
|
|
|
|
|
|
public RevisionFileTreeView()
|
|
|
|
{
|
|
|
|
InitializeComponent();
|
|
|
|
}
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
public void ToggleNodeIsExpanded(ViewModels.RevisionFileTreeNode node)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
_disableSelectionChangingEvent = true;
|
|
|
|
node.IsExpanded = !node.IsExpanded;
|
|
|
|
|
|
|
|
var depth = node.Depth;
|
|
|
|
var idx = _rows.IndexOf(node);
|
|
|
|
if (idx == -1)
|
|
|
|
return;
|
|
|
|
|
|
|
|
if (node.IsExpanded)
|
|
|
|
{
|
|
|
|
var subtree = GetChildrenOfTreeNode(node);
|
|
|
|
if (subtree != null && subtree.Count > 0)
|
|
|
|
{
|
2024-07-14 00:55:15 -07:00
|
|
|
var subrows = new List<ViewModels.RevisionFileTreeNode>();
|
2024-07-12 02:14:52 -07:00
|
|
|
MakeRows(subrows, subtree, depth + 1);
|
2024-07-11 22:54:13 -07:00
|
|
|
_rows.InsertRange(idx + 1, subrows);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
var removeCount = 0;
|
|
|
|
for (int i = idx + 1; i < _rows.Count; i++)
|
|
|
|
{
|
|
|
|
var row = _rows[i];
|
|
|
|
if (row.Depth <= depth)
|
|
|
|
break;
|
|
|
|
|
|
|
|
removeCount++;
|
|
|
|
}
|
|
|
|
_rows.RemoveRange(idx + 1, removeCount);
|
|
|
|
}
|
|
|
|
|
|
|
|
_disableSelectionChangingEvent = false;
|
|
|
|
}
|
|
|
|
|
|
|
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
|
|
|
{
|
|
|
|
base.OnPropertyChanged(change);
|
|
|
|
|
|
|
|
if (change.Property == RevisionProperty)
|
|
|
|
{
|
|
|
|
_tree.Clear();
|
|
|
|
_rows.Clear();
|
|
|
|
|
|
|
|
var vm = DataContext as ViewModels.CommitDetail;
|
|
|
|
if (vm == null || vm.Commit == null)
|
|
|
|
{
|
|
|
|
GC.Collect();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
var objects = vm.GetRevisionFilesUnderFolder(null);
|
|
|
|
if (objects == null || objects.Count == 0)
|
|
|
|
{
|
|
|
|
GC.Collect();
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
|
|
|
foreach (var obj in objects)
|
2024-07-14 00:55:15 -07:00
|
|
|
_tree.Add(new ViewModels.RevisionFileTreeNode { Backend = obj });
|
2024-07-11 22:54:13 -07:00
|
|
|
|
|
|
|
_tree.Sort((l, r) =>
|
|
|
|
{
|
|
|
|
if (l.IsFolder == r.IsFolder)
|
2024-07-14 00:55:15 -07:00
|
|
|
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
|
2024-07-11 22:54:13 -07:00
|
|
|
return l.IsFolder ? -1 : 1;
|
|
|
|
});
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
var topTree = new List<ViewModels.RevisionFileTreeNode>();
|
2024-07-11 22:54:13 -07:00
|
|
|
MakeRows(topTree, _tree, 0);
|
|
|
|
_rows.AddRange(topTree);
|
|
|
|
GC.Collect();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
private void OnTreeNodeContextRequested(object sender, ContextRequestedEventArgs e)
|
|
|
|
{
|
2024-07-14 00:55:15 -07:00
|
|
|
if (DataContext is ViewModels.CommitDetail vm &&
|
|
|
|
sender is Grid { DataContext: ViewModels.RevisionFileTreeNode { Backend: { } obj } } grid)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
if (obj.Type != Models.ObjectType.Tree)
|
|
|
|
{
|
|
|
|
var menu = vm.CreateRevisionFileContextMenu(obj);
|
|
|
|
grid.OpenContextMenu(menu);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
e.Handled = true;
|
|
|
|
}
|
|
|
|
|
|
|
|
private void OnTreeNodeDoubleTapped(object sender, TappedEventArgs e)
|
|
|
|
{
|
2024-07-14 00:55:15 -07:00
|
|
|
if (sender is Grid { DataContext: ViewModels.RevisionFileTreeNode { IsFolder: true } node })
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
var posX = e.GetPosition(this).X;
|
|
|
|
if (posX < node.Depth * 16 + 16)
|
|
|
|
return;
|
|
|
|
|
|
|
|
ToggleNodeIsExpanded(node);
|
2024-07-14 00:55:15 -07:00
|
|
|
}
|
2024-07-11 22:54:13 -07:00
|
|
|
}
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
private void OnRowsSelectionChanged(object sender, SelectionChangedEventArgs _)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
if (_disableSelectionChangingEvent)
|
|
|
|
return;
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
if (sender is ListBox { SelectedItem: ViewModels.RevisionFileTreeNode node } && DataContext is ViewModels.CommitDetail vm)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
2024-07-14 00:55:15 -07:00
|
|
|
if (!node.IsFolder)
|
2024-07-11 22:54:13 -07:00
|
|
|
vm.ViewRevisionFile(node.Backend);
|
|
|
|
else
|
|
|
|
vm.ViewRevisionFile(null);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
private List<ViewModels.RevisionFileTreeNode> GetChildrenOfTreeNode(ViewModels.RevisionFileTreeNode node)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
if (!node.IsFolder)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
if (node.Children.Count > 0)
|
|
|
|
return node.Children;
|
|
|
|
|
|
|
|
var vm = DataContext as ViewModels.CommitDetail;
|
|
|
|
if (vm == null)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
var objects = vm.GetRevisionFilesUnderFolder(node.Backend.Path + "/");
|
|
|
|
if (objects == null || objects.Count == 0)
|
|
|
|
return null;
|
|
|
|
|
|
|
|
foreach (var obj in objects)
|
2024-07-14 00:55:15 -07:00
|
|
|
node.Children.Add(new ViewModels.RevisionFileTreeNode() { Backend = obj });
|
2024-07-11 22:54:13 -07:00
|
|
|
|
|
|
|
node.Children.Sort((l, r) =>
|
|
|
|
{
|
|
|
|
if (l.IsFolder == r.IsFolder)
|
2024-07-14 00:55:15 -07:00
|
|
|
return string.Compare(l.Name, r.Name, StringComparison.Ordinal);
|
2024-07-11 22:54:13 -07:00
|
|
|
return l.IsFolder ? -1 : 1;
|
|
|
|
});
|
|
|
|
|
|
|
|
return node.Children;
|
|
|
|
}
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
private void MakeRows(List<ViewModels.RevisionFileTreeNode> rows, List<ViewModels.RevisionFileTreeNode> nodes, int depth)
|
2024-07-11 22:54:13 -07:00
|
|
|
{
|
|
|
|
foreach (var node in nodes)
|
|
|
|
{
|
|
|
|
node.Depth = depth;
|
|
|
|
rows.Add(node);
|
|
|
|
|
|
|
|
if (!node.IsExpanded || !node.IsFolder)
|
|
|
|
continue;
|
|
|
|
|
|
|
|
MakeRows(rows, node.Children, depth + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2024-07-14 00:55:15 -07:00
|
|
|
private List<ViewModels.RevisionFileTreeNode> _tree = new List<ViewModels.RevisionFileTreeNode>();
|
|
|
|
private AvaloniaList<ViewModels.RevisionFileTreeNode> _rows = new AvaloniaList<ViewModels.RevisionFileTreeNode>();
|
2024-07-11 22:54:13 -07:00
|
|
|
private bool _disableSelectionChangingEvent = false;
|
|
|
|
}
|
|
|
|
}
|