mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-12-23 20:47:25 -08:00
refactor: use custom BranchTree instead of TreeView to improve performance
This commit is contained in:
parent
1c524cf310
commit
3197b4bfe8
8 changed files with 703 additions and 643 deletions
|
@ -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<int, bool> IsSubjectLengthGood =
|
||||
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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,72 +1,22 @@
|
|||
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<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 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<BranchTreeNode> Children { get; private set; } = new List<BranchTreeNode>();
|
||||
|
||||
public bool IsExpanded
|
||||
{
|
||||
|
@ -74,49 +24,39 @@ namespace SourceGit.ViewModels
|
|||
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);
|
||||
get => Backend is Models.Branch;
|
||||
}
|
||||
|
||||
prev = this;
|
||||
|
||||
if (!IsBranch && IsExpanded)
|
||||
public bool IsUpstreamTrackStatusVisible
|
||||
{
|
||||
foreach (var child in Children)
|
||||
child.UpdateCornerRadius(ref prev);
|
||||
}
|
||||
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 _isSelected = false;
|
||||
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)
|
||||
|
|
|
@ -1905,7 +1905,7 @@ namespace SourceGit.ViewModels
|
|||
visibles.Add(b);
|
||||
}
|
||||
|
||||
builder.Run(visibles, remotes, visibles.Count <= 20);
|
||||
builder.Run(visibles, remotes, true);
|
||||
}
|
||||
|
||||
return builder;
|
||||
|
|
110
src/Views/BranchTree.axaml
Normal file
110
src/Views/BranchTree.axaml
Normal 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>
|
||||
|
324
src/Views/BranchTree.axaml.cs
Normal file
324
src/Views/BranchTree.axaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 @@
|
|||
<TextBlock Grid.Row="0" Grid.Column="0" HorizontalAlignment="Right" Text="{DynamicResource Text.DeleteBranch.Branch}"/>
|
||||
<StackPanel Grid.Row="0" Grid.Column="1" Orientation="Horizontal">
|
||||
<Path Width="14" Height="14" Margin="8,0" Data="{StaticResource Icons.Branch}"/>
|
||||
<TextBlock Text="{Binding Target.FriendlyName}}"/>
|
||||
<TextBlock Text="{Binding Target.FriendlyName}"/>
|
||||
</StackPanel>
|
||||
|
||||
<Border Grid.Row="1" Grid.Column="1" Height="32" IsVisible="{Binding !Target.IsLocal}">
|
||||
|
|
|
@ -236,77 +236,12 @@
|
|||
<ToggleButton Grid.Row="0" Classes="group_expander" IsChecked="{Binding IsLocalBranchGroupExpanded, Mode=TwoWay}">
|
||||
<TextBlock Classes="group_header_label" Margin="0" Text="{DynamicResource Text.Repository.LocalBranches}"/>
|
||||
</ToggleButton>
|
||||
<TreeView Grid.Row="1"
|
||||
<v:BranchTree Grid.Row="1"
|
||||
x:Name="localBranchTree"
|
||||
Margin="8,0,4,0"
|
||||
SelectionMode="Multiple"
|
||||
ItemsSource="{Binding LocalBranchTrees}"
|
||||
Nodes="{Binding LocalBranchTrees}"
|
||||
IsVisible="{Binding IsLocalBranchGroupExpanded}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
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>
|
||||
SelectionChanged="OnLocalBranchTreeSelectionChanged"/>
|
||||
|
||||
<!-- Remotes -->
|
||||
<ToggleButton Grid.Row="2" Classes="group_expander" IsChecked="{Binding IsRemoteGroupExpanded, Mode=TwoWay}">
|
||||
|
@ -317,64 +252,12 @@
|
|||
</Button>
|
||||
</Grid>
|
||||
</ToggleButton>
|
||||
<TreeView Grid.Row="3"
|
||||
<v:BranchTree Grid.Row="3"
|
||||
x:Name="remoteBranchTree"
|
||||
Margin="8,0,4,0"
|
||||
SelectionMode="Multiple"
|
||||
ItemsSource="{Binding RemoteBranchTrees}"
|
||||
Nodes="{Binding RemoteBranchTrees}"
|
||||
IsVisible="{Binding IsRemoteGroupExpanded}"
|
||||
ScrollViewer.HorizontalScrollBarVisibility="Disabled"
|
||||
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>
|
||||
SelectionChanged="OnRemoteBranchTreeSelectionChanged"/>
|
||||
|
||||
<!-- Tags -->
|
||||
<ToggleButton Grid.Row="4" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}">
|
||||
|
@ -455,8 +338,8 @@
|
|||
<ToggleButton Classes="filter"
|
||||
Margin="0,0,8,0"
|
||||
Background="Transparent"
|
||||
Checked="OnToggleFilter"
|
||||
Unchecked="OnToggleFilter"
|
||||
Checked="OnToggleTagFilter"
|
||||
Unchecked="OnToggleTagFilter"
|
||||
IsChecked="{Binding IsFiltered}"
|
||||
ToolTip.Tip="{DynamicResource Text.Filter}"/>
|
||||
</DataTemplate>
|
||||
|
|
|
@ -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<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()
|
||||
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<ViewModels.BranchTreeNode> nodes)
|
||||
protected override void OnLoaded(RoutedEventArgs e)
|
||||
{
|
||||
int count = nodes.Count;
|
||||
base.OnLoaded(e);
|
||||
|
||||
foreach (var node in nodes)
|
||||
{
|
||||
if (!node.IsBranch && node.IsExpanded)
|
||||
count += GetTreeRowsCount(node.Children);
|
||||
if (DataContext is ViewModels.Repository { IsSearching: false })
|
||||
UpdateLeftSidebarLayout();
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue