mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-20 11:44:59 +00:00
refactor: show submodule as tree instead of list (#1307)
This commit is contained in:
parent
5ec51eefb9
commit
463d161ac7
6 changed files with 534 additions and 136 deletions
|
@ -210,7 +210,7 @@ namespace SourceGit.ViewModels
|
|||
private set => SetProperty(ref _submodules, value);
|
||||
}
|
||||
|
||||
public List<Models.Submodule> VisibleSubmodules
|
||||
public SubmoduleCollection VisibleSubmodules
|
||||
{
|
||||
get => _visibleSubmodules;
|
||||
private set => SetProperty(ref _visibleSubmodules, value);
|
||||
|
@ -2512,7 +2512,7 @@ namespace SourceGit.ViewModels
|
|||
return visible;
|
||||
}
|
||||
|
||||
private List<Models.Submodule> BuildVisibleSubmodules()
|
||||
private SubmoduleCollection BuildVisibleSubmodules()
|
||||
{
|
||||
var visible = new List<Models.Submodule>();
|
||||
if (string.IsNullOrEmpty(_filter))
|
||||
|
@ -2527,7 +2527,8 @@ namespace SourceGit.ViewModels
|
|||
visible.Add(s);
|
||||
}
|
||||
}
|
||||
return visible;
|
||||
|
||||
return SubmoduleCollection.Build(visible, _visibleSubmodules);
|
||||
}
|
||||
|
||||
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> _visibleTags = new List<Models.Tag>();
|
||||
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 Timer _autoFetchTimer = null;
|
||||
|
|
206
src/ViewModels/SubmoduleTreeNode.cs
Normal file
206
src/ViewModels/SubmoduleTreeNode.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -330,100 +330,14 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
</ToggleButton>
|
||||
<ListBox Grid.Row="7"
|
||||
x:Name="SubmoduleList"
|
||||
Height="0"
|
||||
Margin="12,0,4,0"
|
||||
Classes="repo_left_content_list"
|
||||
ItemsSource="{Binding VisibleSubmodules}"
|
||||
SelectionMode="Single"
|
||||
ContextRequested="OnSubmoduleContextRequested"
|
||||
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>
|
||||
<v:SubmodulesView Grid.Row="7"
|
||||
x:Name="SubmoduleList"
|
||||
Height="0"
|
||||
Margin="8,0,4,0"
|
||||
Submodules="{Binding VisibleSubmodules}"
|
||||
RowsChanged="OnSubmodulesRowsChanged"
|
||||
Focusable="False"
|
||||
IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}"/>
|
||||
|
||||
<!-- Worktrees -->
|
||||
<ToggleButton Grid.Row="8" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}">
|
||||
|
@ -461,7 +375,7 @@
|
|||
SelectionMode="Single"
|
||||
ContextRequested="OnWorktreeContextRequested"
|
||||
DoubleTapped="OnDoubleTappedWorktree"
|
||||
PropertyChanged="OnLeftSidebarListBoxPropertyChanged"
|
||||
PropertyChanged="OnWorktreeListPropertyChanged"
|
||||
IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}">
|
||||
<ListBox.Styles>
|
||||
<Style Selector="ListBoxItem">
|
||||
|
|
|
@ -179,26 +179,9 @@ namespace SourceGit.Views
|
|||
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)
|
||||
{
|
||||
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);
|
||||
}
|
||||
|
||||
UpdateLeftSidebarLayout();
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
|
@ -223,7 +206,7 @@ namespace SourceGit.Views
|
|||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void OnLeftSidebarListBoxPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e)
|
||||
private void OnWorktreeListPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.Property == ListBox.ItemsSourceProperty || e.Property == ListBox.IsVisibleProperty)
|
||||
UpdateLeftSidebarLayout();
|
||||
|
@ -252,26 +235,26 @@ namespace SourceGit.Views
|
|||
var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0;
|
||||
var desiredBranches = (localBranchRows + remoteBranchRows) * 24.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 desiredOthers = desiredTag + desiredSubmodule + desiredWorktree;
|
||||
var hasOverflow = (desiredBranches + desiredOthers > leftHeight);
|
||||
|
||||
if (vm.IsTagGroupExpanded)
|
||||
if (vm.IsWorktreeGroupExpanded)
|
||||
{
|
||||
var height = desiredTag;
|
||||
var height = desiredWorktree;
|
||||
if (hasOverflow)
|
||||
{
|
||||
var test = leftHeight - desiredBranches - desiredSubmodule - desiredWorktree;
|
||||
var test = leftHeight - desiredBranches - desiredTag - desiredSubmodule;
|
||||
if (test < 0)
|
||||
height = Math.Min(200, height);
|
||||
height = Math.Min(120, height);
|
||||
else
|
||||
height = Math.Max(200, test);
|
||||
height = Math.Max(120, test);
|
||||
}
|
||||
|
||||
leftHeight -= height;
|
||||
TagsList.Height = height;
|
||||
hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight;
|
||||
WorktreeList.Height = height;
|
||||
hasOverflow = (desiredBranches + desiredTag + desiredSubmodule) > leftHeight;
|
||||
}
|
||||
|
||||
if (vm.IsSubmoduleGroupExpanded)
|
||||
|
@ -279,32 +262,32 @@ namespace SourceGit.Views
|
|||
var height = desiredSubmodule;
|
||||
if (hasOverflow)
|
||||
{
|
||||
var test = leftHeight - desiredBranches - desiredWorktree;
|
||||
var test = leftHeight - desiredBranches - desiredTag;
|
||||
if (test < 0)
|
||||
height = Math.Min(200, height);
|
||||
height = Math.Min(120, height);
|
||||
else
|
||||
height = Math.Max(200, test);
|
||||
height = Math.Max(120, test);
|
||||
}
|
||||
|
||||
leftHeight -= 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)
|
||||
{
|
||||
var test = leftHeight - desiredBranches;
|
||||
if (test < 0)
|
||||
height = Math.Min(200, height);
|
||||
height = Math.Min(120, height);
|
||||
else
|
||||
height = Math.Max(200, test);
|
||||
height = Math.Max(120, test);
|
||||
}
|
||||
|
||||
leftHeight -= height;
|
||||
WorktreeList.Height = height;
|
||||
TagsList.Height = height;
|
||||
}
|
||||
|
||||
if (leftHeight > 0 && desiredBranches > leftHeight)
|
||||
|
|
120
src/Views/SubmodulesView.axaml
Normal file
120
src/Views/SubmodulesView.axaml
Normal 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>
|
174
src/Views/SubmodulesView.axaml.cs
Normal file
174
src/Views/SubmodulesView.axaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue