feature: use custom BranchSelector instead of ComboBox to select remote branches with searching enabled (#2217)

Signed-off-by: leo <longshuang@msn.cn>
This commit is contained in:
leo
2026-03-31 13:14:45 +08:00
parent 1f1cd2fbf3
commit 4dd2847d35
5 changed files with 380 additions and 51 deletions

View File

@@ -37,12 +37,6 @@ namespace SourceGit.ViewModels
private set;
}
public List<string> RemoteBranches
{
get;
private set;
}
public string SelectedBranch
{
get => _selectedBranch;
@@ -59,10 +53,16 @@ namespace SourceGit.ViewModels
}
}
public string SelectedTrackingBranch
public List<Models.Branch> RemoteBranches
{
get;
set;
private set;
}
public Models.Branch SelectedTrackingBranch
{
get => _selectedTrackingBranch;
set => SetProperty(ref _selectedTrackingBranch, value);
}
public AddWorktree(Repository repo)
@@ -70,13 +70,13 @@ namespace SourceGit.ViewModels
_repo = repo;
LocalBranches = new List<string>();
RemoteBranches = new List<string>();
RemoteBranches = new List<Models.Branch>();
foreach (var branch in repo.Branches)
{
if (branch.IsLocal)
LocalBranches.Add(branch.Name);
else
RemoteBranches.Add(branch.FriendlyName);
RemoteBranches.Add(branch);
}
}
@@ -110,7 +110,7 @@ namespace SourceGit.ViewModels
ProgressDescription = "Adding worktree ...";
var branchName = _selectedBranch;
var tracking = _setTrackingBranch ? SelectedTrackingBranch : string.Empty;
var tracking = (_setTrackingBranch && _selectedTrackingBranch != null) ? _selectedTrackingBranch.FriendlyName : string.Empty;
var log = _repo.CreateLog("Add Worktree");
Use(log);
@@ -129,15 +129,11 @@ namespace SourceGit.ViewModels
return;
var name = string.IsNullOrEmpty(_selectedBranch) ? System.IO.Path.GetFileName(_path.TrimEnd('/', '\\')) : _selectedBranch;
var remoteBranch = RemoteBranches.Find(b => b.EndsWith(name, StringComparison.Ordinal));
if (string.IsNullOrEmpty(remoteBranch))
var remoteBranch = RemoteBranches.Find(b => b.Name.EndsWith(name, StringComparison.Ordinal));
if (remoteBranch == null)
remoteBranch = RemoteBranches[0];
if (!remoteBranch.Equals(SelectedTrackingBranch, StringComparison.Ordinal))
{
SelectedTrackingBranch = remoteBranch;
OnPropertyChanged(nameof(SelectedTrackingBranch));
}
SelectedTrackingBranch = remoteBranch;
}
private Repository _repo = null;
@@ -145,5 +141,6 @@ namespace SourceGit.ViewModels
private bool _createNewBranch = true;
private string _selectedBranch = string.Empty;
private bool _setTrackingBranch = false;
private Models.Branch _selectedTrackingBranch = null;
}
}

View File

@@ -3,6 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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.AddWorktree"
@@ -84,22 +85,12 @@
Margin="0,0,8,0"
Text="{DynamicResource Text.AddWorktree.Tracking}"/>
</Border>
<ComboBox Grid.Row="3" Grid.Column="1"
Height="28" Padding="8,0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
ItemsSource="{Binding RemoteBranches}"
IsTextSearchEnabled="True"
SelectedItem="{Binding SelectedTrackingBranch, Mode=TwoWay}"
IsVisible="{Binding SetTrackingBranch, Mode=OneWay}">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center">
<Path Margin="0,0,8,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG1}" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<v:BranchSelector Grid.Row="3" Grid.Column="1"
Height="28"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
Branches="{Binding RemoteBranches}"
SelectedBranch="{Binding SelectedTrackingBranch, Mode=TwoWay}"
IsVisible="{Binding SetTrackingBranch, Mode=OneWay}"/>
<CheckBox Grid.Row="4" Grid.Column="1"
Height="32"

View File

@@ -0,0 +1,154 @@
<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:m="using:SourceGit.Models"
xmlns:v="using:SourceGit.Views"
mc:Ignorable="d" d:DesignWidth="500" d:DesignHeight="450"
x:Class="SourceGit.Views.BranchSelector">
<UserControl.Styles>
<Style Selector="v|BranchSelector">
<Setter Property="FocusAdorner">
<FocusAdornerTemplate>
<Border/>
</FocusAdornerTemplate>
</Setter>
<Setter Property="Template">
<ControlTemplate>
<Grid Background="Transparent">
<Border x:Name="PART_Background"
Background="{DynamicResource Brush.Contents}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border1}"
CornerRadius="3"/>
<Grid x:Name="PART_Selected"
Background="Transparent"
ColumnDefinitions="Auto,*,32"
PointerPressed="OnToggleDropDown">
<Path Grid.Column="0"
Margin="8,0,0,0"
Width="14" Height="14"
Data="{StaticResource Icons.Branch}"
IsHitTestVisible="False"/>
<ContentControl Grid.Column="1"
Margin="8,0,0,0"
Content="{TemplateBinding SelectedBranch, Mode=OneWay}">
<ContentControl.DataTemplates>
<DataTemplate DataType="m:Branch">
<Grid>
<TextBlock Text="{Binding FriendlyName, Mode=OneWay}"/>
</Grid>
</DataTemplate>
</ContentControl.DataTemplates>
</ContentControl>
<Path Grid.Column="2"
Width="12" Height="12"
Margin="0,0,10,0"
HorizontalAlignment="Right" VerticalAlignment="Center"
Data="M0 0 M1939 486L2029 576L1024 1581L19 576L109 486L1024 1401L1939 486Z"
IsHitTestVisible="False"/>
</Grid>
<Popup x:Name="PART_Popup"
WindowManagerAddShadowHint="False"
IsOpen="{TemplateBinding IsDropDownOpened, Mode=TwoWay}"
Width="{Binding Bounds.Width, ElementName=PART_Background}"
MaxHeight="600"
PlacementTarget="PART_Background"
Placement="BottomEdgeAlignedLeft"
VerticalOffset="2"
IsLightDismissEnabled="True"
InheritsTransform="True">
<Border Background="{DynamicResource Brush.Contents}"
BorderThickness="1"
BorderBrush="{DynamicResource Brush.Border1}"
CornerRadius="4"
Padding="4"
HorizontalAlignment="Stretch">
<Grid RowDefinitions="36,Auto">
<TextBox Grid.Row="0"
x:Name="PART_TextFilter"
Height="24"
Margin="6,0"
BorderThickness="1"
CornerRadius="12"
Text="{TemplateBinding SearchFilter, Mode=TwoWay}"
BorderBrush="{DynamicResource Brush.Border2}"
VerticalContentAlignment="Center"
KeyDown="OnSearchBoxKeyDown">
<TextBox.InnerLeftContent>
<Path Width="14" Height="14"
Margin="6,0,0,0"
Fill="{DynamicResource Brush.FG2}"
Data="{StaticResource Icons.Search}"/>
</TextBox.InnerLeftContent>
<TextBox.InnerRightContent>
<Button Classes="icon_button"
Width="16"
Margin="0,0,6,0"
Click="OnClearSearchFilter"
IsVisible="{Binding ElementName=PART_TextFilter, Path=Text, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Right">
<Path Width="14" Height="14"
Margin="0,1,0,0"
Fill="{DynamicResource Brush.FG1}"
Data="{StaticResource Icons.Clear}"/>
</Button>
</TextBox.InnerRightContent>
</TextBox>
<ListBox Grid.Row="1"
Focusable="True"
Margin="0,4"
MaxHeight="360"
BorderThickness="0"
Background="Transparent"
ItemsSource="{TemplateBinding VisibleBranches, Mode=OneWay}"
SelectedItem="{TemplateBinding SelectedBranch, Mode=TwoWay}"
KeyDown="OnDropDownListKeyDown">
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
<ListBox.Styles>
<Style Selector="ListBoxItem">
<Setter Property="Height" Value="28"/>
<Setter Property="CornerRadius" Value="3"/>
</Style>
</ListBox.Styles>
<ListBox.ItemTemplate>
<DataTemplate DataType="m:Branch">
<StackPanel Orientation="Horizontal" Background="Transparent" Height="28" PointerPressed="OnDropDownItemPointerPressed">
<Path Width="14" Height="14" Fill="{DynamicResource Brush.FG1}" Data="{StaticResource Icons.Branch}"/>
<TextBlock Margin="8,0,0,0" Text="{Binding FriendlyName, Mode=OneWay}" VerticalAlignment="Center"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Grid>
</Border>
</Popup>
</Grid>
</ControlTemplate>
</Setter>
<Style Selector="^:pointerover /template/ Border#PART_Background">
<Setter Property="BorderBrush" Value="{DynamicResource Brush.Accent}" />
</Style>
<Style Selector="^:focus-visible">
<Style Selector="^ /template/ Border#PART_Background">
<Setter Property="Background" Value="{DynamicResource ComboBoxBackgroundUnfocused}" />
</Style>
</Style>
</Style>
</UserControl.Styles>
</UserControl>

View File

@@ -0,0 +1,197 @@
using System;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
namespace SourceGit.Views
{
public partial class BranchSelector : UserControl
{
public static readonly StyledProperty<List<Models.Branch>> BranchesProperty =
AvaloniaProperty.Register<BranchSelector, List<Models.Branch>>(nameof(Branches));
public List<Models.Branch> Branches
{
get => GetValue(BranchesProperty);
set => SetValue(BranchesProperty, value);
}
public static readonly StyledProperty<List<Models.Branch>> VisibleBranchesProperty =
AvaloniaProperty.Register<BranchSelector, List<Models.Branch>>(nameof(VisibleBranches));
public List<Models.Branch> VisibleBranches
{
get => GetValue(VisibleBranchesProperty);
set => SetValue(VisibleBranchesProperty, value);
}
public static readonly StyledProperty<Models.Branch> SelectedBranchProperty =
AvaloniaProperty.Register<BranchSelector, Models.Branch>(nameof(SelectedBranch));
public Models.Branch SelectedBranch
{
get => GetValue(SelectedBranchProperty);
set => SetValue(SelectedBranchProperty, value);
}
public static readonly StyledProperty<bool> UseFriendlyNameProperty =
AvaloniaProperty.Register<BranchSelector, bool>(nameof(UseFriendlyName));
public bool UseFriendlyName
{
get => GetValue(UseFriendlyNameProperty);
set => SetValue(UseFriendlyNameProperty, value);
}
public static readonly StyledProperty<bool> IsDropDownOpenedProperty =
AvaloniaProperty.Register<BranchSelector, bool>(nameof(IsDropDownOpened));
public bool IsDropDownOpened
{
get => GetValue(IsDropDownOpenedProperty);
set => SetValue(IsDropDownOpenedProperty, value);
}
public static readonly StyledProperty<string> SearchFilterProperty =
AvaloniaProperty.Register<BranchSelector, string>(nameof(SearchFilter));
public string SearchFilter
{
get => GetValue(SearchFilterProperty);
set => SetValue(SearchFilterProperty, value);
}
public BranchSelector()
{
Focusable = true;
InitializeComponent();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == BranchesProperty || change.Property == SearchFilterProperty)
{
var branches = Branches;
var filter = SearchFilter;
if (branches is not { Count: > 0 })
{
SetCurrentValue(VisibleBranchesProperty, []);
}
else if (string.IsNullOrEmpty(filter))
{
SetCurrentValue(VisibleBranchesProperty, Branches);
}
else
{
var visible = new List<Models.Branch>();
var oldSelection = SelectedBranch;
var keepSelection = false;
foreach (var b in Branches)
{
if (b.FriendlyName.Contains(SearchFilter, StringComparison.OrdinalIgnoreCase))
{
visible.Add(b);
if (!keepSelection)
keepSelection = (b == oldSelection);
}
}
SetCurrentValue(VisibleBranchesProperty, visible);
if (!keepSelection && visible.Count > 0)
SetCurrentValue(SelectedBranchProperty, visible[0]);
}
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_popup != null)
{
_popup.Opened -= OnPopupOpened;
_popup.Closed -= OnPopupClosed;
}
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += OnPopupOpened;
_popup.Closed += OnPopupClosed;
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
if (e.Key == Key.Space && !IsDropDownOpened)
{
IsDropDownOpened = true;
e.Handled = true;
}
else if (e.Key == Key.Escape && IsDropDownOpened)
{
IsDropDownOpened = false;
e.Handled = true;
}
}
private void OnPopupOpened(object sender, EventArgs e)
{
var listBox = _popup?.Child?.FindDescendantOfType<ListBox>();
listBox?.Focus();
}
private void OnPopupClosed(object sender, EventArgs e)
{
Focus(NavigationMethod.Directional);
}
private void OnToggleDropDown(object sender, PointerPressedEventArgs e)
{
IsDropDownOpened = !IsDropDownOpened;
e.Handled = true;
}
private void OnSearchBoxKeyDown(object _, KeyEventArgs e)
{
if (e.Key == Key.Tab)
{
var listBox = _popup?.Child?.FindDescendantOfType<ListBox>();
listBox?.Focus();
e.Handled = true;
}
}
private void OnClearSearchFilter(object sender, RoutedEventArgs e)
{
SearchFilter = string.Empty;
e.Handled = true;
}
private void OnDropDownListKeyDown(object _, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
IsDropDownOpened = false;
e.Handled = true;
}
}
private void OnDropDownItemPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender is Control { DataContext: Models.Branch branch })
SelectedBranch = branch;
IsDropDownOpened = false;
e.Handled = true;
}
private Popup _popup = null;
}
}

View File

@@ -5,6 +5,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:m="using:SourceGit.Models"
xmlns:vm="using:SourceGit.ViewModels"
xmlns:v="using:SourceGit.Views"
x:Class="SourceGit.Views.SetUpstream"
x:DataType="vm:SetUpstream">
<StackPanel Orientation="Vertical" Margin="8,0">
@@ -32,23 +33,12 @@
HorizontalAlignment="Right" VerticalAlignment="Center"
Margin="0,0,8,0"
Text="{DynamicResource Text.SetUpstream.Upstream}"/>
<ComboBox Grid.Row="1" Grid.Column="1"
Height="28" Padding="8,0"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
ItemsSource="{Binding RemoteBranches}"
SelectedItem="{Binding SelectedRemoteBranch, Mode=TwoWay}"
IsTextSearchEnabled="True"
TextSearch.TextBinding="{Binding Name, DataType=m:Branch}"
IsEnabled="{Binding !Unset, Mode=OneWay}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="{x:Type m:Branch}">
<StackPanel Orientation="Horizontal" Height="20" VerticalAlignment="Center">
<Path Margin="0,0,8,0" Width="14" Height="14" Fill="{DynamicResource Brush.FG1}" Data="{StaticResource Icons.Branch}"/>
<TextBlock Text="{Binding FriendlyName}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<v:BranchSelector Grid.Row="1" Grid.Column="1"
Height="28"
VerticalAlignment="Center" HorizontalAlignment="Stretch"
Branches="{Binding RemoteBranches}"
SelectedBranch="{Binding SelectedRemoteBranch, Mode=TwoWay}"
IsEnabled="{Binding !Unset, Mode=OneWay}"/>
<CheckBox Grid.Row="2" Grid.Column="1"
Content="{DynamicResource Text.SetUpstream.Unset}"