Skip to content

Commit 463d161

Browse files
committed
refactor: show submodule as tree instead of list (#1307)
1 parent 5ec51ee commit 463d161

File tree

6 files changed

+534
-136
lines changed

6 files changed

+534
-136
lines changed

src/ViewModels/Repository.cs

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ public List<Models.Submodule> Submodules
210210
private set => SetProperty(ref _submodules, value);
211211
}
212212

213-
public List<Models.Submodule> VisibleSubmodules
213+
public SubmoduleCollection VisibleSubmodules
214214
{
215215
get => _visibleSubmodules;
216216
private set => SetProperty(ref _visibleSubmodules, value);
@@ -2512,7 +2512,7 @@ private BranchTreeNode.Builder BuildBranchTree(List<Models.Branch> branches, Lis
25122512
return visible;
25132513
}
25142514

2515-
private List<Models.Submodule> BuildVisibleSubmodules()
2515+
private SubmoduleCollection BuildVisibleSubmodules()
25162516
{
25172517
var visible = new List<Models.Submodule>();
25182518
if (string.IsNullOrEmpty(_filter))
@@ -2527,7 +2527,8 @@ private BranchTreeNode.Builder BuildBranchTree(List<Models.Branch> branches, Lis
25272527
visible.Add(s);
25282528
}
25292529
}
2530-
return visible;
2530+
2531+
return SubmoduleCollection.Build(visible, _visibleSubmodules);
25312532
}
25322533

25332534
private void RefreshHistoriesFilters(bool refresh)
@@ -2759,7 +2760,7 @@ private void AutoFetchImpl(object sender)
27592760
private List<Models.Tag> _tags = new List<Models.Tag>();
27602761
private List<Models.Tag> _visibleTags = new List<Models.Tag>();
27612762
private List<Models.Submodule> _submodules = new List<Models.Submodule>();
2762-
private List<Models.Submodule> _visibleSubmodules = new List<Models.Submodule>();
2763+
private SubmoduleCollection _visibleSubmodules = new SubmoduleCollection();
27632764

27642765
private bool _isAutoFetching = false;
27652766
private Timer _autoFetchTimer = null;
Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,206 @@
1+
using System;
2+
using System.Collections.Generic;
3+
4+
using Avalonia.Collections;
5+
6+
using CommunityToolkit.Mvvm.ComponentModel;
7+
8+
namespace SourceGit.ViewModels
9+
{
10+
public class SubmoduleTreeNode : ObservableObject
11+
{
12+
public string FullPath { get; set; } = string.Empty;
13+
public int Depth { get; private set; } = 0;
14+
public Models.Submodule Module { get; private set; } = null;
15+
public List<SubmoduleTreeNode> Children { get; private set; } = [];
16+
public int Counter = 0;
17+
18+
public bool IsFolder
19+
{
20+
get => Module == null;
21+
}
22+
23+
public bool IsExpanded
24+
{
25+
get => _isExpanded;
26+
set => SetProperty(ref _isExpanded, value);
27+
}
28+
29+
public string ChildCounter
30+
{
31+
get => Counter > 0 ? $"({Counter})" : string.Empty;
32+
}
33+
34+
public bool IsDirty
35+
{
36+
get => Module?.IsDirty ?? false;
37+
}
38+
39+
public SubmoduleTreeNode(Models.Submodule module, int depth)
40+
{
41+
FullPath = module.Path;
42+
Depth = depth;
43+
Module = module;
44+
IsExpanded = false;
45+
}
46+
47+
public SubmoduleTreeNode(string path, int depth, bool isExpanded)
48+
{
49+
FullPath = path;
50+
Depth = depth;
51+
IsExpanded = isExpanded;
52+
Counter = 1;
53+
}
54+
55+
public static List<SubmoduleTreeNode> Build(IList<Models.Submodule> submodules, HashSet<string> expaneded)
56+
{
57+
var nodes = new List<SubmoduleTreeNode>();
58+
var folders = new Dictionary<string, SubmoduleTreeNode>();
59+
60+
foreach (var module in submodules)
61+
{
62+
var sepIdx = module.Path.IndexOf('/', StringComparison.Ordinal);
63+
if (sepIdx == -1)
64+
{
65+
nodes.Add(new SubmoduleTreeNode(module, 0));
66+
}
67+
else
68+
{
69+
SubmoduleTreeNode lastFolder = null;
70+
int depth = 0;
71+
72+
while (sepIdx != -1)
73+
{
74+
var folder = module.Path.Substring(0, sepIdx);
75+
if (folders.TryGetValue(folder, out var value))
76+
{
77+
lastFolder = value;
78+
lastFolder.Counter++;
79+
}
80+
else if (lastFolder == null)
81+
{
82+
lastFolder = new SubmoduleTreeNode(folder, depth, expaneded.Contains(folder));
83+
folders.Add(folder, lastFolder);
84+
InsertFolder(nodes, lastFolder);
85+
}
86+
else
87+
{
88+
var cur = new SubmoduleTreeNode(folder, depth, expaneded.Contains(folder));
89+
folders.Add(folder, cur);
90+
InsertFolder(lastFolder.Children, cur);
91+
lastFolder = cur;
92+
}
93+
94+
depth++;
95+
sepIdx = module.Path.IndexOf('/', sepIdx + 1);
96+
}
97+
98+
lastFolder?.Children.Add(new SubmoduleTreeNode(module, depth));
99+
}
100+
}
101+
102+
folders.Clear();
103+
return nodes;
104+
}
105+
106+
private static void InsertFolder(List<SubmoduleTreeNode> collection, SubmoduleTreeNode subFolder)
107+
{
108+
for (int i = 0; i < collection.Count; i++)
109+
{
110+
if (!collection[i].IsFolder)
111+
{
112+
collection.Insert(i, subFolder);
113+
return;
114+
}
115+
}
116+
117+
collection.Add(subFolder);
118+
}
119+
120+
private bool _isExpanded = false;
121+
}
122+
123+
public class SubmoduleCollection
124+
{
125+
public List<SubmoduleTreeNode> Tree
126+
{
127+
get;
128+
set;
129+
} = [];
130+
131+
public AvaloniaList<SubmoduleTreeNode> Rows
132+
{
133+
get;
134+
set;
135+
} = [];
136+
137+
public static SubmoduleCollection Build(List<Models.Submodule> submodules, SubmoduleCollection old)
138+
{
139+
var oldExpanded = new HashSet<string>();
140+
foreach (var row in old.Rows)
141+
{
142+
if (row.IsFolder && row.IsExpanded)
143+
oldExpanded.Add(row.FullPath);
144+
}
145+
146+
var collection = new SubmoduleCollection();
147+
collection.Tree = SubmoduleTreeNode.Build(submodules, oldExpanded);
148+
149+
var rows = new List<SubmoduleTreeNode>();
150+
collection.MakeTreeRows(rows, collection.Tree);
151+
collection.Rows.AddRange(rows);
152+
153+
return collection;
154+
}
155+
156+
public void Clear()
157+
{
158+
Tree.Clear();
159+
Rows.Clear();
160+
}
161+
162+
public void ToggleExpand(SubmoduleTreeNode node)
163+
{
164+
node.IsExpanded = !node.IsExpanded;
165+
166+
var rows = Rows;
167+
var depth = node.Depth;
168+
var idx = rows.IndexOf(node);
169+
if (idx == -1)
170+
return;
171+
172+
if (node.IsExpanded)
173+
{
174+
var subrows = new List<SubmoduleTreeNode>();
175+
MakeTreeRows(subrows, node.Children);
176+
rows.InsertRange(idx + 1, subrows);
177+
}
178+
else
179+
{
180+
var removeCount = 0;
181+
for (int i = idx + 1; i < rows.Count; i++)
182+
{
183+
var row = rows[i];
184+
if (row.Depth <= depth)
185+
break;
186+
187+
removeCount++;
188+
}
189+
rows.RemoveRange(idx + 1, removeCount);
190+
}
191+
}
192+
193+
private void MakeTreeRows(List<SubmoduleTreeNode> rows, List<SubmoduleTreeNode> nodes)
194+
{
195+
foreach (var node in nodes)
196+
{
197+
rows.Add(node);
198+
199+
if (!node.IsExpanded || !node.IsFolder)
200+
continue;
201+
202+
MakeTreeRows(rows, node.Children);
203+
}
204+
}
205+
}
206+
}

src/Views/Repository.axaml

Lines changed: 9 additions & 95 deletions
Original file line numberDiff line numberDiff line change
@@ -330,100 +330,14 @@
330330
</Button>
331331
</Grid>
332332
</ToggleButton>
333-
<ListBox Grid.Row="7"
334-
x:Name="SubmoduleList"
335-
Height="0"
336-
Margin="12,0,4,0"
337-
Classes="repo_left_content_list"
338-
ItemsSource="{Binding VisibleSubmodules}"
339-
SelectionMode="Single"
340-
ContextRequested="OnSubmoduleContextRequested"
341-
DoubleTapped="OnDoubleTappedSubmodule"
342-
PropertyChanged="OnLeftSidebarListBoxPropertyChanged"
343-
IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}">
344-
<ListBox.Styles>
345-
<Style Selector="ListBoxItem">
346-
<Setter Property="CornerRadius" Value="4"/>
347-
</Style>
348-
</ListBox.Styles>
349-
<ListBox.ItemTemplate>
350-
<DataTemplate DataType="m:Submodule">
351-
<Grid ColumnDefinitions="Auto,*,8,8" Background="Transparent">
352-
<ToolTip.Tip>
353-
<StackPanel Orientation="Vertical">
354-
<StackPanel Orientation="Horizontal">
355-
<Path Width="10" Height="10" Data="{StaticResource Icons.Submodule}"/>
356-
<TextBlock FontWeight="Bold" Margin="4,0,0,0" Text="{Binding Path}"/>
357-
</StackPanel>
358-
359-
<Grid RowDefinitions="24,24" ColumnDefinitions="Auto,Auto" Margin="0,8,0,0">
360-
<TextBlock Grid.Row="0" Grid.Column="0"
361-
Classes="info_label"
362-
HorizontalAlignment="Left" VerticalAlignment="Center"
363-
Text="{DynamicResource Text.CommitDetail.Info.SHA}"/>
364-
<StackPanel Grid.Row="0" Grid.Column="1"
365-
Orientation="Horizontal"
366-
Margin="8,0,0,0">
367-
<TextBlock Text="{Binding SHA, Converter={x:Static c:StringConverters.ToShortSHA}}"
368-
VerticalAlignment="Center"/>
369-
370-
<Path Margin="6,0,0,0"
371-
HorizontalAlignment="Left" VerticalAlignment="Center"
372-
Width="12" Height="12"
373-
Data="{StaticResource Icons.Check}"
374-
Fill="Green"
375-
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}"/>
376-
<Border Height="16"
377-
Margin="6,0,0,0" Padding="4,0"
378-
HorizontalAlignment="Left" VerticalAlignment="Center"
379-
Background="DarkOrange"
380-
CornerRadius="4"
381-
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.NotEqual}, ConverterParameter={x:Static m:SubmoduleStatus.Normal}}">
382-
<Grid>
383-
<TextBlock VerticalAlignment="Center"
384-
Text="{DynamicResource Text.Submodule.Status.NotInited}"
385-
Foreground="White"
386-
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.NotInited}}"/>
387-
<TextBlock VerticalAlignment="Center"
388-
Text="{DynamicResource Text.Submodule.Status.Modified}"
389-
Foreground="White"
390-
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Modified}}"/>
391-
<TextBlock VerticalAlignment="Center"
392-
Text="{DynamicResource Text.Submodule.Status.RevisionChanged}"
393-
Foreground="White"
394-
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.RevisionChanged}}"/>
395-
<TextBlock VerticalAlignment="Center"
396-
Text="{DynamicResource Text.Submodule.Status.Unmerged}"
397-
Foreground="White"
398-
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.Equal}, ConverterParameter={x:Static m:SubmoduleStatus.Unmerged}}"/>
399-
</Grid>
400-
</Border>
401-
</StackPanel>
402-
403-
<TextBlock Grid.Row="1" Grid.Column="0"
404-
Classes="info_label"
405-
HorizontalAlignment="Left" VerticalAlignment="Center"
406-
Text="{DynamicResource Text.Submodule.URL}"/>
407-
<TextBlock Grid.Row="1" Grid.Column="1"
408-
Margin="8,0,0,0"
409-
Text="{Binding URL}"
410-
Foreground="{DynamicResource Brush.Link}"
411-
VerticalAlignment="Center"/>
412-
</Grid>
413-
</StackPanel>
414-
</ToolTip.Tip>
415-
416-
<Path Grid.Column="0" Width="10" Height="10" Margin="8,0" Data="{StaticResource Icons.Submodule}"/>
417-
<TextBlock Grid.Column="1" Text="{Binding Path}" ClipToBounds="True" Classes="primary" TextTrimming="CharacterEllipsis"/>
418-
<Path Grid.Column="2"
419-
Width="8" Height="8"
420-
Fill="Goldenrod"
421-
Data="{StaticResource Icons.Modified}"
422-
IsVisible="{Binding IsDirty}"/>
423-
</Grid>
424-
</DataTemplate>
425-
</ListBox.ItemTemplate>
426-
</ListBox>
333+
<v:SubmodulesView Grid.Row="7"
334+
x:Name="SubmoduleList"
335+
Height="0"
336+
Margin="8,0,4,0"
337+
Submodules="{Binding VisibleSubmodules}"
338+
RowsChanged="OnSubmodulesRowsChanged"
339+
Focusable="False"
340+
IsVisible="{Binding IsSubmoduleGroupExpanded, Mode=OneWay}"/>
427341

428342
<!-- Worktrees -->
429343
<ToggleButton Grid.Row="8" Classes="group_expander" IsChecked="{Binding IsWorktreeGroupExpanded, Mode=TwoWay}">
@@ -461,7 +375,7 @@
461375
SelectionMode="Single"
462376
ContextRequested="OnWorktreeContextRequested"
463377
DoubleTapped="OnDoubleTappedWorktree"
464-
PropertyChanged="OnLeftSidebarListBoxPropertyChanged"
378+
PropertyChanged="OnWorktreeListPropertyChanged"
465379
IsVisible="{Binding IsWorktreeGroupExpanded, Mode=OneWay}">
466380
<ListBox.Styles>
467381
<Style Selector="ListBoxItem">

0 commit comments

Comments
 (0)