diff --git a/src/Converters/BranchTreeNodeConverters.cs b/src/Converters/BranchTreeNodeConverters.cs new file mode 100644 index 00000000..14f73fd7 --- /dev/null +++ b/src/Converters/BranchTreeNodeConverters.cs @@ -0,0 +1,99 @@ +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Controls; +using Avalonia.Data.Converters; + +namespace SourceGit.Converters +{ + public static class BranchTreeNodeConverters + { + public static readonly CornerRadius DEFAULT = new CornerRadius(4); + + public static readonly FuncMultiValueConverter ToCornerRadius = + new FuncMultiValueConverter(v => + { + if (v == null) + return DEFAULT; + + var array = new List(); + array.AddRange(v); + if (array.Count != 2) + return DEFAULT; + + var item = array[1] as TreeViewItem; + if (item == null || !item.IsSelected) + return DEFAULT; + + var prev = GetPrevTreeViewItem(item); + var next = GetNextTreeViewItem(item, true); + + double top = 4, bottom = 4; + if (prev != null && prev.IsSelected) + top = 0; + if (next != null && next.IsSelected) + bottom = 0; + + return new CornerRadius(top, bottom); + }); + + private static TreeViewItem GetPrevTreeViewItem(TreeViewItem item) + { + if (item.Parent is TreeView tree) + { + var idx = tree.IndexFromContainer(item); + if (idx == 0) + return null; + + var prev = tree.ContainerFromIndex(idx - 1) as TreeViewItem; + if (prev != null && prev.IsExpanded && prev.ItemCount > 0) + return prev.ContainerFromIndex(prev.ItemCount - 1) as TreeViewItem; + + return prev; + } + else if (item.Parent is TreeViewItem parentItem) + { + var idx = parentItem.IndexFromContainer(item); + if (idx == 0) + return parentItem; + + var prev = parentItem.ContainerFromIndex(idx - 1) as TreeViewItem; + if (prev != null && prev.IsExpanded && prev.ItemCount > 0) + return prev.ContainerFromIndex(prev.ItemCount - 1) as TreeViewItem; + + return prev; + } + else + { + return null; + } + } + + private static TreeViewItem GetNextTreeViewItem(TreeViewItem item, bool intoSelf = false) + { + if (intoSelf && item.IsExpanded && item.ItemCount > 0) + return item.ContainerFromIndex(0) as TreeViewItem; + + if (item.Parent is TreeView tree) + { + var idx = tree.IndexFromContainer(item); + if (idx == tree.ItemCount - 1) + return null; + + return tree.ContainerFromIndex(idx + 1) as TreeViewItem; + } + else if (item.Parent is TreeViewItem parentItem) + { + var idx = parentItem.IndexFromContainer(item); + if (idx == parentItem.ItemCount - 1) + return GetNextTreeViewItem(parentItem); + + return parentItem.ContainerFromIndex(idx + 1) as TreeViewItem; + } + else + { + return null; + } + } + } +} diff --git a/src/Resources/Locales/en_US.axaml b/src/Resources/Locales/en_US.axaml index cf0e0454..04770f37 100644 --- a/src/Resources/Locales/en_US.axaml +++ b/src/Resources/Locales/en_US.axaml @@ -35,6 +35,7 @@ Checkout${0}$ Copy Branch Name Delete${0}$ + Delete selected {0} branches Discard all changes Fast-Forward to${0}$ Git Flow - Finish${0}$ @@ -128,6 +129,9 @@ Branch : You are about to delete a remote branch!!! Also delete remote branch${0}$ + Delete Multiple Branches + Targets : + You are trying to delete multiple branches at one time. Be sure to double-check before taking action! Delete Remote Remote : Target : diff --git a/src/Resources/Locales/zh_CN.axaml b/src/Resources/Locales/zh_CN.axaml index 17f8c128..5617c1a2 100644 --- a/src/Resources/Locales/zh_CN.axaml +++ b/src/Resources/Locales/zh_CN.axaml @@ -35,6 +35,7 @@ 检出(checkout)${0}$ 复制分支名 删除${0}$ + 删除选中的 {0} 个分支 放弃所有更改 快进(fast-forward)到${0}$ GIT工作流 - 完成${0}$ @@ -128,6 +129,9 @@ 分支名 : 您正在删除远程上的分支,请务必小心!!! 同时删除远程分支${0}$ + 删除多个分支 + 分支列表 : + 您正在尝试一次性删除多个分支,请务必仔细检查后再执行操作! 删除远程确认 远程名 : 目标 : diff --git a/src/Resources/Styles.axaml b/src/Resources/Styles.axaml index 0e66f016..d3cdb55b 100644 --- a/src/Resources/Styles.axaml +++ b/src/Resources/Styles.axaml @@ -486,37 +486,15 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/DeleteMultipleBranches.axaml.cs b/src/Views/DeleteMultipleBranches.axaml.cs new file mode 100644 index 00000000..7b3552d5 --- /dev/null +++ b/src/Views/DeleteMultipleBranches.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls; + +namespace SourceGit.Views +{ + public partial class DeleteMultipleBranches : UserControl + { + public DeleteMultipleBranches() + { + InitializeComponent(); + } + } +} + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 6b62a0b1..765210ae 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -109,7 +109,7 @@ - + @@ -119,14 +119,14 @@ - + - + @@ -142,7 +142,7 @@ - + @@ -198,19 +198,37 @@ + + + + - + @@ -250,20 +268,38 @@ + + + + - + @@ -294,7 +330,9 @@ + + + + + + + + + + @@ -364,6 +419,7 @@ + + + + + + + + + + @@ -390,7 +464,7 @@ - + diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 9bea2cc8..1468517b 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Threading.Tasks; using Avalonia; @@ -6,6 +7,7 @@ using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; +using Avalonia.VisualTree; namespace SourceGit.Views { @@ -56,6 +58,24 @@ namespace SourceGit.Views public partial class Repository : UserControl { + public static readonly StyledProperty RefereshLocalBranchSelectionTokenProperty = + AvaloniaProperty.Register(nameof(RefereshLocalBranchSelectionToken), 0); + + public ulong RefereshLocalBranchSelectionToken + { + get => GetValue(RefereshLocalBranchSelectionTokenProperty); + set => SetValue(RefereshLocalBranchSelectionTokenProperty, value); + } + + public static readonly StyledProperty RefereshRemoteBranchSelectionTokenProperty = + AvaloniaProperty.Register(nameof(RefereshRemoteBranchSelectionToken), 0); + + public ulong RefereshRemoteBranchSelectionToken + { + get => GetValue(RefereshRemoteBranchSelectionTokenProperty); + set => SetValue(RefereshRemoteBranchSelectionTokenProperty, value); + } + public Repository() { InitializeComponent(); @@ -71,34 +91,21 @@ namespace SourceGit.Views } } - private void OnLocalBranchTreeLostFocus(object sender, RoutedEventArgs e) - { - if (sender is TreeView tree) - tree.UnselectAll(); - } - - private void OnRemoteBranchTreeLostFocus(object sender, RoutedEventArgs e) - { - if (sender is TreeView tree) - tree.UnselectAll(); - } - - private void OnTagDataGridLostFocus(object sender, RoutedEventArgs e) - { - if (sender is DataGrid datagrid) - datagrid.SelectedItem = null; - } - private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) { if (sender is TreeView tree && tree.SelectedItem != null) { remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch && DataContext is ViewModels.Repository repo) + var next = RefereshLocalBranchSelectionToken + 1; + SetCurrentValue(RefereshLocalBranchSelectionTokenProperty, next); + + if (tree.SelectedItems.Count == 1) { - repo.NavigateToCommit((node.Backend as Models.Branch).Head); + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node.IsBranch && DataContext is ViewModels.Repository repo) + repo.NavigateToCommit((node.Backend as Models.Branch).Head); } } } @@ -108,11 +115,16 @@ namespace SourceGit.Views if (sender is TreeView tree && tree.SelectedItem != null) { localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch && DataContext is ViewModels.Repository repo) + var next = RefereshRemoteBranchSelectionToken + 1; + SetCurrentValue(RefereshRemoteBranchSelectionTokenProperty, next); + + if (tree.SelectedItems.Count == 1) { - repo.NavigateToCommit((node.Backend as Models.Branch).Head); + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node.IsBranch && DataContext is ViewModels.Repository repo) + repo.NavigateToCommit((node.Backend as Models.Branch).Head); } } } @@ -121,11 +133,12 @@ namespace SourceGit.Views { if (sender is DataGrid datagrid && datagrid.SelectedItem != null) { + localBranchTree.UnselectAll(); + remoteBranchTree.UnselectAll(); + var tag = datagrid.SelectedItem as Models.Tag; if (DataContext is ViewModels.Repository repo) - { repo.NavigateToCommit(tag.SHA); - } } } @@ -133,9 +146,7 @@ namespace SourceGit.Views { var grid = sender as Grid; if (e.Property == IsVisibleProperty && grid.IsVisible) - { txtSearchCommitsBox.Focus(); - } } private void OnSearchKeyDown(object sender, KeyEventArgs e) @@ -143,9 +154,8 @@ namespace SourceGit.Views if (e.Key == Key.Enter) { if (DataContext is ViewModels.Repository repo) - { repo.StartSearchCommits(); - } + e.Handled = true; } } @@ -171,9 +181,7 @@ namespace SourceGit.Views if (toggle.DataContext is ViewModels.BranchTreeNode node) { if (node.IsBranch) - { filter = (node.Backend as Models.Branch).FullName; - } } else if (toggle.DataContext is Models.Tag tag) { @@ -192,15 +200,38 @@ namespace SourceGit.Views private void OnLocalBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) { remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; - if (sender is Grid grid && grid.DataContext is ViewModels.BranchTreeNode node) + var repo = DataContext as ViewModels.Repository; + var tree = sender as TreeView; + + var branches = new List(); + foreach (var item in tree.SelectedItems) + CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); + + if (branches.Count == 1) { - if (node.IsBranch && DataContext is ViewModels.Repository repo) + var item = (e.Source as Control)?.FindAncestorOfType(true); + if (item != null) { - var menu = repo.CreateContextMenuForLocalBranch(node.Backend as Models.Branch); - grid.OpenContextMenu(menu); + var menu = repo.CreateContextMenuForLocalBranch(branches[0]); + item.OpenContextMenu(menu); } } + else if (branches.Count > 1 && branches.Find(x => x.IsCurrent) == null) + { + var menu = new ContextMenu(); + var deleteMulti = new MenuItem(); + deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); + deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteMulti.Click += (_, ev) => + { + repo.DeleteMultipleBranches(branches, true); + ev.Handled = true; + }; + menu.Items.Add(deleteMulti); + tree.OpenContextMenu(menu); + } e.Handled = true; } @@ -208,20 +239,55 @@ namespace SourceGit.Views private void OnRemoteBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) { localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; + + var repo = DataContext as ViewModels.Repository; + var tree = sender as TreeView; - if (sender is Grid grid && grid.DataContext is ViewModels.BranchTreeNode node && DataContext is ViewModels.Repository repo) + if (tree.SelectedItems.Count == 1) { - if (node.IsRemote) + var node = tree.SelectedItem as ViewModels.BranchTreeNode; + if (node != null && node.IsRemote) { - var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); - grid.OpenContextMenu(menu); + var item = (e.Source as Control)?.FindAncestorOfType(true); + if (item != null && item.DataContext == node) + { + var menu = repo.CreateContextMenuForRemote(node.Backend as Models.Remote); + item.OpenContextMenu(menu); + } + + e.Handled = true; + return; } - else if (node.IsBranch) + } + + var branches = new List(); + foreach (var item in tree.SelectedItems) + CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode); + + if (branches.Count == 1) + { + var item = (e.Source as Control)?.FindAncestorOfType(true); + if (item != null) { - var menu = repo.CreateContextMenuForRemoteBranch(node.Backend as Models.Branch); - grid.OpenContextMenu(menu); + var menu = repo.CreateContextMenuForRemoteBranch(branches[0]); + item.OpenContextMenu(menu); } } + else + { + var menu = new ContextMenu(); + var deleteMulti = new MenuItem(); + deleteMulti.Header = App.Text("BranchCM.DeleteMultiBranches", branches.Count); + deleteMulti.Icon = App.CreateMenuIcon("Icons.Clear"); + deleteMulti.Click += (_, ev) => + { + repo.DeleteMultipleBranches(branches, false); + ev.Handled = true; + }; + menu.Items.Add(deleteMulti); + tree.OpenContextMenu(menu); + } e.Handled = true; } @@ -304,5 +370,23 @@ namespace SourceGit.Views e.Handled = true; } } + + private void CollectBranchesFromNode(List outs, ViewModels.BranchTreeNode node) + { + if (node == null || node.IsRemote) + return; + + if (node.IsFolder) + { + foreach (var child in node.Children) + CollectBranchesFromNode(outs, child); + } + else + { + var b = node.Backend as Models.Branch; + if (b != null && !outs.Contains(b)) + outs.Add(b); + } + } } } diff --git a/src/Views/TextDiffView.axaml.cs b/src/Views/TextDiffView.axaml.cs index 1af7bc44..20978345 100644 --- a/src/Views/TextDiffView.axaml.cs +++ b/src/Views/TextDiffView.axaml.cs @@ -137,6 +137,9 @@ namespace SourceGit.Views var width = textView.Bounds.Width; foreach (var line in textView.VisualLines) { + if (line.FirstDocumentLine == null) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > _editor.DiffData.Lines.Count) break; @@ -517,6 +520,9 @@ namespace SourceGit.Views var infos = _editor.IsOld ? _editor.DiffData.Old : _editor.DiffData.New; foreach (var line in textView.VisualLines) { + if (line.FirstDocumentLine == null) + continue; + var index = line.FirstDocumentLine.LineNumber; if (index > infos.Count) break;