Merge branch 'develop' into graph_thickness

This commit is contained in:
ghiboz 2024-07-07 10:54:54 +02:00
commit 2c310e8cd4
17 changed files with 614 additions and 509 deletions

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

@ -0,0 +1,108 @@
<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}"
Background="Transparent"
RowHeight="24"
CanUserReorderColumns="False"
CanUserResizeColumns="False"
CanUserSortColumns="False"
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,347 @@
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));
public ViewModels.BranchTreeNode Node
{
get => GetValue(NodeProperty);
set => SetValue(NodeProperty, value);
}
public static readonly StyledProperty<bool> IsExpandedProperty =
AvaloniaProperty.Register<BranchTreeNodeIcon, bool>(nameof(IsExpanded));
public bool IsExpanded
{
get => GetValue(IsExpandedProperty);
set => SetValue(IsExpandedProperty, value);
}
static BranchTreeNodeIcon()
{
NodeProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((icon, _) => icon.UpdateContent());
IsExpandedProperty.Changed.AddClassHandler<BranchTreeNodeIcon>((icon, _) => 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));
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 static readonly RoutedEvent<RoutedEventArgs> RowsChangedEvent =
RoutedEvent.Register<BranchTree, RoutedEventArgs>(nameof(RowsChanged), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<RoutedEventArgs> RowsChanged
{
add { AddHandler(RowsChangedEvent, value); }
remove { RemoveHandler(RowsChangedEvent, value); }
}
public BranchTree()
{
InitializeComponent();
}
public void UnselectAll()
{
BranchesPresenter.SelectedItem = null;
}
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
if (Bounds.Height >= 23.0)
BranchesPresenter.Height = Bounds.Height;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == NodesProperty)
{
Rows.Clear();
if (Nodes is { Count: > 0 })
{
var rows = new List<ViewModels.BranchTreeNode>();
MakeRows(rows, Nodes, 0);
Rows.AddRange(rows);
}
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
else if (change.Property == IsVisibleProperty)
{
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
private void OnNodesSelectionChanged(object _, SelectionChangedEventArgs e)
{
var repo = DataContext as ViewModels.Repository;
if (repo?.Settings == null)
return;
foreach (var item in e.AddedItems)
{
if (item is ViewModels.BranchTreeNode node)
node.IsSelected = true;
}
foreach (var item in e.RemovedItems)
{
if (item is ViewModels.BranchTreeNode node)
node.IsSelected = false;
}
var selected = BranchesPresenter.SelectedItems;
if (selected == null || selected.Count == 0)
return;
if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Branch branch })
repo.NavigateToCommit(branch.Head);
var prev = null as ViewModels.BranchTreeNode;
foreach (var row in Rows)
{
if (row.IsSelected)
{
if (prev is { IsSelected: true })
{
var prevTop = prev.CornerRadius.TopLeft;
prev.CornerRadius = new CornerRadius(prevTop, 0);
row.CornerRadius = new CornerRadius(0, 4);
}
else
{
row.CornerRadius = new CornerRadius(4);
}
}
prev = row;
}
RaiseEvent(new RoutedEventArgs(SelectionChangedEvent));
}
private void OnTreeContextRequested(object _1, ContextRequestedEventArgs _2)
{
var repo = DataContext as ViewModels.Repository;
if (repo?.Settings == null)
return;
var selected = BranchesPresenter.SelectedItems;
if (selected == null || selected.Count == 0)
return;
if (selected.Count == 1 && selected[0] is ViewModels.BranchTreeNode { Backend: Models.Remote remote })
{
var menu = repo.CreateContextMenuForRemote(remote);
this.OpenContextMenu(menu);
return;
}
var branches = new List<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 _)
{
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);
}
RaiseEvent(new RoutedEventArgs(RowsChangedEvent));
}
}
}
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: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}">

View file

@ -236,77 +236,14 @@
<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"
x:Name="localBranchTree"
Margin="8,0,4,0"
SelectionMode="Multiple"
ItemsSource="{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>
<v:BranchTree Grid.Row="1"
x:Name="localBranchTree"
Height="0"
Margin="8,0,4,0"
Nodes="{Binding LocalBranchTrees}"
IsVisible="{Binding IsLocalBranchGroupExpanded}"
SelectionChanged="OnLocalBranchTreeSelectionChanged"
RowsChanged="OnBranchTreeRowsChanged"/>
<!-- Remotes -->
<ToggleButton Grid.Row="2" Classes="group_expander" IsChecked="{Binding IsRemoteGroupExpanded, Mode=TwoWay}">
@ -317,64 +254,14 @@
</Button>
</Grid>
</ToggleButton>
<TreeView Grid.Row="3"
x:Name="remoteBranchTree"
Margin="8,0,4,0"
SelectionMode="Multiple"
ItemsSource="{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>
<v:BranchTree Grid.Row="3"
x:Name="remoteBranchTree"
Height="0"
Margin="8,0,4,0"
Nodes="{Binding RemoteBranchTrees}"
IsVisible="{Binding IsRemoteGroupExpanded}"
SelectionChanged="OnRemoteBranchTreeSelectionChanged"
RowsChanged="OnBranchTreeRowsChanged"/>
<!-- Tags -->
<ToggleButton Grid.Row="4" Classes="group_expander" IsChecked="{Binding IsTagGroupExpanded, Mode=TwoWay}">
@ -388,6 +275,7 @@
</ToggleButton>
<DataGrid Grid.Row="5"
x:Name="tagsList"
Height="0"
Margin="8,0,4,0"
Background="Transparent"
ItemsSource="{Binding VisibleTags}"
@ -455,8 +343,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>
@ -491,7 +379,7 @@
</ToggleButton>
<DataGrid Grid.Row="7"
x:Name="submoduleList"
MaxHeight="200"
Height="0"
Margin="8,0,4,0"
Background="Transparent"
ItemsSource="{Binding Submodules}"
@ -574,6 +462,7 @@
</ToggleButton>
<DataGrid Grid.Row="9"
x:Name="worktreeList"
Height="0"
Margin="8,0,4,0"
Background="Transparent"
ItemsSource="{Binding Worktrees}"

View file

@ -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
{
@ -21,11 +18,7 @@ namespace SourceGit.Views
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
if (DataContext is ViewModels.Repository repo && !repo.IsSearching)
{
UpdateLeftSidebarLayout();
}
UpdateLeftSidebarLayout();
}
private void OpenWithExternalTools(object sender, RoutedEventArgs e)
@ -60,24 +53,23 @@ namespace SourceGit.Views
e.Handled = true;
}
private async void OpenStatistics(object sender, RoutedEventArgs e)
private async void OpenStatistics(object _, RoutedEventArgs e)
{
if (DataContext is ViewModels.Repository repo)
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(TopLevel.GetTopLevel(this) as Window);
await dialog.ShowDialog(owner);
e.Handled = true;
}
}
private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
var grid = sender as Grid;
if (e.Property == IsVisibleProperty && grid.IsVisible)
if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true})
txtSearchCommitsBox.Focus();
}
private void OnSearchKeyDown(object sender, KeyEventArgs e)
private void OnSearchKeyDown(object _, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
@ -90,199 +82,39 @@ namespace SourceGit.Views
private void OnSearchResultDataGridSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null)
if (sender is DataGrid { SelectedItem: Models.Commit commit } && DataContext is ViewModels.Repository repo)
{
if (DataContext is ViewModels.Repository repo)
{
var commit = datagrid.SelectedItem as Models.Commit;
repo.NavigateToCommit(commit.SHA);
}
repo.NavigateToCommit(commit.SHA);
}
e.Handled = true;
}
private void OnLocalBranchTreeSelectionChanged(object sender, SelectionChangedEventArgs e)
private void OnBranchTreeRowsChanged(object _, RoutedEventArgs 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);
}
}
UpdateLeftSidebarLayout();
e.Handled = true;
}
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)
private void OnLocalBranchTreeSelectionChanged(object _1, RoutedEventArgs _2)
{
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)
private void OnRemoteBranchTreeSelectionChanged(object _1, RoutedEventArgs _2)
{
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)
private void OnTagDataGridSelectionChanged(object sender, SelectionChangedEventArgs _)
{
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)
if (sender is DataGrid { SelectedItem: Models.Tag tag })
{
localBranchTree.UnselectAll();
remoteBranchTree.UnselectAll();
var tag = datagrid.SelectedItem as Models.Tag;
if (DataContext is ViewModels.Repository repo)
repo.NavigateToCommit(tag.SHA);
}
@ -300,25 +132,11 @@ namespace SourceGit.Views
e.Handled = true;
}
private void OnToggleFilter(object sender, RoutedEventArgs e)
private void OnToggleTagFilter(object sender, RoutedEventArgs e)
{
if (sender is ToggleButton toggle)
if (sender is ToggleButton { DataContext: Models.Tag tag } toggle && DataContext is ViewModels.Repository repo)
{
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);
}
repo.UpdateFilter(tag.Name, toggle.IsChecked == true);
}
e.Handled = true;
@ -338,10 +156,10 @@ namespace SourceGit.Views
private void OnDoubleTappedSubmodule(object sender, TappedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo)
if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo)
{
var submodule = datagrid.SelectedItem as string;
(DataContext as ViewModels.Repository).OpenSubmodule(submodule);
var submodule = grid.SelectedItem as string;
repo.OpenSubmodule(submodule);
}
e.Handled = true;
@ -349,11 +167,11 @@ namespace SourceGit.Views
private void OnWorktreeContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo)
if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo)
{
var worktree = datagrid.SelectedItem as Models.Worktree;
var worktree = grid.SelectedItem as Models.Worktree;
var menu = repo.CreateContextMenuForWorktree(worktree);
datagrid.OpenContextMenu(menu);
grid.OpenContextMenu(menu);
}
e.Handled = true;
@ -361,42 +179,16 @@ namespace SourceGit.Views
private void OnDoubleTappedWorktree(object sender, TappedEventArgs e)
{
if (sender is DataGrid datagrid && datagrid.SelectedItem != null && DataContext is ViewModels.Repository repo)
if (sender is DataGrid { SelectedItem: not null } grid && DataContext is ViewModels.Repository repo)
{
var worktree = datagrid.SelectedItem as Models.Worktree;
(DataContext as ViewModels.Repository).OpenWorktree(worktree);
var worktree = grid.SelectedItem as Models.Worktree;
repo.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)
private void OnLeftSidebarDataGridPropertyChanged(object _, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == DataGrid.ItemsSourceProperty || e.Property == DataGrid.IsVisibleProperty)
{
@ -414,8 +206,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;
@ -434,7 +226,7 @@ namespace SourceGit.Views
else
height = Math.Max(200, test);
}
leftHeight -= height;
tagsList.Height = height;
hasOverflow = (desiredBranches + desiredSubmodule + desiredWorktree) > leftHeight;
@ -522,19 +314,8 @@ namespace SourceGit.Views
remoteBranchTree.Height = height;
}
}
}
private int GetTreeRowsCount(List<ViewModels.BranchTreeNode> nodes)
{
int count = nodes.Count;
foreach (var node in nodes)
{
if (!node.IsBranch && node.IsExpanded)
count += GetTreeRowsCount(node.Children);
}
return count;
leftSidebarGroups.InvalidateMeasure();
}
}
}

View file

@ -174,7 +174,7 @@
<v:CommitMessageTextBox Grid.Row="2" Text="{Binding CommitMessage, Mode=TwoWay}"/>
<!-- Commit Options -->
<Grid Grid.Row="3" Margin="0,6,0,0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
<Grid Grid.Row="3" Margin="0,6,0,0" ColumnDefinitions="Auto,Auto,Auto,*,Auto,Auto,Auto">
<Button Grid.Column="0"
Classes="icon_button"
Width="14" Height="14"
@ -184,15 +184,23 @@
</Button>
<CheckBox Grid.Column="1"
Height="24"
Margin="12,0,0,0"
HorizontalAlignment="Left"
IsChecked="{Binding AutoStageBeforeCommit, Mode=TwoWay}"
Content="{DynamicResource Text.WorkingCopy.AutoStage}"
ToolTip.Tip="{DynamicResource Text.WorkingCopy.AutoStage.Tip}"/>
<CheckBox Grid.Column="2"
Height="24"
Margin="12,0,0,0"
HorizontalAlignment="Left"
IsChecked="{Binding UseAmend, Mode=TwoWay}"
Content="{DynamicResource Text.WorkingCopy.Amend}"/>
<v:LoadingIcon Grid.Column="3" Width="18" Height="18" IsVisible="{Binding IsCommitting}"/>
<v:LoadingIcon Grid.Column="4" Width="18" Height="18" IsVisible="{Binding IsCommitting}"/>
<Button Grid.Column="4"
<Button Grid.Column="5"
Classes="flat primary"
Content="{DynamicResource Text.WorkingCopy.Commit}"
Height="28"
@ -202,7 +210,7 @@
HotKey="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"
ToolTip.Tip="{OnPlatform Ctrl+Enter, macOS=⌘+Enter}"/>
<Button Grid.Column="5"
<Button Grid.Column="6"
Classes="flat"
Content="{DynamicResource Text.WorkingCopy.CommitAndPush}"
Height="28"