using System; using System.Collections.Generic; using System.Globalization; 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)); public ViewModels.BranchTreeNode 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 BranchTreeNodeIcon() { 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.Backend is Models.Remote) { CreateContent(new Thickness(0, 2, 0, 0), "Icons.Remote"); } else if (node.Backend is Models.Branch branch) { if (branch.IsCurrent) CreateContent(new Thickness(0, 2, 0, 0), "Icons.Check"); else CreateContent(new Thickness(2, 0, 0, 0), "Icons.Branch"); } 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 Path() { Width = 12, Height = 12, HorizontalAlignment = HorizontalAlignment.Left, VerticalAlignment = VerticalAlignment.Center, Margin = margin, Data = geo, }; } } public class BranchTreeNodeToggleButton : ToggleButton { protected override Type StyleKeyOverride => typeof(ToggleButton); protected override void OnPointerPressed(PointerPressedEventArgs e) { if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed && DataContext is ViewModels.BranchTreeNode { IsBranch: false } node) { var tree = this.FindAncestorOfType(); tree?.ToggleNodeIsExpanded(node); } e.Handled = true; } } public class BranchTreeNodeTrackStatusPresenter : Control { public static readonly StyledProperty FontFamilyProperty = TextBlock.FontFamilyProperty.AddOwner(); public FontFamily FontFamily { get => GetValue(FontFamilyProperty); set => SetValue(FontFamilyProperty, value); } public static readonly StyledProperty FontSizeProperty = TextBlock.FontSizeProperty.AddOwner(); public double FontSize { get => GetValue(FontSizeProperty); set => SetValue(FontSizeProperty, value); } public static readonly StyledProperty ForegroundProperty = AvaloniaProperty.Register(nameof(Foreground), Brushes.White); public IBrush Foreground { get => GetValue(ForegroundProperty); set => SetValue(ForegroundProperty, value); } public static readonly StyledProperty BackgroundProperty = AvaloniaProperty.Register(nameof(Background), Brushes.White); public IBrush Background { get => GetValue(BackgroundProperty); set => SetValue(BackgroundProperty, value); } static BranchTreeNodeTrackStatusPresenter() { AffectsMeasure( FontSizeProperty, FontFamilyProperty, ForegroundProperty); AffectsRender( ForegroundProperty, BackgroundProperty); } public override void Render(DrawingContext context) { base.Render(context); if (_label != null) { context.DrawRectangle(Background, null, new RoundedRect(new Rect(8, 0, _label.Width + 18, 18), new CornerRadius(9))); context.DrawText(_label, new Point(17, 9 - _label.Height * 0.5)); } } protected override void OnDataContextChanged(EventArgs e) { base.OnDataContextChanged(e); InvalidateMeasure(); InvalidateVisual(); } protected override Size MeasureOverride(Size availableSize) { _label = null; if (DataContext is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) { var status = branch.TrackStatus.ToString(); if (!string.IsNullOrEmpty(status)) { _label = new FormattedText( status, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontFamily), FontSize, Foreground); } } return _label != null ? new Size(_label.Width + 18 /* Padding */ + 16 /* Margin */, 18) : new Size(0, 0); } private FormattedText _label = null; } public partial class BranchTree : UserControl { public static readonly StyledProperty> NodesProperty = AvaloniaProperty.Register>(nameof(Nodes)); 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 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 BranchTree() { InitializeComponent(); } public void UnselectAll() { BranchesPresenter.SelectedItem = null; } public void ToggleNodeIsExpanded(ViewModels.BranchTreeNode node) { _disableSelectionChangingEvent = true; 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; row.IsSelected = false; removeCount++; } rows.RemoveRange(idx + 1, removeCount); } RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); _disableSelectionChangingEvent = false; } protected override void OnSizeChanged(SizeChangedEventArgs e) { base.OnSizeChanged(e); if (Bounds.Height >= 23.0) BranchesPresenter.Height = Bounds.Height; } 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); } RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } else if (change.Property == IsVisibleProperty) { RaiseEvent(new RoutedEventArgs(RowsChangedEvent)); } } private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e) { if (_disableSelectionChangingEvent) return; var repo = DataContext as ViewModels.Repository; if (repo?.Settings == null) return; foreach (var item in e.AddedItems) { if (item is ViewModels.BranchTreeNode node) node.IsSelected = true; } foreach (var item in e.RemovedItems) { if (item is ViewModels.BranchTreeNode node) node.IsSelected = false; } var selected = BranchesPresenter.SelectedItems; if (selected == null || selected.Count == 0) return; if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch }) repo.NavigateToCommit(branch.Head); var prev = null as ViewModels.BranchTreeNode; foreach (var row in Rows) { if (row.IsSelected) { if (prev is { IsSelected: true }) { var prevTop = prev.CornerRadius.TopLeft; prev.CornerRadius = new CornerRadius(prevTop, 0); row.CornerRadius = new CornerRadius(0, 4); } else { row.CornerRadius = new CornerRadius(4); } } prev = row; } RaiseEvent(new RoutedEventArgs(SelectionChangedEvent)); } private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2) { 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 _) { 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 { ToggleNodeIsExpanded(node); } } } 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; node.IsSelected = false; 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); } private bool _disableSelectionChangingEvent = false; } }