refactor: use custom BranchTree instead of TreeView to improve performance

This commit is contained in:
leo 2024-07-06 17:17:41 +08:00
parent 1c524cf310
commit 3197b4bfe8
No known key found for this signature in database
8 changed files with 703 additions and 643 deletions

View file

@ -1,4 +1,5 @@
using Avalonia.Data.Converters; using Avalonia;
using Avalonia.Data.Converters;
namespace SourceGit.Converters namespace SourceGit.Converters
{ {
@ -24,5 +25,8 @@ namespace SourceGit.Converters
public static readonly FuncValueConverter<int, bool> IsSubjectLengthGood = public static readonly FuncValueConverter<int, bool> IsSubjectLengthGood =
new FuncValueConverter<int, bool>(v => v <= ViewModels.Preference.Instance.SubjectGuideLength); new FuncValueConverter<int, bool>(v => v <= ViewModels.Preference.Instance.SubjectGuideLength);
public static readonly FuncValueConverter<int, Thickness> ToTreeMargin =
new FuncValueConverter<int, Thickness>(v => new Thickness(v * 16, 0, 0, 0));
} }
} }

View file

@ -1,72 +1,22 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using Avalonia; using Avalonia;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Media;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.ViewModels namespace SourceGit.ViewModels
{ {
public enum BranchTreeNodeType
{
DetachedHead,
Remote,
Folder,
Branch,
}
public class BranchTreeNode : ObservableObject public class BranchTreeNode : ObservableObject
{ {
public const double DEFAULT_CORNER = 4.0; public string Name { get; private set; } = string.Empty;
public object Backend { get; private set; } = null;
public string Name { get; set; } public int Depth { get; set; } = 0;
public BranchTreeNodeType Type { get; set; } public bool IsFiltered { get; set; } = false;
public object Backend { get; set; } public List<BranchTreeNode> Children { get; private set; } = new List<BranchTreeNode>();
public bool IsFiltered { get; set; }
public List<BranchTreeNode> Children { get; set; } = new List<BranchTreeNode>();
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 bool IsExpanded public bool IsExpanded
{ {
@ -74,49 +24,39 @@ namespace SourceGit.ViewModels
set => SetProperty(ref _isExpanded, value); set => SetProperty(ref _isExpanded, value);
} }
public string Tooltip
{
get
{
if (Backend is Models.Branch b)
return b.FriendlyName;
return null;
}
}
public CornerRadius CornerRadius public CornerRadius CornerRadius
{ {
get => _cornerRadius; get => _cornerRadius;
set => SetProperty(ref _cornerRadius, value); set => SetProperty(ref _cornerRadius, value);
} }
public void UpdateCornerRadius(ref BranchTreeNode prev) public bool IsBranch
{ {
if (_isSelected && prev != null && prev.IsSelected) get => Backend is Models.Branch;
{
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; public bool IsUpstreamTrackStatusVisible
if (!IsBranch && IsExpanded)
{ {
foreach (var child in Children) get => Backend is Models.Branch { IsLocal: true } branch && !string.IsNullOrEmpty(branch.UpstreamTrackStatus);
child.UpdateCornerRadius(ref prev); }
}
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 _isSelected = false;
private bool _isExpanded = false; private bool _isExpanded = false;
private CornerRadius _cornerRadius = new CornerRadius(DEFAULT_CORNER); private CornerRadius _cornerRadius = new CornerRadius(4);
public class Builder public class Builder
{ {
@ -133,7 +73,6 @@ namespace SourceGit.ViewModels
var node = new BranchTreeNode() var node = new BranchTreeNode()
{ {
Name = remote.Name, Name = remote.Name,
Type = BranchTreeNodeType.Remote,
Backend = remote, Backend = remote,
IsExpanded = bForceExpanded || _expanded.Contains(path), IsExpanded = bForceExpanded || _expanded.Contains(path),
}; };
@ -176,9 +115,13 @@ namespace SourceGit.ViewModels
{ {
foreach (var node in nodes) foreach (var node in nodes)
{ {
if (node.Backend is Models.Branch)
continue;
var path = prefix + "/" + node.Name; var path = prefix + "/" + node.Name;
if (node.Type != BranchTreeNodeType.Branch && node.IsExpanded) if (node.IsExpanded)
_expanded.Add(path); _expanded.Add(path);
CollectExpandedNodes(node.Children, path); CollectExpandedNodes(node.Children, path);
} }
} }
@ -191,7 +134,6 @@ namespace SourceGit.ViewModels
roots.Add(new BranchTreeNode() roots.Add(new BranchTreeNode()
{ {
Name = branch.Name, Name = branch.Name,
Type = BranchTreeNodeType.Branch,
Backend = branch, Backend = branch,
IsExpanded = false, IsExpanded = false,
IsFiltered = isFiltered, IsFiltered = isFiltered,
@ -215,7 +157,6 @@ namespace SourceGit.ViewModels
lastFolder = new BranchTreeNode() lastFolder = new BranchTreeNode()
{ {
Name = name, Name = name,
Type = BranchTreeNodeType.Folder,
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
}; };
roots.Add(lastFolder); roots.Add(lastFolder);
@ -226,7 +167,6 @@ namespace SourceGit.ViewModels
var cur = new BranchTreeNode() var cur = new BranchTreeNode()
{ {
Name = name, Name = name,
Type = BranchTreeNodeType.Folder,
IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder), IsExpanded = bForceExpanded || branch.IsCurrent || _expanded.Contains(folder),
}; };
lastFolder.Children.Add(cur); lastFolder.Children.Add(cur);
@ -238,10 +178,9 @@ namespace SourceGit.ViewModels
sepIdx = branch.Name.IndexOf('/', start); sepIdx = branch.Name.IndexOf('/', start);
} }
lastFolder.Children.Add(new BranchTreeNode() lastFolder?.Children.Add(new BranchTreeNode()
{ {
Name = Path.GetFileName(branch.Name), Name = Path.GetFileName(branch.Name),
Type = branch.IsHead ? BranchTreeNodeType.DetachedHead : BranchTreeNodeType.Branch,
Backend = branch, Backend = branch,
IsExpanded = false, IsExpanded = false,
IsFiltered = isFiltered, IsFiltered = isFiltered,
@ -252,16 +191,13 @@ namespace SourceGit.ViewModels
{ {
nodes.Sort((l, r) => nodes.Sort((l, r) =>
{ {
if (l.Type == BranchTreeNodeType.DetachedHead) if (l.Backend is Models.Branch { IsHead: true })
{
return -1; 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) foreach (var node in nodes)

View file

@ -1905,7 +1905,7 @@ namespace SourceGit.ViewModels
visibles.Add(b); visibles.Add(b);
} }
builder.Run(visibles, remotes, visibles.Count <= 20); builder.Run(visibles, remotes, true);
} }
return builder; return builder;

110
src/Views/BranchTree.axaml Normal file
View file

@ -0,0 +1,110 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:c="using:SourceGit.Converters"
xmlns:v="using:SourceGit.Views"
xmlns:vm="using:SourceGit.ViewModels"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.BranchTree"
x:Name="ThisControl">
<DataGrid x:Name="BranchesPresenter"
ItemsSource="{Binding #ThisControl.Rows}"
Height="{Binding #ThisControl.Height}"
Background="Transparent"
RowHeight="24"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
HorizontalAlignment="Stretch"
HorizontalScrollBarVisibility="Disabled"
VerticalScrollBarVisibility="Auto"
HeadersVisibility="None"
SelectionChanged="OnNodesSelectionChanged"
ContextRequested="OnTreeContextRequested">
<DataGrid.Styles>
<Style Selector="DataGridRow" x:DataType="vm:BranchTreeNode">
<Setter Property="CornerRadius" Value="{Binding CornerRadius}" />
</Style>
<Style Selector="DataGridRow /template/ Border#RowBorder">
<Setter Property="ClipToBounds" Value="True" />
</Style>
<Style Selector="Grid.repository_leftpanel DataGridRow:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".5"/>
</Style>
<Style Selector="Grid.repository_leftpanel DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within DataGridRow:selected:pointerover /template/ Rectangle#BackgroundRectangle">
<Setter Property="Fill" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</DataGrid.Styles>
<DataGrid.Columns>
<DataGridTemplateColumn Width="*">
<DataGridTemplateColumn.CellTemplate>
<DataTemplate x:DataType="vm:BranchTreeNode">
<Grid Height="24"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
ColumnDefinitions="16,20,*,Auto,Auto"
Background="Transparent"
DoubleTapped="OnDoubleTappedBranchNode"
ToolTip.Tip="{Binding Tooltip}">
<!-- Tree Expander -->
<ToggleButton Grid.Column="0"
Classes="tree_expander"
Focusable="False"
HorizontalAlignment="Center"
IsChecked="{Binding IsExpanded}"
IsHitTestVisible="False"
IsVisible="{Binding !IsBranch}"/>
<!-- Icon -->
<v:BranchTreeNodeIcon Grid.Column="1"
Node="{Binding}"
IsExpanded="{Binding IsExpanded}"/>
<!-- Name -->
<TextBlock Grid.Column="2"
Text="{Binding Name}"
Classes="monospace"
FontWeight="{Binding NameFontWeight}"/>
<!-- Tracking status -->
<Border Grid.Column="3"
Margin="8,0"
Height="18"
CornerRadius="9"
VerticalAlignment="Center"
Background="{DynamicResource Brush.Badge}"
IsVisible="{Binding IsUpstreamTrackStatusVisible}">
<TextBlock Classes="monospace" FontSize="10" HorizontalAlignment="Center" Margin="9,0" Text="{Binding UpstreamTrackStatus}" Foreground="{DynamicResource Brush.BadgeFG}"/>
</Border>
<!-- Filter Toggle Button -->
<ToggleButton Grid.Column="4"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
IsCheckedChanged="OnToggleFilter"
IsVisible="{Binding IsBranch}"
IsChecked="{Binding IsFiltered}"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
</Grid>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
</DataGridTemplateColumn>
</DataGrid.Columns>
</DataGrid>
</UserControl>

View file

@ -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<ViewModels.BranchTreeNode> NodeProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, ViewModels.BranchTreeNode>(nameof(Node), null);
public ViewModels.BranchTreeNode Node
{
get => GetValue(NodeProperty);
set => SetValue(NodeProperty, value);
}
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, bool>(nameof(IsExpanded), false);
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
static BranchTreeNodeIcon()
{
NodeProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((icon, e) => icon.UpdateContent());
IsExpandedProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((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<List<ViewModels.BranchTreeNode>> NodesProperty =
AvaloniaProperty.Register<BranchTree, List<ViewModels.BranchTreeNode>>(nameof(Nodes), null);
public List<ViewModels.BranchTreeNode> Nodes
{
get => GetValue(NodesProperty);
set => SetValue(NodesProperty, value);
}
public AvaloniaList<ViewModels.BranchTreeNode> Rows
{
get;
private set;
} = new AvaloniaList<ViewModels.BranchTreeNode>();
public static readonly RoutedEvent<RoutedEventArgs> SelectionChangedEvent =
RoutedEvent.Register<BranchTree, RoutedEventArgs>(nameof(SelectionChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> 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<ViewModels.BranchTreeNode>();
MakeRows(rows, Nodes, 0);
Rows.AddRange(rows);
}
var repo = this.FindAncestorOfType<Repository>();
repo?.UpdateLeftSidebarLayout();
}
else if (change.Property == IsVisibleProperty)
{
var repo = this.FindAncestorOfType<Repository>();
repo?.UpdateLeftSidebarLayout();
}
}
private void OnNodesSelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selected = BranchesPresenter.SelectedItems;
if (selected == null || selected.Count == 0)
return;
var set = new HashSet<ViewModels.BranchTreeNode>();
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<Models.Branch>();
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<ViewModels.BranchTreeNode>();
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<Repository>();
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<ViewModels.BranchTreeNode> rows, List<ViewModels.BranchTreeNode> 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<Models.Branch> 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);
}
}
}

View file

@ -2,10 +2,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels" xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.DeleteBranch" x:Class="SourceGit.Views.DeleteBranch"
x:DataType="vm:DeleteBranch"> x:DataType="vm:DeleteBranch">
@ -18,7 +15,7 @@
<TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Right" Text="{DynamicResource Text.DeleteBranch.Branch}"/> <TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Right" Text="{DynamicResource Text.DeleteBranch.Branch}"/>
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal"> <StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
<Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/> <Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding Target.FriendlyName}}"/> <TextBlock Text="{Binding Target.FriendlyName}"/>
</StackPanel> </StackPanel>
<Border Grid.Row="1" Grid.Column="1" Height="32" IsVisible="{Binding !Target.IsLocal}"> <Border Grid.Row="1" Grid.Column="1" Height="32" IsVisible="{Binding !Target.IsLocal}">

View file

@ -236,77 +236,12 @@
<ToggleButton Grid.Row="0" Classes="group_expander" IsChecked="{Binding IsLocalBranchGroupExpanded, Mode=TwoWay}"> <ToggleButton Grid.Row="0" Classes="group_expander" IsChecked="{Binding IsLocalBranchGroupExpanded, Mode=TwoWay}">
<TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.LocalBranches}"/> <TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.LocalBranches}"/>
</ToggleButton> </ToggleButton>
<TreeView Grid.Row="1" <v:BranchTree Grid.Row="1"
x:Name="localBranchTree" x:Name="localBranchTree"
Margin="8,0,4,0" Margin="8,0,4,0"
SelectionMode="Multiple" Nodes="{Binding LocalBranchTrees}"
ItemsSource="{Binding LocalBranchTrees}"
IsVisible="{Binding IsLocalBranchGroupExpanded}" IsVisible="{Binding IsLocalBranchGroupExpanded}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionChanged="OnLocalBranchTreeSelectionChanged"/>
ScrollViewer.VerticalScrollBarVisibility="Auto"
ContextRequested="OnLocalBranchContextMenuRequested"
SelectionChanged="OnLocalBranchTreeSelectionChanged"
PropertyChanged="OnLeftSidebarTreeViewPropertyChanged">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}" x:DataType="{x:Type vm:BranchTreeNode}">
<Grid Height="24" ColumnDefinitions="20,*,Auto,Auto" Background="Transparent" DoubleTapped="OnDoubleTappedBranchNode" ToolTip.Tip="{Binding Tooltip}">
<Path Grid.Column="0" Classes="folder_icon" Width="12" Height="12" HorizontalAlignment="Left" Margin="0,1,0,0" IsVisible="{Binding IsFolder}"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="0,2,0,0" Data="{StaticResource Icons.Check}" IsVisible="{Binding IsCurrent}" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="2,0,0,0" Data="{StaticResource Icons.Branch}" VerticalAlignment="Center">
<Path.IsVisible>
<MultiBinding Converter="{x:Static BoolConverters.And}">
<Binding Path="!IsFolder"/>
<Binding Path="!IsCurrent"/>
</MultiBinding>
</Path.IsVisible>
</Path>
<TextBlock Grid.Column="1"
Text="{Binding Name}"
Classes="monospace"
FontWeight="{Binding IsCurrent, Converter={x:Static c:BoolConverters.BoldIfTrue}}"/>
<Border Grid.Column="2" Margin="8,0" Height="18" CornerRadius="9" VerticalAlignment="Center" Background="{DynamicResource Brush.Badge}" IsVisible="{Binding IsUpstreamTrackStatusVisible}">
<TextBlock Classes="monospace" FontSize="10" HorizontalAlignment="Center" Margin="9,0" Text="{Binding UpstreamTrackStatus}" Foreground="{DynamicResource Brush.BadgeFG}"/>
</Border>
<ToggleButton Grid.Column="3"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
IsVisible="{Binding IsBranch}"
Checked="OnToggleFilter"
Unchecked="OnToggleFilter"
IsChecked="{Binding IsFiltered}"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
</Grid>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Remotes --> <!-- Remotes -->
<ToggleButton Grid.Row="2" Classes="group_expander" IsChecked="{Binding IsRemoteGroupExpanded, Mode=TwoWay}"> <ToggleButton Grid.Row="2" Classes="group_expander" IsChecked="{Binding IsRemoteGroupExpanded, Mode=TwoWay}">
@ -317,64 +252,12 @@
</Button> </Button>
</Grid> </Grid>
</ToggleButton> </ToggleButton>
<TreeView Grid.Row="3" <v:BranchTree Grid.Row="3"
x:Name="remoteBranchTree" x:Name="remoteBranchTree"
Margin="8,0,4,0" Margin="8,0,4,0"
SelectionMode="Multiple" Nodes="{Binding RemoteBranchTrees}"
ItemsSource="{Binding RemoteBranchTrees}"
IsVisible="{Binding IsRemoteGroupExpanded}" IsVisible="{Binding IsRemoteGroupExpanded}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionChanged="OnRemoteBranchTreeSelectionChanged"/>
ScrollViewer.VerticalScrollBarVisibility="Auto"
ContextRequested="OnRemoteBranchContextMenuRequested"
SelectionChanged="OnRemoteBranchTreeSelectionChanged"
PropertyChanged="OnLeftSidebarTreeViewPropertyChanged">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:BranchTreeNode">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
<Setter Property="IsSelected" Value="{Binding IsSelected, Mode=TwoWay}"/>
<Setter Property="CornerRadius" Value="{Binding CornerRadius}"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.AccentHovered}" />
<Setter Property="Opacity" Value="1"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".65"/>
</Style>
<Style Selector="Grid.repository_leftpanel:focus-within TreeViewItem:selected /template/ Border#PART_LayoutRoot:pointerover Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource Brush.Accent}" />
<Setter Property="Opacity" Value=".8"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate ItemsSource="{Binding Children}" x:DataType="{x:Type vm:BranchTreeNode}">
<Grid Height="24" ColumnDefinitions="20,*,Auto" Background="Transparent" DoubleTapped="OnDoubleTappedBranchNode" ToolTip.Tip="{Binding Tooltip}">
<Path Grid.Column="0" Classes="folder_icon" Width="10" Height="10" HorizontalAlignment="Left" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="0,2,0,0" Data="{StaticResource Icons.Remote}" IsVisible="{Binding IsRemote}" VerticalAlignment="Center"/>
<Path Grid.Column="0" Width="12" Height="12" HorizontalAlignment="Left" Margin="2,0,0,0" Data="{StaticResource Icons.Branch}" IsVisible="{Binding IsBranch}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Text="{Binding Name}" Classes="monospace"/>
<ToggleButton Grid.Column="2"
Classes="filter"
Margin="0,0,8,0"
Background="Transparent"
Checked="OnToggleFilter"
Unchecked="OnToggleFilter"
IsVisible="{Binding IsBranch}"
IsChecked="{Binding IsFiltered}"
ToolTip.Tip="{DynamicResource Text.Filter}"/>
</Grid>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Tags --> <!-- Tags -->
<ToggleButton Grid.Row="4" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}"> <ToggleButton Grid.Row="4" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}">
@ -455,8 +338,8 @@
<ToggleButton Classes="filter" <ToggleButton Classes="filter"
Margin="0,0,8,0" Margin="0,0,8,0"
Background="Transparent" Background="Transparent"
Checked="OnToggleFilter" Checked="OnToggleTagFilter"
Unchecked="OnToggleFilter" Unchecked="OnToggleTagFilter"
IsChecked="{Binding IsFiltered}" IsChecked="{Binding IsFiltered}"
ToolTip.Tip="{DynamicResource Text.Filter}"/> ToolTip.Tip="{DynamicResource Text.Filter}"/>
</DataTemplate> </DataTemplate>

View file

@ -1,13 +1,10 @@
using System; using System;
using System.Collections.Generic;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree;
using AvaloniaEdit.Utils;
namespace SourceGit.Views namespace SourceGit.Views
{ {
@ -18,393 +15,7 @@ namespace SourceGit.Views
InitializeComponent(); InitializeComponent();
} }
protected override void OnLoaded(RoutedEventArgs e) public void UpdateLeftSidebarLayout()
{
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<Models.Branch>();
foreach (var item in tree.SelectedItems)
CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode);
if (branches.Count == 1)
{
var item = (e.Source as Control)?.FindAncestorOfType<TreeViewItem>(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<TreeViewItem>(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<Models.Branch>();
foreach (var item in tree.SelectedItems)
CollectBranchesFromNode(branches, item as ViewModels.BranchTreeNode);
if (branches.Count == 1)
{
var item = (e.Source as Control)?.FindAncestorOfType<TreeViewItem>(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<Models.Branch> 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()
{ {
var vm = DataContext as ViewModels.Repository; var vm = DataContext as ViewModels.Repository;
if (vm == null || vm.Settings == null) if (vm == null || vm.Settings == null)
@ -414,8 +25,8 @@ namespace SourceGit.Views
return; return;
var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5; var leftHeight = leftSidebarGroups.Bounds.Height - 28.0 * 5;
var localBranchRows = vm.IsLocalBranchGroupExpanded ? GetTreeRowsCount(vm.LocalBranchTrees) : 0; var localBranchRows = vm.IsLocalBranchGroupExpanded ? localBranchTree.Rows.Count : 0;
var remoteBranchRows = vm.IsRemoteGroupExpanded ? GetTreeRowsCount(vm.RemoteBranchTrees) : 0; var remoteBranchRows = vm.IsRemoteGroupExpanded ? remoteBranchTree.Rows.Count : 0;
var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0; var desiredBranches = (localBranchRows + remoteBranchRows) * 24.0;
var desiredTag = vm.IsTagGroupExpanded ? tagsList.RowHeight * vm.VisibleTags.Count : 0; var desiredTag = vm.IsTagGroupExpanded ? tagsList.RowHeight * vm.VisibleTags.Count : 0;
var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0; var desiredSubmodule = vm.IsSubmoduleGroupExpanded ? submoduleList.RowHeight * vm.Submodules.Count : 0;
@ -524,17 +135,212 @@ namespace SourceGit.Views
} }
} }
private int GetTreeRowsCount(List<ViewModels.BranchTreeNode> 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();
if (!node.IsBranch && node.IsExpanded)
count += GetTreeRowsCount(node.Children);
} }
return count; 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 && 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;
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();
}
} }
} }
} }