diff --git a/src/Converters/IntConverters.cs b/src/Converters/IntConverters.cs index 64f9b357..137f6c9b 100644 --- a/src/Converters/IntConverters.cs +++ b/src/Converters/IntConverters.cs @@ -1,4 +1,5 @@ -using Avalonia.Data.Converters; +using Avalonia; +using Avalonia.Data.Converters; namespace SourceGit.Converters { @@ -24,5 +25,8 @@ namespace SourceGit.Converters public static readonly FuncValueConverter IsSubjectLengthGood = new FuncValueConverter(v => v <= ViewModels.Preference.Instance.SubjectGuideLength); + + public static readonly FuncValueConverter ToTreeMargin = + new FuncValueConverter(v => new Thickness(v * 16, 0, 0, 0)); } } diff --git a/src/ViewModels/BranchTreeNode.cs b/src/ViewModels/BranchTreeNode.cs index f4044476..63848b9f 100644 --- a/src/ViewModels/BranchTreeNode.cs +++ b/src/ViewModels/BranchTreeNode.cs @@ -1,122 +1,62 @@ -using System; +using System; using System.Collections.Generic; using System.IO; using Avalonia; using Avalonia.Collections; +using Avalonia.Media; using CommunityToolkit.Mvvm.ComponentModel; namespace SourceGit.ViewModels { - public enum BranchTreeNodeType - { - DetachedHead, - Remote, - Folder, - Branch, - } - public class BranchTreeNode : ObservableObject { - public const double DEFAULT_CORNER = 4.0; - - public string Name { get; set; } - public BranchTreeNodeType Type { get; set; } - public object Backend { get; set; } - public bool IsFiltered { get; set; } - public List Children { get; set; } = new List(); - - public bool IsUpstreamTrackStatusVisible - { - get => IsBranch && !string.IsNullOrEmpty((Backend as Models.Branch).UpstreamTrackStatus); - } - - public string UpstreamTrackStatus - { - get => Type == BranchTreeNodeType.Branch ? (Backend as Models.Branch).UpstreamTrackStatus : ""; - } - - public bool IsRemote - { - get => Type == BranchTreeNodeType.Remote; - } - - public bool IsFolder - { - get => Type == BranchTreeNodeType.Folder; - } - - public bool IsBranch - { - get => Type == BranchTreeNodeType.Branch; - } - - public bool IsDetachedHead - { - get => Type == BranchTreeNodeType.DetachedHead; - } - - public bool IsCurrent - { - get => IsBranch && (Backend as Models.Branch).IsCurrent; - } - - public bool IsSelected - { - get => _isSelected; - set => SetProperty(ref _isSelected, value); - } - + public string Name { get; private set; } = string.Empty; + public object Backend { get; private set; } = null; + public int Depth { get; set; } = 0; + public bool IsFiltered { get; set; } = false; + public List Children { get; private set; } = new List(); + public bool IsExpanded { get => _isExpanded; set => SetProperty(ref _isExpanded, value); } - - public string Tooltip - { - get - { - if (Backend is Models.Branch b) - return b.FriendlyName; - - return null; - } - } - + public CornerRadius CornerRadius { get => _cornerRadius; set => SetProperty(ref _cornerRadius, value); } - - public void UpdateCornerRadius(ref BranchTreeNode prev) + + public bool IsBranch { - if (_isSelected && prev != null && prev.IsSelected) - { - var prevTop = prev.CornerRadius.TopLeft; - prev.CornerRadius = new CornerRadius(prevTop, 0); - CornerRadius = new CornerRadius(0, DEFAULT_CORNER); - } - else if (CornerRadius.TopLeft != DEFAULT_CORNER || - CornerRadius.BottomLeft != DEFAULT_CORNER) - { - CornerRadius = new CornerRadius(DEFAULT_CORNER); - } - - prev = this; - - if (!IsBranch && IsExpanded) - { - foreach (var child in Children) - child.UpdateCornerRadius(ref prev); - } + get => Backend is Models.Branch; } - private bool _isSelected = false; + public bool IsUpstreamTrackStatusVisible + { + get => Backend is Models.Branch { IsLocal: true } branch && !string.IsNullOrEmpty(branch.UpstreamTrackStatus); + } + + public string UpstreamTrackStatus + { + get => Backend is Models.Branch branch ? branch.UpstreamTrackStatus : ""; + } + + public FontWeight NameFontWeight + { + get => Backend is Models.Branch { IsCurrent: true } ? FontWeight.Bold : FontWeight.Regular; + } + + public string Tooltip + { + get => Backend is Models.Branch b ? b.FriendlyName : null; + } + private bool _isExpanded = false; - private CornerRadius _cornerRadius = new CornerRadius(DEFAULT_CORNER); + private CornerRadius _cornerRadius = new CornerRadius(4); public class Builder { @@ -133,7 +73,6 @@ namespace SourceGit.ViewModels var node = new BranchTreeNode() { Name = remote.Name, - Type = BranchTreeNodeType.Remote, Backend = remote, IsExpanded = bForceExpanded || _expanded.Contains(path), }; @@ -176,9 +115,13 @@ namespace SourceGit.ViewModels { foreach (var node in nodes) { + if (node.Backend is Models.Branch) + continue; + var path = prefix + "/" + node.Name; - if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) + if (node.IsExpanded) _expanded.Add(path); + CollectExpandedNodes(node.Children, path); } } @@ -191,7 +134,6 @@ namespace SourceGit.ViewModels roots.Add(new BranchTreeNode() { Name = branch.Name, - Type = BranchTreeNodeType.Branch, Backend = branch, IsExpanded = false, IsFiltered = isFiltered, @@ -215,7 +157,6 @@ namespace SourceGit.ViewModels lastFolder = new BranchTreeNode() { Name = name, - Type = BranchTreeNodeType.Folder, IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), }; roots.Add(lastFolder); @@ -226,7 +167,6 @@ namespace SourceGit.ViewModels var cur = new BranchTreeNode() { Name = name, - Type = BranchTreeNodeType.Folder, IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), }; lastFolder.Children.Add(cur); @@ -238,10 +178,9 @@ namespace SourceGit.ViewModels sepIdx = branch.Name.IndexOf('/', start); } - lastFolder.Children.Add(new BranchTreeNode() + lastFolder?.Children.Add(new BranchTreeNode() { Name = Path.GetFileName(branch.Name), - Type = branch.IsHead ? BranchTreeNodeType.DetachedHead : BranchTreeNodeType.Branch, Backend = branch, IsExpanded = false, IsFiltered = isFiltered, @@ -252,16 +191,13 @@ namespace SourceGit.ViewModels { nodes.Sort((l, r) => { - if (l.Type == BranchTreeNodeType.DetachedHead) - { + if (l.Backend is Models.Branch { IsHead: true }) return -1; - } - if (l.Type == r.Type) - { - return l.Name.CompareTo(r.Name); - } - return (int)l.Type - (int)r.Type; + if (l.Backend is Models.Branch) + return r.Backend is Models.Branch ? string.Compare(l.Name, r.Name, StringComparison.Ordinal) : 1; + + return r.Backend is Models.Branch ? -1 : string.Compare(l.Name, r.Name, StringComparison.Ordinal); }); foreach (var node in nodes) diff --git a/src/ViewModels/Repository.cs b/src/ViewModels/Repository.cs index 55962b73..67250c4b 100644 --- a/src/ViewModels/Repository.cs +++ b/src/ViewModels/Repository.cs @@ -1905,7 +1905,7 @@ namespace SourceGit.ViewModels visibles.Add(b); } - builder.Run(visibles, remotes, visibles.Count <= 20); + builder.Run(visibles, remotes, true); } return builder; diff --git a/src/Views/BranchTree.axaml b/src/Views/BranchTree.axaml new file mode 100644 index 00000000..3ccadbc9 --- /dev/null +++ b/src/Views/BranchTree.axaml @@ -0,0 +1,110 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchTree.axaml.cs b/src/Views/BranchTree.axaml.cs new file mode 100644 index 00000000..deab507c --- /dev/null +++ b/src/Views/BranchTree.axaml.cs @@ -0,0 +1,324 @@ +using System; +using System.Collections.Generic; + +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Shapes; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.Layout; +using Avalonia.Media; +using Avalonia.VisualTree; + +namespace SourceGit.Views +{ + public class BranchTreeNodeIcon : UserControl + { + public static readonly StyledProperty NodeProperty = + AvaloniaProperty.Register(nameof(Node), null); + + public ViewModels.BranchTreeNode Node + { + get => GetValue(NodeProperty); + set => SetValue(NodeProperty, value); + } + + public static readonly StyledProperty IsExpandedProperty = + AvaloniaProperty.Register(nameof(IsExpanded), false); + + public bool IsExpanded + { + get => GetValue(IsExpandedProperty); + set => SetValue(IsExpandedProperty, value); + } + + static BranchTreeNodeIcon() + { + NodeProperty.Changed.AddClassHandler((icon, e) => icon.UpdateContent()); + IsExpandedProperty.Changed.AddClassHandler((icon, e) => icon.UpdateContent()); + } + + private void UpdateContent() + { + var node = Node; + if (node == null) + { + Content = null; + return; + } + + if (node.Backend is Models.Remote) + { + CreateContent(12, new Thickness(0,2,0,0), "Icons.Remote"); + } + else if (node.Backend is Models.Branch branch) + { + if (branch.IsCurrent) + CreateContent(12, new Thickness(0,2,0,0), "Icons.Check"); + else + CreateContent(12, new Thickness(2,0,0,0), "Icons.Branch"); + } + else + { + if (node.IsExpanded) + CreateContent(10, new Thickness(0,2,0,0), "Icons.Folder.Open"); + else + CreateContent(10, new Thickness(0,2,0,0), "Icons.Folder.Fill"); + } + } + + private void CreateContent(double size, Thickness margin, string iconKey) + { + var geo = this.FindResource(iconKey) as StreamGeometry; + if (geo == null) + return; + + Content = new Path() + { + Width = size, + Height = size, + HorizontalAlignment = HorizontalAlignment.Left, + VerticalAlignment = VerticalAlignment.Center, + Margin = margin, + Data = geo, + }; + } + } + + public partial class BranchTree : UserControl + { + public static readonly StyledProperty> NodesProperty = + AvaloniaProperty.Register>(nameof(Nodes), null); + + public List Nodes + { + get => GetValue(NodesProperty); + set => SetValue(NodesProperty, value); + } + + public AvaloniaList Rows + { + get; + private set; + } = new AvaloniaList(); + + 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 BranchTree() + { + InitializeComponent(); + } + + public void UnselectAll() + { + BranchesPresenter.SelectedItem = null; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + if (change.Property == NodesProperty) + { + Rows.Clear(); + + if (Nodes is { Count: > 0 }) + { + var rows = new List(); + MakeRows(rows, Nodes, 0); + Rows.AddRange(rows); + } + + var repo = this.FindAncestorOfType(); + repo?.UpdateLeftSidebarLayout(); + } + else if (change.Property == IsVisibleProperty) + { + var repo = this.FindAncestorOfType(); + repo?.UpdateLeftSidebarLayout(); + } + } + + private void OnNodesSelectionChanged(object sender, SelectionChangedEventArgs e) + { + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + var set = new HashSet(); + foreach (var item in selected) + { + if (item is ViewModels.BranchTreeNode node) + set.Add(node); + } + + var prev = null as ViewModels.BranchTreeNode; + var isPrevSelected = false; + foreach (var row in Rows) + { + var isSelected = set.Contains(row); + if (isSelected) + { + if (isPrevSelected) + { + var prevTop = prev.CornerRadius.TopLeft; + prev.CornerRadius = new CornerRadius(prevTop, 0); + row.CornerRadius = new CornerRadius(0, 4); + } + else + { + row.CornerRadius = new CornerRadius(4); + } + } + + isPrevSelected = isSelected; + prev = row; + } + + RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); + } + + private void OnTreeContextRequested(object sender, ContextRequestedEventArgs e) + { + var repo = DataContext as ViewModels.Repository; + if (repo?.Settings == null) + return; + + var selected = BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote }) + { + var menu = repo.CreateContextMenuForRemote(remote); + this.OpenContextMenu(menu); + return; + } + + var branches = new List(); + foreach (var item in selected) + { + if (item is ViewModels.BranchTreeNode node) + CollectBranchesInNode(branches, node); + } + + if (branches.Count == 1) + { + var branch = branches[0]; + var menu = branch.IsLocal ? + repo.CreateContextMenuForLocalBranch(branch) : + repo.CreateContextMenuForRemoteBranch(branch); + this.OpenContextMenu(menu); + } + else if (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, branches[0].IsLocal); + ev.Handled = true; + }; + menu.Items.Add(deleteMulti); + this.OpenContextMenu(menu); + } + } + + private void OnDoubleTappedBranchNode(object sender, TappedEventArgs e) + { + if (sender is Grid { DataContext: ViewModels.BranchTreeNode node }) + { + if (node.Backend is Models.Branch branch) + { + if (branch.IsCurrent) + return; + + if (DataContext is ViewModels.Repository { Settings: not null } repo) + repo.CheckoutBranch(branch); + } + else + { + node.IsExpanded = !node.IsExpanded; + + var rows = Rows; + var depth = node.Depth; + var idx = rows.IndexOf(node); + if (idx == -1) + return; + + if (node.IsExpanded) + { + var subtree = new List(); + MakeRows(subtree, node.Children, depth + 1); + rows.InsertRange(idx + 1, subtree); + } + 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); + } + + var repo = this.FindAncestorOfType(); + repo?.UpdateLeftSidebarLayout(); + } + } + } + + private void OnToggleFilter(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton toggle && DataContext is ViewModels.Repository repo) + { + if (toggle.DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.UpdateFilter(branch.FullName, toggle.IsChecked == true); + } + + e.Handled = true; + } + + private void MakeRows(List rows, List nodes, int depth) + { + foreach (var node in nodes) + { + node.Depth = depth; + rows.Add(node); + + if (!node.IsExpanded || node.Backend is Models.Branch) + continue; + + MakeRows(rows, node.Children, depth + 1); + } + } + + private void CollectBranchesInNode(List outs, ViewModels.BranchTreeNode node) + { + if (node.Backend is Models.Branch branch && !outs.Contains(branch)) + { + outs.Add(branch); + return; + } + + foreach (var sub in node.Children) + CollectBranchesInNode(outs, sub); + } + } +} + diff --git a/src/Views/DeleteBranch.axaml b/src/Views/DeleteBranch.axaml index 07230de9..b2693bf0 100644 --- a/src/Views/DeleteBranch.axaml +++ b/src/Views/DeleteBranch.axaml @@ -2,10 +2,7 @@ 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:vm="using:SourceGit.ViewModels" - xmlns:v="using:SourceGit.Views" - xmlns:c="using:SourceGit.Converters" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="SourceGit.Views.DeleteBranch" x:DataType="vm:DeleteBranch"> @@ -18,7 +15,7 @@ - + diff --git a/src/Views/Repository.axaml b/src/Views/Repository.axaml index 2db2f166..2ffcc485 100644 --- a/src/Views/Repository.axaml +++ b/src/Views/Repository.axaml @@ -236,77 +236,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -317,64 +252,12 @@ - - - - - - - - - - - - - - - - - - - - - - - - + @@ -455,8 +338,8 @@ diff --git a/src/Views/Repository.axaml.cs b/src/Views/Repository.axaml.cs index 9046a375..f5612d70 100644 --- a/src/Views/Repository.axaml.cs +++ b/src/Views/Repository.axaml.cs @@ -1,13 +1,10 @@ using System; -using System.Collections.Generic; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; -using Avalonia.VisualTree; -using AvaloniaEdit.Utils; namespace SourceGit.Views { @@ -18,393 +15,7 @@ namespace SourceGit.Views InitializeComponent(); } - protected override void OnLoaded(RoutedEventArgs e) - { - base.OnLoaded(e); - - if (DataContext is ViewModels.Repository repo && !repo.IsSearching) - { - UpdateLeftSidebarLayout(); - } - } - - private void OpenWithExternalTools(object sender, RoutedEventArgs e) - { - if (sender is Button button && DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForExternalTools(); - button.OpenContextMenu(menu); - e.Handled = true; - } - } - - private void OpenGitFlowMenu(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForGitFlow(); - (sender as Control)?.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OpenGitLFSMenu(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var menu = repo.CreateContextMenuForGitLFS(); - (sender as Control)?.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private async void OpenStatistics(object sender, RoutedEventArgs e) - { - if (DataContext is ViewModels.Repository repo) - { - var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; - await dialog.ShowDialog(TopLevel.GetTopLevel(this) as Window); - e.Handled = true; - } - } - - private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - var grid = sender as Grid; - if (e.Property == IsVisibleProperty && grid.IsVisible) - txtSearchCommitsBox.Focus(); - } - - private void OnSearchKeyDown(object sender, KeyEventArgs e) - { - if (e.Key == Key.Enter) - { - if (DataContext is ViewModels.Repository repo) - repo.StartSearchCommits(); - - e.Handled = true; - } - } - - private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null) - { - if (DataContext is ViewModels.Repository repo) - { - var commit = datagrid.SelectedItem as Models.Commit; - repo.NavigateToCommit(commit.SHA); - } - } - e.Handled = true; - } - - private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - remoteBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - ViewModels.BranchTreeNode prev = null; - foreach (var node in repo.LocalBranchTrees) - node.UpdateCornerRadius(ref prev); - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch) - repo.NavigateToCommit((node.Backend as Models.Branch).Head); - } - } - } - - private void OnRemoteBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e) - { - if (sender is TreeView tree && tree.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - localBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - ViewModels.BranchTreeNode prev = null; - foreach (var node in repo.RemoteBranchTrees) - node.UpdateCornerRadius(ref prev); - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node.IsBranch) - repo.NavigateToCommit((node.Backend as Models.Branch).Head); - } - } - } - - private void OnLocalBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) - { - remoteBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - var repo = DataContext as ViewModels.Repository; - var tree = sender as TreeView; - if (tree.SelectedItems.Count == 0) - { - e.Handled = true; - return; - } - - 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.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; - } - - private void OnRemoteBranchContextMenuRequested(object sender, ContextRequestedEventArgs e) - { - localBranchTree.UnselectAll(); - tagsList.SelectedItem = null; - - var repo = DataContext as ViewModels.Repository; - var tree = sender as TreeView; - if (tree.SelectedItems.Count == 0) - { - e.Handled = true; - return; - } - - if (tree.SelectedItems.Count == 1) - { - var node = tree.SelectedItem as ViewModels.BranchTreeNode; - if (node != null && node.IsRemote) - { - 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; - } - } - - 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(branches[0]); - item.OpenContextMenu(menu); - } - } - else if (branches.Count > 1) - { - 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; - } - - private void OnDoubleTappedBranchNode(object sender, TappedEventArgs e) - { - if (!ViewModels.PopupHost.CanCreatePopup()) - return; - - if (sender is Grid grid && DataContext is ViewModels.Repository repo) - { - var node = grid.DataContext as ViewModels.BranchTreeNode; - if (node == null) - return; - - if (node.IsBranch) - { - var branch = node.Backend as Models.Branch; - if (branch.IsCurrent) - return; - - repo.CheckoutBranch(branch); - } - else - { - node.IsExpanded = !node.IsExpanded; - UpdateLeftSidebarLayout(); - } - - e.Handled = true; - } - } - - private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) - { - 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); - } - } - - private void OnTagContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var tag = datagrid.SelectedItem as Models.Tag; - var menu = repo.CreateContextMenuForTag(tag); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnToggleFilter(object sender, RoutedEventArgs e) - { - if (sender is ToggleButton toggle) - { - var filter = string.Empty; - 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) - { - filter = tag.Name; - } - - if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) - { - repo.UpdateFilter(filter, toggle.IsChecked == true); - } - } - - e.Handled = true; - } - - private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var submodule = datagrid.SelectedItem as string; - var menu = repo.CreateContextMenuForSubmodule(submodule); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var submodule = datagrid.SelectedItem as string; - (DataContext as ViewModels.Repository).OpenSubmodule(submodule); - } - - e.Handled = true; - } - - private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var worktree = datagrid.SelectedItem as Models.Worktree; - var menu = repo.CreateContextMenuForWorktree(worktree); - datagrid.OpenContextMenu(menu); - } - - e.Handled = true; - } - - private void OnDoubleTappedWorktree(object sender, TappedEventArgs e) - { - if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) - { - var worktree = datagrid.SelectedItem as Models.Worktree; - (DataContext as ViewModels.Repository).OpenWorktree(worktree); - } - - 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); - } - } - - private void OnLeftSidebarTreeViewPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == TreeView.ItemsSourceProperty || e.Property == TreeView.IsVisibleProperty) - { - UpdateLeftSidebarLayout(); - } - } - - private void OnLeftSidebarDataGridPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == DataGrid.ItemsSourceProperty || e.Property == DataGrid.IsVisibleProperty) - { - UpdateLeftSidebarLayout(); - } - } - - private void UpdateLeftSidebarLayout() + public void UpdateLeftSidebarLayout() { var vm = DataContext as ViewModels.Repository; if (vm == null || vm.Settings == null) @@ -414,8 +25,8 @@ namespace SourceGit.Views return; var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5; - var localBranchRows = vm.IsLocalBranchGroupExpanded ? GetTreeRowsCount(vm.LocalBranchTrees) : 0; - var remoteBranchRows = vm.IsRemoteGroupExpanded ? GetTreeRowsCount(vm.RemoteBranchTrees) : 0; + 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 desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0; @@ -524,17 +135,212 @@ namespace SourceGit.Views } } - private int GetTreeRowsCount(List nodes) + protected override void OnLoaded(RoutedEventArgs e) { - int count = nodes.Count; + base.OnLoaded(e); - foreach (var node in nodes) + if (DataContext is ViewModels.Repository { IsSearching: false }) + UpdateLeftSidebarLayout(); + } + + private void OpenWithExternalTools(object sender, RoutedEventArgs e) + { + if (sender is Button button && DataContext is ViewModels.Repository repo) { - if (!node.IsBranch && node.IsExpanded) - count += GetTreeRowsCount(node.Children); + var menu = repo.CreateContextMenuForExternalTools(); + button.OpenContextMenu(menu); + e.Handled = true; } + } + + private void OpenGitFlowMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForGitFlow(); + (sender as Control)?.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OpenGitLFSMenu(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo) + { + var menu = repo.CreateContextMenuForGitLFS(); + (sender as Control)?.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private async void OpenStatistics(object sender, RoutedEventArgs e) + { + if (DataContext is ViewModels.Repository repo && TopLevel.GetTopLevel(this) is Window owner) + { + var dialog = new Statistics() { DataContext = new ViewModels.Statistics(repo.FullPath) }; + await dialog.ShowDialog(owner); + e.Handled = true; + } + } + + private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true} grid) + txtSearchCommitsBox.Focus(); + } + + private void OnSearchKeyDown(object sender, KeyEventArgs e) + { + if (e.Key == Key.Enter) + { + if (DataContext is ViewModels.Repository repo) + repo.StartSearchCommits(); + + e.Handled = true; + } + } + + private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var commit = grid.SelectedItem as Models.Commit; + repo.NavigateToCommit(commit.SHA); + } + + e.Handled = true; + } + + private void OnLocalBranchTreeSelectionChanged(object sender, RoutedEventArgs e) + { + if (sender is BranchTree tree && DataContext is ViewModels.Repository repo) + { + var selected = tree.BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; - return count; + remoteBranchTree.UnselectAll(); + tagsList.SelectedItem = null; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.NavigateToCommit(branch.Head); + } + } + + private void OnRemoteBranchTreeSelectionChanged(object sender, RoutedEventArgs e) + { + if (sender is BranchTree tree && DataContext is ViewModels.Repository repo) + { + var selected = tree.BranchesPresenter.SelectedItems; + if (selected == null || selected.Count == 0) + return; + + localBranchTree.UnselectAll(); + tagsList.SelectedItem = null; + + if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) + repo.NavigateToCommit(branch.Head); + } + } + + private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid) + { + localBranchTree.UnselectAll(); + remoteBranchTree.UnselectAll(); + + var tag = grid.SelectedItem as Models.Tag; + if (DataContext is ViewModels.Repository repo) + repo.NavigateToCommit(tag.SHA); + } + } + + private void OnTagContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + { + var tag = datagrid.SelectedItem as Models.Tag; + var menu = repo.CreateContextMenuForTag(tag); + datagrid.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnToggleTagFilter(object sender, RoutedEventArgs e) + { + if (sender is ToggleButton toggle) + { + var filter = string.Empty; + if (toggle.DataContext is Models.Tag tag) + { + filter = tag.Name; + } + + if (!string.IsNullOrEmpty(filter) && DataContext is ViewModels.Repository repo) + { + repo.UpdateFilter(filter, toggle.IsChecked == true); + } + } + + e.Handled = true; + } + + private void OnSubmoduleContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo) + { + var submodule = datagrid.SelectedItem as string; + var menu = repo.CreateContextMenuForSubmodule(submodule); + datagrid.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var submodule = grid.SelectedItem as string; + repo.OpenSubmodule(submodule); + } + + e.Handled = true; + } + + private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var worktree = grid.SelectedItem as Models.Worktree; + var menu = repo.CreateContextMenuForWorktree(worktree); + grid.OpenContextMenu(menu); + } + + e.Handled = true; + } + + private void OnDoubleTappedWorktree(object sender, TappedEventArgs e) + { + if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo) + { + var worktree = grid.SelectedItem as Models.Worktree; + repo.OpenWorktree(worktree); + } + + e.Handled = true; + } + + private void OnLeftSidebarDataGridPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == DataGrid.ItemsSourceProperty || e.Property == DataGrid.IsVisibleProperty) + { + UpdateLeftSidebarLayout(); + } } } }