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

View file

@ -4,7 +4,7 @@ namespace SourceGit.Commands
{ {
public class Commit : Command public class Commit : Command
{ {
public Commit(string repo, string message, bool amend, bool allowEmpty = false) public Commit(string repo, string message, bool autoStage, bool amend, bool allowEmpty = false)
{ {
var file = Path.GetTempFileName(); var file = Path.GetTempFileName();
File.WriteAllText(file, message); File.WriteAllText(file, message);
@ -12,6 +12,8 @@ namespace SourceGit.Commands
WorkingDirectory = repo; WorkingDirectory = repo;
Context = repo; Context = repo;
Args = $"commit --file=\"{file}\""; Args = $"commit --file=\"{file}\"";
if (autoStage)
Args += " --all";
if (amend) if (amend)
Args += " --amend --no-edit"; Args += " --amend --no-edit";
if (allowEmpty) if (allowEmpty)

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

@ -543,6 +543,8 @@
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">Ignore files in the same folder</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">Ignore files in the same folder</x:String>
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">Ignore this file only</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">Ignore this file only</x:String>
<x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">Amend</x:String> <x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">Amend</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage" xml:space="preserve">Auto-Stage</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage.Tip" xml:space="preserve">Tell the command to automatically stage files that have been modified and deleted, but new files you have not told Git about are not affected.</x:String>
<x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">You can stage this file now.</x:String> <x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">You can stage this file now.</x:String>
<x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">COMMIT</x:String> <x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">COMMIT</x:String>
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">COMMIT &amp; PUSH</x:String> <x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">COMMIT &amp; PUSH</x:String>

View file

@ -545,6 +545,8 @@
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同目录下所有文件</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同目录下所有文件</x:String>
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本文件</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本文件</x:String>
<x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修补(--amend)</x:String> <x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修补(--amend)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage" xml:space="preserve">自动暂存(--all)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage.Tip" xml:space="preserve">提交前自动将修改过和删除的文件加入暂存区,但新增文件需要手动添加。</x:String>
<x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">现在您已可将其加入暂存区中</x:String> <x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">现在您已可将其加入暂存区中</x:String>
<x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String> <x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String>
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交并推送</x:String> <x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交并推送</x:String>

View file

@ -545,6 +545,8 @@
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同路徑下所有檔案</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.InSameFolder" xml:space="preserve">忽略同路徑下所有檔案</x:String>
<x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本檔案</x:String> <x:String x:Key="Text.WorkingCopy.AddToGitIgnore.SingleFile" xml:space="preserve">忽略本檔案</x:String>
<x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修補(--amend)</x:String> <x:String x:Key="Text.WorkingCopy.Amend" xml:space="preserve">修補(--amend)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage" xml:space="preserve">自動暫存(--all)</x:String>
<x:String x:Key="Text.WorkingCopy.AutoStage.Tip" xml:space="preserve">提交前自動將修改過和刪除的檔案加入暫存區,但新增檔案需要手動添加。</x:String>
<x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">現在您已可將其加入暫存區中</x:String> <x:String x:Key="Text.WorkingCopy.CanStageTip" xml:space="preserve">現在您已可將其加入暫存區中</x:String>
<x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String> <x:String x:Key="Text.WorkingCopy.Commit" xml:space="preserve">提交</x:String>
<x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交併推送</x:String> <x:String x:Key="Text.WorkingCopy.CommitAndPush" xml:space="preserve">提交併推送</x:String>

View file

@ -772,6 +772,7 @@
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
WindowManagerAddShadowHint="False" WindowManagerAddShadowHint="False"
Placement="RightEdgeAlignedTop" Placement="RightEdgeAlignedTop"
MaxHeight="400"
IsLightDismissEnabled="False" IsLightDismissEnabled="False"
HorizontalOffset="-4" HorizontalOffset="-4"
VerticalOffset="-4" VerticalOffset="-4"
@ -793,7 +794,13 @@
<ItemsPresenter Name="PART_ItemsPresenter" <ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" ItemsPanel="{TemplateBinding ItemsPanel}"
Margin="{DynamicResource MenuFlyoutScrollerMargin}" Margin="{DynamicResource MenuFlyoutScrollerMargin}"
Grid.IsSharedSizeScope="True" /> Grid.IsSharedSizeScope="True">
<ItemsPresenter.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ItemsPresenter.ItemsPanel>
</ItemsPresenter>
</ScrollViewer> </ScrollViewer>
</Border> </Border>
</Grid> </Grid>

View file

@ -1,122 +1,63 @@
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 bool IsSelected { get; set; } = false;
public bool IsFiltered { get; set; } public List<BranchTreeNode> Children { get; private set; } = new List<BranchTreeNode>();
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
{ {
get => _isExpanded; get => _isExpanded;
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;
if (!IsBranch && IsExpanded)
{
foreach (var child in Children)
child.UpdateCornerRadius(ref prev);
}
} }
private bool _isSelected = false; public bool IsUpstreamTrackStatusVisible
{
get => Backend is Models.Branch { IsLocal: true } branch && !string.IsNullOrEmpty(branch.UpstreamTrackStatus);
}
public string UpstreamTrackStatus
{
get => Backend is Models.Branch branch ? branch.UpstreamTrackStatus : "";
}
public FontWeight NameFontWeight
{
get => Backend is Models.Branch { IsCurrent: true } ? FontWeight.Bold : FontWeight.Regular;
}
public string Tooltip
{
get => Backend is Models.Branch b ? b.FriendlyName : null;
}
private bool _isExpanded = false; private bool _isExpanded = false;
private CornerRadius _cornerRadius = new CornerRadius(DEFAULT_CORNER); private CornerRadius _cornerRadius = new CornerRadius(4);
public class Builder public class Builder
{ {
@ -133,7 +74,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 +116,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 +135,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 +158,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 +168,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 +179,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 +192,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

@ -64,6 +64,12 @@ namespace SourceGit.ViewModels
set; set;
} = true; } = true;
public bool AutoStageBeforeCommit
{
get;
set;
} = false;
public AvaloniaList<string> Filters public AvaloniaList<string> Filters
{ {
get; get;
@ -1905,7 +1911,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;

View file

@ -39,7 +39,7 @@ namespace SourceGit.ViewModels
return Task.Run(() => return Task.Run(() =>
{ {
var succ = new Commands.Commit(_repo.FullPath, _message, true, true).Exec(); var succ = new Commands.Commit(_repo.FullPath, _message, false, true, true).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true)); CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ; return succ;
}); });

View file

@ -43,7 +43,7 @@ namespace SourceGit.ViewModels
{ {
var succ = new Commands.Reset(_repo.FullPath, Parent.SHA, "--soft").Exec(); var succ = new Commands.Reset(_repo.FullPath, Parent.SHA, "--soft").Exec();
if (succ) if (succ)
succ = new Commands.Commit(_repo.FullPath, _message, true).Exec(); succ = new Commands.Commit(_repo.FullPath, _message, false, true).Exec();
CallUIThread(() => _repo.SetWatcherEnabled(true)); CallUIThread(() => _repo.SetWatcherEnabled(true));
return succ; return succ;
}); });

View file

@ -77,6 +77,12 @@ namespace SourceGit.ViewModels
private set => SetProperty(ref _isCommitting, value); private set => SetProperty(ref _isCommitting, value);
} }
public bool AutoStageBeforeCommit
{
get => _repo.Settings.AutoStageBeforeCommit;
set => _repo.Settings.AutoStageBeforeCommit = value;
}
public bool UseAmend public bool UseAmend
{ {
get => _useAmend; get => _useAmend;
@ -1216,7 +1222,13 @@ namespace SourceGit.ViewModels
return; return;
} }
if (_staged.Count == 0) if (_count == 0)
{
App.RaiseException(_repo.FullPath, "No files added to commit!");
return;
}
if (!AutoStageBeforeCommit && _staged.Count == 0)
{ {
App.RaiseException(_repo.FullPath, "No files added to commit!"); App.RaiseException(_repo.FullPath, "No files added to commit!");
return; return;
@ -1234,9 +1246,10 @@ namespace SourceGit.ViewModels
IsCommitting = true; IsCommitting = true;
_repo.SetWatcherEnabled(false); _repo.SetWatcherEnabled(false);
var autoStage = AutoStageBeforeCommit;
Task.Run(() => Task.Run(() =>
{ {
var succ = new Commands.Commit(_repo.FullPath, _commitMessage, _useAmend).Exec(); var succ = new Commands.Commit(_repo.FullPath, _commitMessage, autoStage, _useAmend).Exec();
Dispatcher.UIThread.Post(() => Dispatcher.UIThread.Post(() =>
{ {
if (succ) if (succ)

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: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,14 @@
<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" Height="0"
SelectionMode="Multiple" Margin="8,0,4,0"
ItemsSource="{Binding LocalBranchTrees}" Nodes="{Binding LocalBranchTrees}"
IsVisible="{Binding IsLocalBranchGroupExpanded}" IsVisible="{Binding IsLocalBranchGroupExpanded}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionChanged="OnLocalBranchTreeSelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Auto" RowsChanged="OnBranchTreeRowsChanged"/>
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 +254,14 @@
</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" Height="0"
SelectionMode="Multiple" Margin="8,0,4,0"
ItemsSource="{Binding RemoteBranchTrees}" Nodes="{Binding RemoteBranchTrees}"
IsVisible="{Binding IsRemoteGroupExpanded}" IsVisible="{Binding IsRemoteGroupExpanded}"
ScrollViewer.HorizontalScrollBarVisibility="Disabled" SelectionChanged="OnRemoteBranchTreeSelectionChanged"
ScrollViewer.VerticalScrollBarVisibility="Auto" RowsChanged="OnBranchTreeRowsChanged"/>
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}">
@ -388,6 +275,7 @@
</ToggleButton> </ToggleButton>
<DataGrid Grid.Row="5" <DataGrid Grid.Row="5"
x:Name="tagsList" x:Name="tagsList"
Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding VisibleTags}" ItemsSource="{Binding VisibleTags}"
@ -455,8 +343,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>
@ -491,7 +379,7 @@
</ToggleButton> </ToggleButton>
<DataGrid Grid.Row="7" <DataGrid Grid.Row="7"
x:Name="submoduleList" x:Name="submoduleList"
MaxHeight="200" Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding Submodules}" ItemsSource="{Binding Submodules}"
@ -574,6 +462,7 @@
</ToggleButton> </ToggleButton>
<DataGrid Grid.Row="9" <DataGrid Grid.Row="9"
x:Name="worktreeList" x:Name="worktreeList"
Height="0"
Margin="8,0,4,0" Margin="8,0,4,0"
Background="Transparent" Background="Transparent"
ItemsSource="{Binding Worktrees}" ItemsSource="{Binding Worktrees}"

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

View file

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