refactor: show submodule as tree instead of list (#1307)

This commit is contained in:
leo 2025-05-14 17:55:28 +08:00
parent 5ec51eefb9
commit 463d161ac7
No known key found for this signature in database
6 changed files with 534 additions and 136 deletions

View file

@ -210,7 +210,7 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _submodules, value); private set => SetProperty(ref _submodules, value);
} }
public List<Models.Submodule> VisibleSubmodules public SubmoduleCollection VisibleSubmodules
{ {
get => _visibleSubmodules; get => _visibleSubmodules;
private set => SetProperty(ref _visibleSubmodules, value); private set => SetProperty(ref _visibleSubmodules, value);
@ -2512,7 +2512,7 @@ namespace SourceGit.ViewModels
return visible; return visible;
} }
private List<Models.Submodule> BuildVisibleSubmodules() private SubmoduleCollection BuildVisibleSubmodules()
{ {
var visible = new List<Models.Submodule>(); var visible = new List<Models.Submodule>();
if (string.IsNullOrEmpty(_filter)) if (string.IsNullOrEmpty(_filter))
@ -2527,7 +2527,8 @@ namespace SourceGit.ViewModels
visible.Add(s); visible.Add(s);
} }
} }
return visible;
return SubmoduleCollection.Build(visible, _visibleSubmodules);
} }
private void RefreshHistoriesFilters(bool refresh) private void RefreshHistoriesFilters(bool refresh)
@ -2759,7 +2760,7 @@ namespace SourceGit.ViewModels
private List<Models.Tag> _tags = new List<Models.Tag>(); private List<Models.Tag> _tags = new List<Models.Tag>();
private List<Models.Tag> _visibleTags = new List<Models.Tag>(); private List<Models.Tag> _visibleTags = new List<Models.Tag>();
private List<Models.Submodule> _submodules = new List<Models.Submodule>(); private List<Models.Submodule> _submodules = new List<Models.Submodule>();
private List<Models.Submodule> _visibleSubmodules = new List<Models.Submodule>(); private SubmoduleCollection _visibleSubmodules = new SubmoduleCollection();
private bool _isAutoFetching = false; private bool _isAutoFetching = false;
private Timer _autoFetchTimer = null; private Timer _autoFetchTimer = null;

View file

@ -0,0 +1,206 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels
{
public class SubmoduleTreeNode : ObservableObject
{
public string FullPath { get; set; } = string.Empty;
public int Depth { get; private set; } = 0;
public Models.Submodule Module { get; private set; } = null;
public List<SubmoduleTreeNode> Children { get; private set; } = [];
public int Counter = 0;
public bool IsFolder
{
get => Module == null;
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public string ChildCounter
{
get => Counter > 0 ? $"({Counter})" : string.Empty;
}
public bool IsDirty
{
get => Module?.IsDirty ?? false;
}
public SubmoduleTreeNode(Models.Submodule module, int depth)
{
FullPath = module.Path;
Depth = depth;
Module = module;
IsExpanded = false;
}
public SubmoduleTreeNode(string path, int depth, bool isExpanded)
{
FullPath = path;
Depth = depth;
IsExpanded = isExpanded;
Counter = 1;
}
public static List<SubmoduleTreeNode> Build(IList<Models.Submodule> submodules, HashSet<string> expaneded)
{
var nodes = new List<SubmoduleTreeNode>();
var folders = new Dictionary<string, SubmoduleTreeNode>();
foreach (var module in submodules)
{
var sepIdx = module.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1)
{
nodes.Add(new SubmoduleTreeNode(module, 0));
}
else
{
SubmoduleTreeNode lastFolder = null;
int depth = 0;
while (sepIdx != -1)
{
var folder = module.Path.Substring(0, sepIdx);
if (folders.TryGetValue(folder, out var value))
{
lastFolder = value;
lastFolder.Counter++;
}
else if (lastFolder == null)
{
lastFolder = new SubmoduleTreeNode(folder, depth, expaneded.Contains(folder));
folders.Add(folder, lastFolder);
InsertFolder(nodes, lastFolder);
}
else
{
var cur = new SubmoduleTreeNode(folder, depth, expaneded.Contains(folder));
folders.Add(folder, cur);
InsertFolder(lastFolder.Children, cur);
lastFolder = cur;
}
depth++;
sepIdx = module.Path.IndexOf('/', sepIdx + 1);
}
lastFolder?.Children.Add(new SubmoduleTreeNode(module, depth));
}
}
folders.Clear();
return nodes;
}
private static void InsertFolder(List<SubmoduleTreeNode> collection, SubmoduleTreeNode subFolder)
{
for (int i = 0; i < collection.Count; i++)
{
if (!collection[i].IsFolder)
{
collection.Insert(i, subFolder);
return;
}
}
collection.Add(subFolder);
}
private bool _isExpanded = false;
}
public class SubmoduleCollection
{
public List<SubmoduleTreeNode> Tree
{
get;
set;
} = [];
public AvaloniaList<SubmoduleTreeNode> Rows
{
get;
set;
} = [];
public static SubmoduleCollection Build(List<Models.Submodule> submodules, SubmoduleCollection old)
{
var oldExpanded = new HashSet<string>();
foreach (var row in old.Rows)
{
if (row.IsFolder && row.IsExpanded)
oldExpanded.Add(row.FullPath);
}
var collection = new SubmoduleCollection();
collection.Tree = SubmoduleTreeNode.Build(submodules, oldExpanded);
var rows = new List<SubmoduleTreeNode>();
collection.MakeTreeRows(rows, collection.Tree);
collection.Rows.AddRange(rows);
return collection;
}
public void Clear()
{
Tree.Clear();
Rows.Clear();
}
public void ToggleExpand(SubmoduleTreeNode node)
{
node.IsExpanded = !node.IsExpanded;
var rows = Rows;
var depth = node.Depth;
var idx = rows.IndexOf(node);
if (idx == -1)
return;
if (node.IsExpanded)
{
var subrows = new List<SubmoduleTreeNode>();
MakeTreeRows(subrows, node.Children);
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);
}
}
private void MakeTreeRows(List<SubmoduleTreeNode> rows, List<SubmoduleTreeNode> nodes)
{
foreach (var node in nodes)
{
rows.Add(node);
if (!node.IsExpanded || !node.IsFolder)
continue;
MakeTreeRows(rows, node.Children);
}
}
}
}

View file

@ -330,100 +330,14 @@
</Button> </Button>
</Grid> </Grid>
</ToggleButton> </ToggleButton>
<ListBox Grid.Row="7" <v:SubmodulesView Grid.Row="7"
x:Name="SubmoduleList" x:Name="SubmoduleList"
Height="0" Height="0"
Margin="12,0,4,0" Margin="8,0,4,0"
Classes="repo_left_content_list" Submodules="{Binding VisibleSubmodules}"
ItemsSource="{Binding VisibleSubmodules}" RowsChanged="OnSubmodulesRowsChanged"
SelectionMode="Single" Focusable="False"
ContextRequested="OnSubmoduleContextRequested" IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}"/>
DoubleTapped="OnDoubleTappedSubmodule"
PropertyChanged="OnLeftSidebarListBoxPropertyChanged"
IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Submodule">
<Grid ColumnDefinitions="Auto,*,8,8" Background="Transparent">
<ToolTip.Tip>
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icons.Submodule}"/>
<TextBlock FontWeight="Bold" Margin="4,0,0,0" Text="{Binding Path}"/>
</StackPanel>
<Grid RowDefinitions="24,24" ColumnDefinitions="Auto,Auto" Margin="0,8,0,0">
<TextBlock Grid.Row="0" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.CommitDetail.Info.SHA}"/>
<StackPanel Grid.Row="0" Grid.Column="1"
Orientation="Horizontal"
Margin="8,0,0,0">
<TextBlock Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
VerticalAlignment="Center"/>
<Path Margin="6,0,0,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Width="12" Height="12"
Data="{StaticResource Icons.Check}"
Fill="Green"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}"/>
<Border Height="16"
Margin="6,0,0,0" Padding="4,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Background="DarkOrange"
CornerRadius="4"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}">
<Grid>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.NotInited}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.NotInited}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Modified}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Modified}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.RevisionChanged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.RevisionChanged}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Unmerged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Unmerged}}"/>
</Grid>
</Border>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.URL}"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Margin="8,0,0,0"
Text="{Binding URL}"
Foreground="{DynamicResource Brush.Link}"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</ToolTip.Tip>
<Path Grid.Column="0" Width="10" Height="10" Margin="8,0" Data="{StaticResource Icons.Submodule}"/>
<TextBlock Grid.Column="1" Text="{Binding Path}" ClipToBounds="True" Classes="primary" TextTrimming="CharacterEllipsis"/>
<Path Grid.Column="2"
Width="8" Height="8"
Fill="Goldenrod"
Data="{StaticResource Icons.Modified}"
IsVisible="{Binding IsDirty}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
<!-- Worktrees --> <!-- Worktrees -->
<ToggleButton Grid.Row="8" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}"> <ToggleButton Grid.Row="8" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}">
@ -461,7 +375,7 @@
SelectionMode="Single" SelectionMode="Single"
ContextRequested="OnWorktreeContextRequested" ContextRequested="OnWorktreeContextRequested"
DoubleTapped="OnDoubleTappedWorktree" DoubleTapped="OnDoubleTappedWorktree"
PropertyChanged="OnLeftSidebarListBoxPropertyChanged" PropertyChanged="OnWorktreeListPropertyChanged"
IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}"> IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}">
<ListBox.Styles> <ListBox.Styles>
<Style Selector="ListBoxItem"> <Style Selector="ListBoxItem">

View file

@ -179,26 +179,9 @@ namespace SourceGit.Views
RemoteBranchTree.UnselectAll(); RemoteBranchTree.UnselectAll();
} }
private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) private void OnSubmodulesRowsChanged(object _, RoutedEventArgs e)
{ {
if (sender is ListBox { SelectedItem: Models.Submodule submodule } grid && DataContext is ViewModels.Repository repo) UpdateLeftSidebarLayout();
{
var menu = repo.CreateContextMenuForSubmodule(submodule);
menu?.Open(grid);
}
e.Handled = true;
}
private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e)
{
if (sender is ListBox { SelectedItem: Models.Submodule submodule } &&
submodule.Status != Models.SubmoduleStatus.NotInited &&
DataContext is ViewModels.Repository repo)
{
repo.OpenSubmodule(submodule.Path);
}
e.Handled = true; e.Handled = true;
} }
@ -223,7 +206,7 @@ namespace SourceGit.Views
e.Handled = true; e.Handled = true;
} }
private void OnLeftSidebarListBoxPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e) private void OnWorktreeListPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e)
{ {
if (e.Property == ListBox.ItemsSourceProperty || e.Property == ListBox.IsVisibleProperty) if (e.Property == ListBox.ItemsSourceProperty || e.Property == ListBox.IsVisibleProperty)
UpdateLeftSidebarLayout(); UpdateLeftSidebarLayout();
@ -252,26 +235,26 @@ namespace SourceGit.Views
var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0; var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0;
var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0;
var desiredTag = vm.IsTagGroupExpanded ? 24.0 * TagsList.Rows : 0; var desiredTag = vm.IsTagGroupExpanded ? 24.0 * TagsList.Rows : 0;
var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? 24.0 * vm.VisibleSubmodules.Count : 0; var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? 24.0 * SubmoduleList.Rows : 0;
var desiredWorktree = vm.IsWorktreeGroupExpanded ? 24.0 * vm.Worktrees.Count : 0; var desiredWorktree = vm.IsWorktreeGroupExpanded ? 24.0 * vm.Worktrees.Count : 0;
var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree; var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree;
var hasOverflow = (desiredBranches + desiredOthers > leftHeight); var hasOverflow = (desiredBranches + desiredOthers > leftHeight);
if (vm.IsTagGroupExpanded) if (vm.IsWorktreeGroupExpanded)
{ {
var height = desiredTag; var height = desiredWorktree;
if (hasOverflow) if (hasOverflow)
{ {
var test = leftHeight - desiredBranches - desiredSubmodule - desiredWorktree; var test = leftHeight - desiredBranches - desiredTag - desiredSubmodule;
if (test < 0) if (test < 0)
height = Math.Min(200, height); height = Math.Min(120, height);
else else
height = Math.Max(200, test); height = Math.Max(120, test);
} }
leftHeight -= height; leftHeight -= height;
TagsList.Height = height; WorktreeList.Height = height;
hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight; hasOverflow = (desiredBranches + desiredTag + desiredSubmodule) > leftHeight;
} }
if (vm.IsSubmoduleGroupExpanded) if (vm.IsSubmoduleGroupExpanded)
@ -279,32 +262,32 @@ namespace SourceGit.Views
var height = desiredSubmodule; var height = desiredSubmodule;
if (hasOverflow) if (hasOverflow)
{ {
var test = leftHeight - desiredBranches - desiredWorktree; var test = leftHeight - desiredBranches - desiredTag;
if (test < 0) if (test < 0)
height = Math.Min(200, height); height = Math.Min(120, height);
else else
height = Math.Max(200, test); height = Math.Max(120, test);
} }
leftHeight -= height; leftHeight -= height;
SubmoduleList.Height = height; SubmoduleList.Height = height;
hasOverflow = (desiredBranches + desiredWorktree) > leftHeight; hasOverflow = (desiredBranches + desiredTag) > leftHeight;
} }
if (vm.IsWorktreeGroupExpanded) if (vm.IsTagGroupExpanded)
{ {
var height = desiredWorktree; var height = desiredTag;
if (hasOverflow) if (hasOverflow)
{ {
var test = leftHeight - desiredBranches; var test = leftHeight - desiredBranches;
if (test < 0) if (test < 0)
height = Math.Min(200, height); height = Math.Min(120, height);
else else
height = Math.Max(200, test); height = Math.Max(120, test);
} }
leftHeight -= height; leftHeight -= height;
WorktreeList.Height = height; TagsList.Height = height;
} }
if (leftHeight > 0 && desiredBranches > leftHeight) if (leftHeight > 0 && desiredBranches > leftHeight)

View file

@ -0,0 +1,120 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:v="using:SourceGit.Views"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.SubmodulesView"
x:Name="ThisControl">
<UserControl.DataTemplates>
<DataTemplate DataType="m:Submodule">
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Path Width="10" Height="10" Data="{StaticResource Icons.Submodule}"/>
<TextBlock FontWeight="Bold" Margin="4,0,0,0" Text="{Binding Path}"/>
</StackPanel>
<Grid RowDefinitions="24,24" ColumnDefinitions="Auto,Auto" Margin="0,8,0,0">
<TextBlock Grid.Row="0" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.CommitDetail.Info.SHA}"/>
<StackPanel Grid.Row="0" Grid.Column="1"
Orientation="Horizontal"
Margin="8,0,0,0">
<TextBlock Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
VerticalAlignment="Center"/>
<Path Margin="6,0,0,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Width="12" Height="12"
Data="{StaticResource Icons.Check}"
Fill="Green"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}"/>
<Border Height="16"
Margin="6,0,0,0" Padding="4,0"
HorizontalAlignment="Left" VerticalAlignment="Center"
Background="DarkOrange"
CornerRadius="4"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}">
<Grid>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.NotInited}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.NotInited}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Modified}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Modified}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.RevisionChanged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.RevisionChanged}}"/>
<TextBlock VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.Status.Unmerged}"
Foreground="White"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Unmerged}}"/>
</Grid>
</Border>
</StackPanel>
<TextBlock Grid.Row="1" Grid.Column="0"
Classes="info_label"
HorizontalAlignment="Left" VerticalAlignment="Center"
Text="{DynamicResource Text.Submodule.URL}"/>
<TextBlock Grid.Row="1" Grid.Column="1"
Margin="8,0,0,0"
Text="{Binding URL}"
Foreground="{DynamicResource Brush.Link}"
VerticalAlignment="Center"/>
</Grid>
</StackPanel>
</DataTemplate>
</UserControl.DataTemplates>
<ListBox Classes="repo_left_content_list" ItemsSource="{Binding #ThisControl.Submodules.Rows}" SelectionMode="Single">
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="CornerRadius" Value="4"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="vm:SubmoduleTreeNode">
<Border Height="24" Background="Transparent" DoubleTapped="OnDoubleTappedNode" ContextRequested="OnRowContextRequested" ToolTip.Tip="{Binding Module}">
<Grid ColumnDefinitions="16,Auto,*,Auto,Auto"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
VerticalAlignment="Center">
<v:SubmoduleTreeNodeToggleButton Grid.Column="0"
Classes="tree_expander"
Focusable="False"
HorizontalAlignment="Center"
IsChecked="{Binding IsExpanded, Mode=OneWay}"
IsVisible="{Binding IsFolder}"/>
<v:SubmoduleTreeNodeIcon Grid.Column="1"
IsExpanded="{Binding IsExpanded, Mode=OneWay}"/>
<TextBlock Grid.Column="2"
Classes="primary"
Margin="8,0,0,0"
TextTrimming="CharacterEllipsis">
<Run Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}"/>
<Run Text="{Binding ChildCounter}" Foreground="{DynamicResource Brush.FG2}"/>
</TextBlock>
<Path Grid.Column="3"
Width="8" Height="8"
Margin="0,0,12,0"
Fill="Goldenrod"
Data="{StaticResource Icons.Modified}"
IsVisible="{Binding IsDirty}"/>
</Grid>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</UserControl>

View file

@ -0,0 +1,174 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public class SubmoduleTreeNodeToggleButton : ToggleButton
{
protected override Type StyleKeyOverride => typeof(ToggleButton);
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
DataContext is ViewModels.SubmoduleTreeNode { IsFolder: true } node)
{
var view = this.FindAncestorOfType<SubmodulesView>();
view?.ToggleNodeIsExpanded(node);
}
e.Handled = true;
}
}
public class SubmoduleTreeNodeIcon : UserControl
{
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<SubmoduleTreeNodeIcon, bool>(nameof(IsExpanded));
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsExpandedProperty)
UpdateContent();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
UpdateContent();
}
private void UpdateContent()
{
if (DataContext is not ViewModels.SubmoduleTreeNode node)
{
Content = null;
return;
}
if (node.Module != null)
CreateContent(new Thickness(0, 0, 0, 0), "Icons.Submodule");
else if (node.IsExpanded)
CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder.Open");
else
CreateContent(new Thickness(0, 2, 0, 0), "Icons.Folder");
}
private void CreateContent(Thickness margin, string iconKey)
{
var geo = this.FindResource(iconKey) as StreamGeometry;
if (geo == null)
return;
Content = new Avalonia.Controls.Shapes.Path()
{
Width = 12,
Height = 12,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Center,
Margin = margin,
Data = geo,
};
}
}
public partial class SubmodulesView : UserControl
{
public static readonly StyledProperty<ViewModels.SubmoduleCollection> SubmodulesProperty =
AvaloniaProperty.Register<SubmodulesView, ViewModels.SubmoduleCollection>(nameof(Submodules));
public ViewModels.SubmoduleCollection Submodules
{
get => GetValue(SubmodulesProperty);
set => SetValue(SubmodulesProperty, value);
}
public static readonly RoutedEvent<RoutedEventArgs> RowsChangedEvent =
RoutedEvent.Register<TagsView, RoutedEventArgs>(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> RowsChanged
{
add { AddHandler(RowsChangedEvent, value); }
remove { RemoveHandler(RowsChangedEvent, value); }
}
public int Rows
{
get;
private set;
}
public SubmodulesView()
{
InitializeComponent();
}
public void ToggleNodeIsExpanded(ViewModels.SubmoduleTreeNode node)
{
var submodules = Submodules;
if (submodules != null)
{
submodules.ToggleExpand(node);
Rows = submodules.Rows.Count;
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == SubmodulesProperty)
{
Rows = Submodules?.Rows.Count ?? 0;
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
else if (change.Property == IsVisibleProperty)
{
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
private void OnDoubleTappedNode(object sender, TappedEventArgs e)
{
if (sender is Control { DataContext: ViewModels.SubmoduleTreeNode node } &&
DataContext is ViewModels.Repository repo)
{
if (node.IsFolder)
ToggleNodeIsExpanded(node);
else if (node.Module.Status != Models.SubmoduleStatus.NotInited)
repo.OpenSubmodule(node.Module.Path);
}
e.Handled = true;
}
private void OnRowContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is Control { DataContext: ViewModels.SubmoduleTreeNode node } control &&
node.Module != null &&
DataContext is ViewModels.Repository repo)
{
var menu = repo.CreateContextMenuForSubmodule(node.Module);
menu?.Open(control);
}
e.Handled = true;
}
}
}