diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml
index 54addac8..78f3bbca 100644
--- a/src/Resources/Styles.axaml
+++ b/src/Resources/Styles.axaml
@@ -1070,15 +1070,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/RevisionFileTreeView.axaml.cs b/src/Views/RevisionFileTreeView.axaml.cs
new file mode 100644
index 00000000..6a026276
--- /dev/null
+++ b/src/Views/RevisionFileTreeView.axaml.cs
@@ -0,0 +1,322 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+using Avalonia;
+using Avalonia.Collections;
+using Avalonia.Controls;
+using Avalonia.Controls.Primitives;
+using Avalonia.Input;
+using Avalonia.Layout;
+using Avalonia.Media;
+using Avalonia.VisualTree;
+
+using CommunityToolkit.Mvvm.ComponentModel;
+
+namespace SourceGit.Views
+{
+ public class RevisionFileTreeNode : ObservableObject
+ {
+ public Models.Object Backend { get; set; } = null;
+ public int Depth { get; set; } = 0;
+ public List Children { get; set; } = new List();
+
+ public string Name
+ {
+ get => Backend == null ? string.Empty : Path.GetFileName(Backend.Path);
+ }
+
+ public bool IsFolder
+ {
+ get => Backend != null && Backend.Type == Models.ObjectType.Tree;
+ }
+
+ public bool IsExpanded
+ {
+ get => _isExpanded;
+ set => SetProperty(ref _isExpanded, value);
+ }
+
+ private bool _isExpanded = false;
+ }
+
+ public class RevisionFileTreeNodeToggleButton : ToggleButton
+ {
+ protected override Type StyleKeyOverride => typeof(ToggleButton);
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
+ DataContext is RevisionFileTreeNode { IsFolder: true } node)
+ {
+ var tree = this.FindAncestorOfType();
+ tree.ToggleNodeIsExpanded(node);
+ }
+
+ e.Handled = true;
+ }
+ }
+
+ public class RevisionTreeNodeIcon : UserControl
+ {
+ public static readonly StyledProperty NodeProperty =
+ AvaloniaProperty.Register(nameof(Node));
+
+ public RevisionFileTreeNode Node
+ {
+ get => GetValue(NodeProperty);
+ set => SetValue(NodeProperty, value);
+ }
+
+ public static readonly StyledProperty IsExpandedProperty =
+ AvaloniaProperty.Register(nameof(IsExpanded));
+
+ public bool IsExpanded
+ {
+ get => GetValue(IsExpandedProperty);
+ set => SetValue(IsExpandedProperty, value);
+ }
+
+ static RevisionTreeNodeIcon()
+ {
+ NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent());
+ IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent());
+ }
+
+ private void UpdateContent()
+ {
+ var node = Node;
+ if (node == null || node.Backend == null)
+ {
+ Content = null;
+ return;
+ }
+
+ var obj = node.Backend;
+ if (obj.Type == Models.ObjectType.Blob)
+ {
+ CreateContent(14, new Thickness(0, 0, 0, 0), "Icons.File");
+ }
+ else if (obj.Type == Models.ObjectType.Commit)
+ {
+ CreateContent(14, new Thickness(0, 0, 0, 0), "Icons.Submodule");
+ }
+ else
+ {
+ if (node.IsExpanded)
+ CreateContent(14, new Thickness(0, 2, 0, 0), "Icons.Folder.Open", Brushes.Goldenrod);
+ else
+ CreateContent(14, new Thickness(0, 2, 0, 0), "Icons.Folder.Fill", Brushes.Goldenrod);
+ }
+ }
+
+ private void CreateContent(double size, Thickness margin, string iconKey, IBrush fill = null)
+ {
+ var geo = this.FindResource(iconKey) as StreamGeometry;
+ if (geo == null)
+ return;
+
+ var icon = new Avalonia.Controls.Shapes.Path()
+ {
+ Width = size,
+ Height = size,
+ HorizontalAlignment = HorizontalAlignment.Left,
+ VerticalAlignment = VerticalAlignment.Center,
+ Margin = margin,
+ Data = geo,
+ };
+
+ if (fill != null)
+ icon.Fill = fill;
+
+ Content = icon;
+ }
+ }
+
+ public partial class RevisionFileTreeView : UserControl
+ {
+ public static readonly StyledProperty RevisionProperty =
+ AvaloniaProperty.Register(nameof(Revision), null);
+
+ public string Revision
+ {
+ get => GetValue(RevisionProperty);
+ set => SetValue(RevisionProperty, value);
+ }
+
+ public AvaloniaList Rows
+ {
+ get => _rows;
+ }
+
+ public RevisionFileTreeView()
+ {
+ InitializeComponent();
+ }
+
+ public void ToggleNodeIsExpanded(RevisionFileTreeNode node)
+ {
+ _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)
+ {
+ var subrows = new List();
+ MakeRows(subrows, node.Children, depth + 1);
+ _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)
+ _tree.Add(new RevisionFileTreeNode { Backend = obj });
+
+ _tree.Sort((l, r) =>
+ {
+ if (l.IsFolder == r.IsFolder)
+ return l.Name.CompareTo(r.Name);
+ return l.IsFolder ? -1 : 1;
+ });
+
+ var topTree = new List();
+ MakeRows(topTree, _tree, 0);
+ _rows.AddRange(topTree);
+ GC.Collect();
+ }
+ }
+
+ private void OnTreeNodeContextRequested(object sender, ContextRequestedEventArgs e)
+ {
+ if (DataContext is ViewModels.CommitDetail vm && sender is Grid { DataContext: RevisionFileTreeNode { Backend: Models.Object obj } } grid)
+ {
+ if (obj.Type != Models.ObjectType.Tree)
+ {
+ var menu = vm.CreateRevisionFileContextMenu(obj);
+ grid.OpenContextMenu(menu);
+ }
+ }
+
+ e.Handled = true;
+ }
+
+ private void OnTreeNodeDoubleTapped(object sender, TappedEventArgs e)
+ {
+ if (sender is Grid { DataContext: RevisionFileTreeNode { IsFolder: true } node })
+ {
+ var posX = e.GetPosition(this).X;
+ if (posX < node.Depth * 16 + 16)
+ return;
+
+ ToggleNodeIsExpanded(node);
+ }
+ }
+
+ private void OnRowsSelectionChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_disableSelectionChangingEvent)
+ return;
+
+ if (sender is ListBox list && DataContext is ViewModels.CommitDetail vm)
+ {
+ var node = list.SelectedItem as RevisionFileTreeNode;
+ if (node != null && !node.IsFolder)
+ vm.ViewRevisionFile(node.Backend);
+ else
+ vm.ViewRevisionFile(null);
+ }
+ }
+
+ private List GetChildrenOfTreeNode(RevisionFileTreeNode node)
+ {
+ 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)
+ node.Children.Add(new RevisionFileTreeNode() { Backend = obj });
+
+ node.Children.Sort((l, r) =>
+ {
+ if (l.IsFolder == r.IsFolder)
+ return l.Name.CompareTo(r.Name);
+ return l.IsFolder ? -1 : 1;
+ });
+
+ return node.Children;
+ }
+
+ private void MakeRows(List rows, List nodes, int depth)
+ {
+ foreach (var node in nodes)
+ {
+ node.Depth = depth;
+ rows.Add(node);
+
+ if (!node.IsExpanded || !node.IsFolder)
+ continue;
+
+ MakeRows(rows, node.Children, depth + 1);
+ }
+ }
+
+ private List _tree = new List();
+ private AvaloniaList _rows = new AvaloniaList();
+ private bool _disableSelectionChangingEvent = false;
+ }
+}
diff --git a/src/Views/RevisionFiles.axaml b/src/Views/RevisionFiles.axaml
index 82a06b9f..0004ddb3 100644
--- a/src/Views/RevisionFiles.axaml
+++ b/src/Views/RevisionFiles.axaml
@@ -17,17 +17,7 @@
-
-
-
-
-
-
-
-
-
-
-
+
Children { get; set; } = new List();
-
- public bool IsFolder => Backend != null && Backend.Type == Models.ObjectType.Tree;
- public string Name => Backend != null ? Path.GetFileName(Backend.Path) : string.Empty;
- }
-
- public class RevisionFileTreeView : UserControl
- {
- public static readonly StyledProperty RevisionProperty =
- AvaloniaProperty.Register(nameof(Revision), null);
-
- public string Revision
- {
- get => GetValue(RevisionProperty);
- set => SetValue(RevisionProperty, value);
- }
-
- public Models.Object SelectedObject
- {
- get;
- private set;
- } = null;
-
- protected override Type StyleKeyOverride => typeof(UserControl);
-
- protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
- {
- base.OnPropertyChanged(change);
-
- if (change.Property == RevisionProperty)
- {
- SelectedObject = null;
-
- if (Content is TreeDataGrid tree && tree.Source is IDisposable disposable)
- disposable.Dispose();
-
- var vm = DataContext as ViewModels.CommitDetail;
- if (vm == null || vm.Commit == null)
- {
- Content = null;
- GC.Collect();
- return;
- }
-
- var objects = vm.GetRevisionFilesUnderFolder(null);
- if (objects == null || objects.Count == 0)
- {
- Content = null;
- GC.Collect();
- return;
- }
-
- var toplevelObjects = new List();
- foreach (var obj in objects)
- toplevelObjects.Add(new RevisionFileTreeNode() { Backend = obj });
-
- toplevelObjects.Sort((l, r) =>
- {
- if (l.IsFolder == r.IsFolder)
- return l.Name.CompareTo(r.Name);
- return l.IsFolder ? -1 : 1;
- });
-
- var template = this.FindResource("RevisionFileTreeNodeTemplate") as IDataTemplate;
- var source = new HierarchicalTreeDataGridSource(toplevelObjects)
- {
- Columns =
- {
- new HierarchicalExpanderColumn(
- new TemplateColumn(null, template, null, GridLength.Auto),
- GetChildrenOfTreeNode,
- x => x.IsFolder,
- x => x.IsExpanded)
- }
- };
-
- var selection = new Models.TreeDataGridSelectionModel(source, GetChildrenOfTreeNode);
- selection.SingleSelect = true;
- selection.SelectionChanged += (s, _) =>
- {
- if (s is Models.TreeDataGridSelectionModel model)
- {
- var node = model.SelectedItem;
- var detail = DataContext as ViewModels.CommitDetail;
-
- if (node != null && !node.IsFolder)
- {
- SelectedObject = node.Backend;
- detail.ViewRevisionFile(node.Backend);
- }
- else
- {
- SelectedObject = null;
- detail.ViewRevisionFile(null);
- }
- }
- };
-
- source.Selection = selection;
- Content = new TreeDataGrid()
- {
- AutoDragDropRows = false,
- ShowColumnHeaders = false,
- CanUserResizeColumns = false,
- CanUserSortColumns = false,
- Source = source,
- };
-
- GC.Collect();
- }
- }
-
- private List GetChildrenOfTreeNode(RevisionFileTreeNode node)
- {
- 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)
- node.Children.Add(new RevisionFileTreeNode() { Backend = obj });
-
- node.Children.Sort((l, r) =>
- {
- if (l.IsFolder == r.IsFolder)
- return l.Name.CompareTo(r.Name);
- return l.IsFolder ? -1 : 1;
- });
-
- return node.Children;
- }
- }
-
public class RevisionTextFileView : TextEditor
{
protected override Type StyleKeyOverride => typeof(TextEditor);
@@ -241,19 +91,5 @@ namespace SourceGit.Views
{
InitializeComponent();
}
-
- private void OnRevisionFileTreeViewContextRequested(object sender, ContextRequestedEventArgs e)
- {
- if (DataContext is ViewModels.CommitDetail vm && sender is RevisionFileTreeView view)
- {
- if (view.SelectedObject != null && view.SelectedObject.Type != Models.ObjectType.Tree)
- {
- var menu = vm.CreateRevisionFileContextMenu(view.SelectedObject);
- view.OpenContextMenu(menu);
- }
- }
-
- e.Handled = true;
- }
}
}