Skip to content

Commit de2f70b

Browse files
committed
feature: supports display tags in a tree (#350)
1 parent f59af0a commit de2f70b

File tree

11 files changed

+652
-113
lines changed

11 files changed

+652
-113
lines changed

src/Models/Watcher.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -198,7 +198,7 @@ private void OnRepositoryChanged(object o, FileSystemEventArgs e)
198198
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
199199
{
200200
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
201-
201+
202202
lock (_submodules)
203203
{
204204
if (_submodules.Count > 0)

src/Resources/Locales/en_US.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -485,6 +485,7 @@
485485
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">SHA</x:String>
486486
<x:String x:Key="Text.Repository.Search.ByUser" xml:space="preserve">Author &amp; Committer</x:String>
487487
<x:String x:Key="Text.Repository.SearchBranchTag" xml:space="preserve">Search Branches &amp; Tags</x:String>
488+
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">Show Tags as Tree</x:String>
488489
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">Statistics</x:String>
489490
<x:String x:Key="Text.Repository.Submodules" xml:space="preserve">SUBMODULES</x:String>
490491
<x:String x:Key="Text.Repository.Submodules.Add" xml:space="preserve">ADD SUBMODULE</x:String>

src/Resources/Locales/zh_CN.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@
487487
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">提交指纹</x:String>
488488
<x:String x:Key="Text.Repository.Search.ByUser" xml:space="preserve">作者及提交者</x:String>
489489
<x:String x:Key="Text.Repository.SearchBranchTag" xml:space="preserve">快速查找分支、标签</x:String>
490+
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">以树型结构展示</x:String>
490491
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">提交统计</x:String>
491492
<x:String x:Key="Text.Repository.Submodules" xml:space="preserve">子模块列表</x:String>
492493
<x:String x:Key="Text.Repository.Submodules.Add" xml:space="preserve">添加子模块</x:String>

src/Resources/Locales/zh_TW.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,6 +487,7 @@
487487
<x:String x:Key="Text.Repository.Search.BySHA" xml:space="preserve">提交指紋</x:String>
488488
<x:String x:Key="Text.Repository.Search.ByUser" xml:space="preserve">作者及提交者</x:String>
489489
<x:String x:Key="Text.Repository.SearchBranchTag" xml:space="preserve">快速查找分支、標籤</x:String>
490+
<x:String x:Key="Text.Repository.ShowTagsAsTree" xml:space="preserve">以樹型結構展示</x:String>
490491
<x:String x:Key="Text.Repository.Statistics" xml:space="preserve">提交統計</x:String>
491492
<x:String x:Key="Text.Repository.Submodules" xml:space="preserve">子模組列表</x:String>
492493
<x:String x:Key="Text.Repository.Submodules.Add" xml:space="preserve">新增子模組</x:String>

src/Resources/Styles.axaml

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1257,6 +1257,38 @@
12571257
<Setter Property="Opacity" Value="1"/>
12581258
</Style>
12591259

1260+
<Style Selector="ToggleButton.tag_display_mode">
1261+
<Setter Property="Margin" Value="0" />
1262+
<Setter Property="Background" Value="Transparent"/>
1263+
<Setter Property="Template">
1264+
<ControlTemplate>
1265+
<Border Background="Transparent"
1266+
Width="{TemplateBinding Width}"
1267+
Height="{TemplateBinding Height}"
1268+
HorizontalAlignment="Left"
1269+
VerticalAlignment="Center">
1270+
<Path x:Name="ChevronPath"
1271+
Width="11" Height="11"
1272+
Margin="0,1,0,0"
1273+
Data="{StaticResource Icons.Tree}"
1274+
Fill="{DynamicResource Brush.FG1}"
1275+
HorizontalAlignment="Center"
1276+
VerticalAlignment="Center"
1277+
Opacity="0.65"/>
1278+
</Border>
1279+
</ControlTemplate>
1280+
</Setter>
1281+
1282+
<Style Selector="^:checked /template/ Path#ChevronPath">
1283+
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
1284+
</Style>
1285+
1286+
<Style Selector="^:pointerover /template/ Path#ChevronPath">
1287+
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
1288+
<Setter Property="Opacity" Value="1"/>
1289+
</Style>
1290+
</Style>
1291+
12601292
<Style Selector="Slider">
12611293
<Style.Resources>
12621294
<Thickness x:Key="SliderTopHeaderMargin">0,0,0,4</Thickness>

src/ViewModels/Preference.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,12 @@ public string IgnoreUpdateTag
177177
set;
178178
} = string.Empty;
179179

180+
public bool ShowTagsAsTree
181+
{
182+
get => _showTagsAsTree;
183+
set => SetProperty(ref _showTagsAsTree, value);
184+
}
185+
180186
public bool UseTwoColumnsLayoutInHistories
181187
{
182188
get => _useTwoColumnsLayoutInHistories;
@@ -520,6 +526,7 @@ private bool RemoveNodeRecursive(RepositoryNode node, AvaloniaList<RepositoryNod
520526
private bool _useFixedTabWidth = true;
521527
private bool _check4UpdatesOnStartup = true;
522528

529+
private bool _showTagsAsTree = false;
523530
private bool _useTwoColumnsLayoutInHistories = false;
524531
private bool _displayTimeAsPeriodInHistories = false;
525532
private bool _useSideBySideDiff = false;

src/ViewModels/TagCollection.cs

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using Avalonia.Collections;
4+
using CommunityToolkit.Mvvm.ComponentModel;
5+
6+
namespace SourceGit.ViewModels
7+
{
8+
public class TagTreeNode : ObservableObject
9+
{
10+
public string FullPath { get; set; }
11+
public int Depth { get; private set; } = 0;
12+
public Models.Tag Tag { get; private set; } = null;
13+
public List<TagTreeNode> Children { get; private set; } = [];
14+
15+
public bool IsFolder
16+
{
17+
get => Tag == null;
18+
}
19+
20+
public bool IsFiltered
21+
{
22+
get => Tag?.IsFiltered ?? false;
23+
set
24+
{
25+
if (Tag != null)
26+
Tag.IsFiltered = value;
27+
}
28+
}
29+
30+
public bool IsExpanded
31+
{
32+
get => _isExpanded;
33+
set => SetProperty(ref _isExpanded, value);
34+
}
35+
36+
public TagTreeNode(Models.Tag t, int depth)
37+
{
38+
FullPath = t.Name;
39+
Depth = depth;
40+
Tag = t;
41+
IsExpanded = false;
42+
}
43+
44+
public TagTreeNode(string path, bool isExpanded, int depth)
45+
{
46+
FullPath = path;
47+
Depth = depth;
48+
IsExpanded = isExpanded;
49+
}
50+
51+
public static List<TagTreeNode> Build(IList<Models.Tag> tags, HashSet<string> expaneded)
52+
{
53+
var nodes = new List<TagTreeNode>();
54+
var folders = new Dictionary<string, TagTreeNode>();
55+
56+
foreach (var tag in tags)
57+
{
58+
var sepIdx = tag.Name.IndexOf('/', StringComparison.Ordinal);
59+
if (sepIdx == -1)
60+
{
61+
nodes.Add(new TagTreeNode(tag, 0));
62+
}
63+
else
64+
{
65+
TagTreeNode lastFolder = null;
66+
int depth = 0;
67+
68+
while (sepIdx != -1)
69+
{
70+
var folder = tag.Name.Substring(0, sepIdx);
71+
if (folders.TryGetValue(folder, out var value))
72+
{
73+
lastFolder = value;
74+
}
75+
else if (lastFolder == null)
76+
{
77+
lastFolder = new TagTreeNode(folder, expaneded.Contains(folder), depth);
78+
folders.Add(folder, lastFolder);
79+
InsertFolder(nodes, lastFolder);
80+
}
81+
else
82+
{
83+
var cur = new TagTreeNode(folder, expaneded.Contains(folder), depth);
84+
folders.Add(folder, cur);
85+
InsertFolder(lastFolder.Children, cur);
86+
lastFolder = cur;
87+
}
88+
89+
depth++;
90+
sepIdx = tag.Name.IndexOf('/', sepIdx + 1);
91+
}
92+
93+
lastFolder?.Children.Add(new TagTreeNode(tag, depth));
94+
}
95+
}
96+
97+
folders.Clear();
98+
return nodes;
99+
}
100+
101+
private static void InsertFolder(List<TagTreeNode> collection, TagTreeNode subFolder)
102+
{
103+
for (int i = 0; i < collection.Count; i++)
104+
{
105+
if (!collection[i].IsFolder)
106+
{
107+
collection.Insert(i, subFolder);
108+
return;
109+
}
110+
}
111+
112+
collection.Add(subFolder);
113+
}
114+
115+
private bool _isExpanded = true;
116+
}
117+
118+
public class TagCollectionAsList
119+
{
120+
public AvaloniaList<Models.Tag> Tags
121+
{
122+
get;
123+
set;
124+
} = [];
125+
}
126+
127+
public class TagCollectionAsTree
128+
{
129+
public List<TagTreeNode> Tree
130+
{
131+
get;
132+
set;
133+
} = [];
134+
135+
public AvaloniaList<TagTreeNode> Rows
136+
{
137+
get;
138+
set;
139+
} = [];
140+
}
141+
}

src/Views/Repository.axaml

Lines changed: 23 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -188,93 +188,35 @@
188188

189189
<!-- Tags -->
190190
<ToggleButton Grid.Row="4" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}">
191-
<Grid ColumnDefinitions="Auto,*,Auto">
191+
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
192192
<TextBlock Grid.Column="0" Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.Tags}"/>
193193
<TextBlock Grid.Column="1" Text="{Binding Tags, Converter={x:Static c:ListConverters.ToCount}}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold"/>
194-
<Button Grid.Column="2" Classes="icon_button" Width="14" Margin="8,0" Command="{Binding CreateNewTag}" ToolTip.Tip="{DynamicResource Text.Repository.Tags.Add}">
194+
<ToggleButton Grid.Column="2"
195+
Classes="tag_display_mode"
196+
Width="14"
197+
IsChecked="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowTagsAsTree, Mode=TwoWay}"
198+
ToolTip.Tip="{DynamicResource Text.Repository.ShowTagsAsTree}"/>
199+
<Button Grid.Column="3"
200+
Classes="icon_button"
201+
Width="14"
202+
Margin="8,0"
203+
Command="{Binding CreateNewTag}"
204+
ToolTip.Tip="{DynamicResource Text.Repository.Tags.Add}">
195205
<Path Width="12" Height="12" Data="{StaticResource Icons.Tag.Add}"/>
196206
</Button>
197207
</Grid>
198208
</ToggleButton>
199-
<DataGrid Grid.Row="5"
200-
x:Name="TagsList"
201-
Height="0"
202-
Margin="8,0,4,0"
203-
Background="Transparent"
204-
ItemsSource="{Binding VisibleTags}"
205-
SelectionMode="Single"
206-
CanUserReorderColumns="False"
207-
CanUserResizeColumns="False"
208-
CanUserSortColumns="False"
209-
IsReadOnly="True"
210-
HeadersVisibility="None"
211-
Focusable="False"
212-
RowHeight="24"
213-
HorizontalScrollBarVisibility="Disabled"
214-
VerticalScrollBarVisibility="Auto"
215-
IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}"
216-
SelectionChanged="OnTagDataGridSelectionChanged"
217-
ContextRequested="OnTagContextRequested"
218-
PropertyChanged="OnLeftSidebarDataGridPropertyChanged">
219-
<DataGrid.Styles>
220-
<Style Selector="DataGridRow">
221-
<Setter Property="CornerRadius" Value="4" />
222-
<Setter Property="Height" Value="24"/>
223-
</Style>
224-
225-
<Style Selector="DataGridRow /template/ Border#RowBorder">
226-
<Setter Property="ClipToBounds" Value="True" />
227-
</Style>
228-
229-
<Style Selector="Grid.repository_leftpanel DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
230-
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
231-
<Setter Property="Opacity" Value=".5"/>
232-
</Style>
233-
<Style Selector="Grid.repository_leftpanel DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
234-
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
235-
<Setter Property="Opacity" Value="1"/>
236-
</Style>
237-
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
238-
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
239-
<Setter Property="Opacity" Value=".65"/>
240-
</Style>
241-
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
242-
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
243-
<Setter Property="Opacity" Value=".8"/>
244-
</Style>
245-
</DataGrid.Styles>
246-
247-
<DataGrid.Columns>
248-
<DataGridTemplateColumn Header="ICON">
249-
<DataGridTemplateColumn.CellTemplate>
250-
<DataTemplate x:DataType="{x:Type m:Tag}">
251-
<Path Width="10" Height="10" Margin="8,0" Data="{StaticResource Icons.Tag}"/>
252-
</DataTemplate>
253-
</DataGridTemplateColumn.CellTemplate>
254-
</DataGridTemplateColumn>
255-
256-
<DataGridTemplateColumn Width="*" Header="NAME">
257-
<DataGridTemplateColumn.CellTemplate>
258-
<DataTemplate x:DataType="{x:Type m:Tag}">
259-
<TextBlock Text="{Binding Name}" Classes="primary" TextTrimming="CharacterEllipsis" />
260-
</DataTemplate>
261-
</DataGridTemplateColumn.CellTemplate>
262-
</DataGridTemplateColumn>
263-
264-
<DataGridTemplateColumn Header="FILTER">
265-
<DataGridTemplateColumn.CellTemplate>
266-
<DataTemplate x:DataType="{x:Type m:Tag}">
267-
<ToggleButton Classes="filter"
268-
Margin="0,0,8,0"
269-
Background="Transparent"
270-
IsCheckedChanged="OnTagFilterIsCheckedChanged"
271-
IsChecked="{Binding IsFiltered}"
272-
ToolTip.Tip="{DynamicResource Text.Filter}"/>
273-
</DataTemplate>
274-
</DataGridTemplateColumn.CellTemplate>
275-
</DataGridTemplateColumn>
276-
</DataGrid.Columns>
277-
</DataGrid>
209+
<v:TagsView Grid.Row="5"
210+
x:Name="TagsList"
211+
Height="0"
212+
Margin="8,0,4,0"
213+
Background="Transparent"
214+
ShowTagsAsTree="{Binding Source={x:Static vm:Preference.Instance}, Path=ShowTagsAsTree, Mode=OneWay}"
215+
Tags="{Binding VisibleTags}"
216+
Focusable="False"
217+
IsVisible="{Binding IsTagGroupExpanded, Mode=OneWay}"
218+
SelectionChanged="OnTagsSelectionChanged"
219+
RowsChanged="OnTagsRowsChanged"/>
278220

279221
<!-- Submodules -->
280222
<ToggleButton Grid.Row="6" Classes="group_expander" IsChecked="{Binding IsSubmoduleGroupExpanded, Mode=TwoWay}">

0 commit comments

Comments
 (0)