diff --git a/src/Models/Watcher.cs b/src/Models/Watcher.cs
index d10d8670..6cd77a15 100644
--- a/src/Models/Watcher.cs
+++ b/src/Models/Watcher.cs
@@ -198,7 +198,7 @@ namespace SourceGit.Models
(name.StartsWith("worktrees/", StringComparison.Ordinal) && name.EndsWith("/HEAD", StringComparison.Ordinal)))
{
_updateBranch = DateTime.Now.AddSeconds(.5).ToFileTime();
-
+
lock (_submodules)
{
if (_submodules.Count > 0)
diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml
index 2c1e570e..364494f0 100644
--- a/src/Resources/Locales/en_US.axaml
+++ b/src/Resources/Locales/en_US.axaml
@@ -485,6 +485,7 @@
SHA
Author & Committer
Search Branches & Tags
+ Show Tags as Tree
Statistics
SUBMODULES
ADD SUBMODULE
diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml
index c0ef6b6e..58e915db 100644
--- a/src/Resources/Locales/zh_CN.axaml
+++ b/src/Resources/Locales/zh_CN.axaml
@@ -487,6 +487,7 @@
提交指纹
作者及提交者
快速查找分支、标签
+ 以树型结构展示
提交统计
子模块列表
添加子模块
diff --git a/src/Resources/Locales/zh_TW.axaml b/src/Resources/Locales/zh_TW.axaml
index f991c912..167933da 100644
--- a/src/Resources/Locales/zh_TW.axaml
+++ b/src/Resources/Locales/zh_TW.axaml
@@ -487,6 +487,7 @@
提交指紋
作者及提交者
快速查找分支、標籤
+ 以樹型結構展示
提交統計
子模組列表
新增子模組
diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml
index ffe69765..f40e88f6 100644
--- a/src/Resources/Styles.axaml
+++ b/src/Resources/Styles.axaml
@@ -1257,6 +1257,38 @@
+
+
+
+
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs
index 0c9a106f..8b3eaac4 100644
--- a/src/Views/Repository.axaml.cs
+++ b/src/Views/Repository.axaml.cs
@@ -2,7 +2,6 @@ using System;
using Avalonia;
using Avalonia.Controls;
-using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
@@ -30,6 +29,9 @@ namespace SourceGit.Views
private void OnSearchKeyDown(object _, KeyEventArgs e)
{
var repo = DataContext as ViewModels.Repository;
+ if (repo == null)
+ return;
+
if (e.Key == Key.Enter)
{
if (!string.IsNullOrWhiteSpace(repo.SearchCommitFilter))
@@ -79,46 +81,25 @@ namespace SourceGit.Views
private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2)
{
RemoteBranchTree.UnselectAll();
- TagsList.SelectedItem = null;
+ TagsList.UnselectAll();
}
private void OnRemoteBranchTreeSelectionChanged(object _1, RoutedEventArgs _2)
{
LocalBranchTree.UnselectAll();
- TagsList.SelectedItem = null;
+ TagsList.UnselectAll();
}
- private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs _)
+ private void OnTagsRowsChanged(object _, RoutedEventArgs e)
{
- if (sender is DataGrid { SelectedItem: Models.Tag tag })
- {
- LocalBranchTree.UnselectAll();
- RemoteBranchTree.UnselectAll();
-
- if (DataContext is ViewModels.Repository repo)
- repo.NavigateToCommit(tag.SHA);
- }
- }
-
- private void OnTagContextRequested(object sender, ContextRequestedEventArgs e)
- {
- if (sender is DataGrid { SelectedItem: Models.Tag tag } grid && DataContext is ViewModels.Repository repo)
- {
- var menu = repo.CreateContextMenuForTag(tag);
- grid.OpenContextMenu(menu);
- }
-
+ UpdateLeftSidebarLayout();
e.Handled = true;
}
- private void OnTagFilterIsCheckedChanged(object sender, RoutedEventArgs e)
+ private void OnTagsSelectionChanged(object _1, RoutedEventArgs _2)
{
- if (sender is ToggleButton { DataContext: Models.Tag tag } toggle && DataContext is ViewModels.Repository repo)
- {
- repo.UpdateFilter(tag.Name, toggle.IsChecked == true);
- }
-
- e.Handled = true;
+ LocalBranchTree.UnselectAll();
+ RemoteBranchTree.UnselectAll();
}
private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e)
@@ -188,7 +169,7 @@ namespace SourceGit.Views
var localBranchRows = vm.IsLocalBranchGroupExpanded ? LocalBranchTree.Rows.Count : 0;
var remoteBranchRows = vm.IsRemoteGroupExpanded ? RemoteBranchTree.Rows.Count : 0;
var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0;
- var desiredTag = vm.IsTagGroupExpanded ? TagsList.RowHeight * vm.VisibleTags.Count : 0;
+ var desiredTag = vm.IsTagGroupExpanded ? 24.0 * TagsList.Rows : 0;
var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? SubmoduleList.RowHeight * vm.Submodules.Count : 0;
var desiredWorktree = vm.IsWorktreeGroupExpanded ? WorktreeList.RowHeight * vm.Worktrees.Count : 0;
var desiredOthers = desiredTag + desiredSubmodule + desiredWorktree;
@@ -295,9 +276,12 @@ namespace SourceGit.Views
}
}
- private void OnSearchSuggestionBoxKeyDown(object sender, KeyEventArgs e)
+ private void OnSearchSuggestionBoxKeyDown(object _, KeyEventArgs e)
{
var repo = DataContext as ViewModels.Repository;
+ if (repo == null)
+ return;
+
if (e.Key == Key.Escape)
{
repo.IsSearchCommitSuggestionOpen = false;
@@ -317,6 +301,9 @@ namespace SourceGit.Views
private void OnSearchSuggestionDoubleTapped(object sender, TappedEventArgs e)
{
var repo = DataContext as ViewModels.Repository;
+ if (repo == null)
+ return;
+
var content = (sender as StackPanel)?.DataContext as string;
if (!string.IsNullOrEmpty(content))
{
diff --git a/src/Views/TagsView.axaml b/src/Views/TagsView.axaml
new file mode 100644
index 00000000..bcbbe358
--- /dev/null
+++ b/src/Views/TagsView.axaml
@@ -0,0 +1,103 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Views/TagsView.axaml.cs b/src/Views/TagsView.axaml.cs
new file mode 100644
index 00000000..23d31ab4
--- /dev/null
+++ b/src/Views/TagsView.axaml.cs
@@ -0,0 +1,324 @@
+using System;
+using System.Collections.Generic;
+
+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 TagTreeNodeToggleButton : ToggleButton
+ {
+ protected override Type StyleKeyOverride => typeof(ToggleButton);
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
+ DataContext is ViewModels.TagTreeNode { IsFolder: true } node)
+ {
+ var view = this.FindAncestorOfType();
+ view?.ToggleNodeIsExpanded(node);
+ }
+
+ e.Handled = true;
+ }
+ }
+
+ public class TagTreeNodeIcon : UserControl
+ {
+ public static readonly StyledProperty NodeProperty =
+ AvaloniaProperty.Register(nameof(Node));
+
+ public ViewModels.TagTreeNode 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 TagTreeNodeIcon()
+ {
+ NodeProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent());
+ IsExpandedProperty.Changed.AddClassHandler((icon, _) => icon.UpdateContent());
+ }
+
+ private void UpdateContent()
+ {
+ var node = Node;
+ if (node == null)
+ {
+ Content = null;
+ return;
+ }
+
+ if (node.Tag != null)
+ CreateContent(new Thickness(0, 2, 0, 0), "Icons.Tag");
+ 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 TagsView : UserControl
+ {
+ public static readonly StyledProperty ShowTagsAsTreeProperty =
+ AvaloniaProperty.Register(nameof(ShowTagsAsTree));
+
+ public bool ShowTagsAsTree
+ {
+ get => GetValue(ShowTagsAsTreeProperty);
+ set => SetValue(ShowTagsAsTreeProperty, value);
+ }
+
+ public static readonly StyledProperty> TagsProperty =
+ AvaloniaProperty.Register>(nameof(Tags));
+
+ public List Tags
+ {
+ get => GetValue(TagsProperty);
+ set => SetValue(TagsProperty, value);
+ }
+
+ public static readonly RoutedEvent SelectionChangedEvent =
+ RoutedEvent.Register(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ public event EventHandler SelectionChanged
+ {
+ add { AddHandler(SelectionChangedEvent, value); }
+ remove { RemoveHandler(SelectionChangedEvent, value); }
+ }
+
+ public static readonly RoutedEvent RowsChangedEvent =
+ RoutedEvent.Register(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
+
+ public event EventHandler RowsChanged
+ {
+ add { AddHandler(RowsChangedEvent, value); }
+ remove { RemoveHandler(RowsChangedEvent, value); }
+ }
+
+ public int Rows
+ {
+ get;
+ private set;
+ }
+
+ public TagsView()
+ {
+ InitializeComponent();
+ }
+
+ public void UnselectAll()
+ {
+ var list = this.FindDescendantOfType();
+ if (list != null)
+ list.SelectedItem = null;
+ }
+
+ public void ToggleNodeIsExpanded(ViewModels.TagTreeNode node)
+ {
+ if (Content is ViewModels.TagCollectionAsTree tree)
+ {
+ node.IsExpanded = !node.IsExpanded;
+
+ var depth = node.Depth;
+ var idx = tree.Rows.IndexOf(node);
+ if (idx == -1)
+ return;
+
+ if (node.IsExpanded)
+ {
+ var subrows = new List();
+ MakeTreeRows(subrows, node.Children);
+ tree.Rows.InsertRange(idx + 1, subrows);
+ }
+ else
+ {
+ var removeCount = 0;
+ for (int i = idx + 1; i < tree.Rows.Count; i++)
+ {
+ var row = tree.Rows[i];
+ if (row.Depth <= depth)
+ break;
+
+ removeCount++;
+ }
+ tree.Rows.RemoveRange(idx + 1, removeCount);
+ }
+
+ Rows = tree.Rows.Count;
+ RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
+ }
+ }
+
+ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
+ {
+ base.OnPropertyChanged(change);
+
+ if (change.Property == ShowTagsAsTreeProperty || change.Property == TagsProperty)
+ {
+ UpdateDataSource();
+ RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
+ }
+ else if (change.Property == IsVisibleProperty)
+ {
+ RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
+ }
+ }
+
+ private void OnDoubleTappedNode(object sender, TappedEventArgs e)
+ {
+ if (sender is Grid { DataContext: ViewModels.TagTreeNode node })
+ {
+ if (node.IsFolder)
+ ToggleNodeIsExpanded(node);
+ }
+
+ e.Handled = true;
+ }
+
+ private void OnRowContextRequested(object sender, ContextRequestedEventArgs e)
+ {
+ var control = sender as Control;
+ if (control == null)
+ return;
+
+ Models.Tag selected;
+ if (control.DataContext is ViewModels.TagTreeNode node)
+ selected = node.Tag;
+ else if (control.DataContext is Models.Tag tag)
+ selected = tag;
+ else
+ selected = null;
+
+ if (selected != null && DataContext is ViewModels.Repository repo)
+ {
+ var menu = repo.CreateContextMenuForTag(selected);
+ control.OpenContextMenu(menu);
+ }
+
+ e.Handled = true;
+ }
+
+ private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs _)
+ {
+ var selected = (sender as ListBox)?.SelectedItem;
+ var selectedTag = null as Models.Tag;
+ if (selected is ViewModels.TagTreeNode node)
+ selectedTag = node.Tag;
+ else if (selected is Models.Tag tag)
+ selectedTag = tag;
+
+ if (selectedTag != null && DataContext is ViewModels.Repository repo)
+ {
+ RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
+ repo.NavigateToCommit(selectedTag.SHA);
+ }
+ }
+
+ private void OnToggleFilter(object sender, RoutedEventArgs e)
+ {
+ if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo)
+ {
+ var target = null as Models.Tag;
+ if (toggle.DataContext is ViewModels.TagTreeNode node)
+ target = node.Tag;
+ else if (toggle.DataContext is Models.Tag tag)
+ target = tag;
+
+ if (target != null)
+ repo.UpdateFilter(target.Name, toggle.IsChecked == true);
+ }
+
+ e.Handled = true;
+ }
+
+ private void MakeTreeRows(List rows, List nodes)
+ {
+ foreach (var node in nodes)
+ {
+ rows.Add(node);
+
+ if (!node.IsExpanded || !node.IsFolder)
+ continue;
+
+ MakeTreeRows(rows, node.Children);
+ }
+ }
+
+ private void UpdateDataSource()
+ {
+ var tags = Tags;
+ if (tags == null || tags.Count == 0)
+ {
+ Content = null;
+ return;
+ }
+
+ if (ShowTagsAsTree)
+ {
+ var oldExpanded = new HashSet();
+ if (Content is ViewModels.TagCollectionAsTree oldTree)
+ {
+ foreach (var row in oldTree.Rows)
+ {
+ if (row.IsFolder && row.IsExpanded)
+ oldExpanded.Add(row.FullPath);
+ }
+ }
+
+ var tree = new ViewModels.TagCollectionAsTree();
+ tree.Tree = ViewModels.TagTreeNode.Build(tags, oldExpanded);
+
+ var rows = new List();
+ MakeTreeRows(rows, tree.Tree);
+ tree.Rows.AddRange(rows);
+
+ Content = tree;
+ Rows = rows.Count;
+ }
+ else
+ {
+ var list = new ViewModels.TagCollectionAsList();
+ list.Tags.AddRange(tags);
+
+ Content = list;
+ Rows = tags.Count;
+ }
+
+ RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
+ }
+ }
+}
+