mirror of
https://fastgit.cc/github.com/sourcegit-scm/sourcegit
synced 2026-04-21 13:20:30 +08:00
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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
154
src/Views/BranchSelector.axaml
Normal file
154
src/Views/BranchSelector.axaml
Normal 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>
|
||||
197
src/Views/BranchSelector.axaml.cs
Normal file
197
src/Views/BranchSelector.axaml.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
@@ -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}"
|
||||
|
||||
Reference in New Issue
Block a user