From 4dd2847d35819dee58e356b5b0b9b0dfb6484c6a Mon Sep 17 00:00:00 2001 From: leo Date: Tue, 31 Mar 2026 13:14:45 +0800 Subject: [PATCH] feature: use custom `BranchSelector` instead of `ComboBox` to select remote branches with searching enabled (#2217) Signed-off-by: leo --- src/ViewModels/AddWorktree.cs | 33 +++-- src/Views/AddWorktree.axaml | 23 ++-- src/Views/BranchSelector.axaml | 154 +++++++++++++++++++++++ src/Views/BranchSelector.axaml.cs | 197 ++++++++++++++++++++++++++++++ src/Views/SetUpstream.axaml | 24 ++-- 5 files changed, 380 insertions(+), 51 deletions(-) create mode 100644 src/Views/BranchSelector.axaml create mode 100644 src/Views/BranchSelector.axaml.cs diff --git a/src/ViewModels/AddWorktree.cs b/src/ViewModels/AddWorktree.cs index 5c7b21d2..a818fdc1 100644 --- a/src/ViewModels/AddWorktree.cs +++ b/src/ViewModels/AddWorktree.cs @@ -37,12 +37,6 @@ namespace SourceGit.ViewModels private set; } - public List RemoteBranches - { - get; - private set; - } - public string SelectedBranch { get => _selectedBranch; @@ -59,10 +53,16 @@ namespace SourceGit.ViewModels } } - public string SelectedTrackingBranch + public List 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(); - RemoteBranches = new List(); + RemoteBranches = new List(); 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; } } diff --git a/src/Views/AddWorktree.axaml b/src/Views/AddWorktree.axaml index 6fcc546e..3419eff8 100644 --- a/src/Views/AddWorktree.axaml +++ b/src/Views/AddWorktree.axaml @@ -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}"/> - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Views/BranchSelector.axaml.cs b/src/Views/BranchSelector.axaml.cs new file mode 100644 index 00000000..94f86c51 --- /dev/null +++ b/src/Views/BranchSelector.axaml.cs @@ -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> BranchesProperty = + AvaloniaProperty.Register>(nameof(Branches)); + + public List Branches + { + get => GetValue(BranchesProperty); + set => SetValue(BranchesProperty, value); + } + + public static readonly StyledProperty> VisibleBranchesProperty = + AvaloniaProperty.Register>(nameof(VisibleBranches)); + + public List VisibleBranches + { + get => GetValue(VisibleBranchesProperty); + set => SetValue(VisibleBranchesProperty, value); + } + + public static readonly StyledProperty SelectedBranchProperty = + AvaloniaProperty.Register(nameof(SelectedBranch)); + + public Models.Branch SelectedBranch + { + get => GetValue(SelectedBranchProperty); + set => SetValue(SelectedBranchProperty, value); + } + + public static readonly StyledProperty UseFriendlyNameProperty = + AvaloniaProperty.Register(nameof(UseFriendlyName)); + + public bool UseFriendlyName + { + get => GetValue(UseFriendlyNameProperty); + set => SetValue(UseFriendlyNameProperty, value); + } + + public static readonly StyledProperty IsDropDownOpenedProperty = + AvaloniaProperty.Register(nameof(IsDropDownOpened)); + + public bool IsDropDownOpened + { + get => GetValue(IsDropDownOpenedProperty); + set => SetValue(IsDropDownOpenedProperty, value); + } + + public static readonly StyledProperty SearchFilterProperty = + AvaloniaProperty.Register(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(); + 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("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?.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?.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; + } +} diff --git a/src/Views/SetUpstream.axaml b/src/Views/SetUpstream.axaml index ba116e2d..367dca66 100644 --- a/src/Views/SetUpstream.axaml +++ b/src/Views/SetUpstream.axaml @@ -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"> @@ -32,23 +33,12 @@ HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,8,0" Text="{DynamicResource Text.SetUpstream.Upstream}"/> - - - - - - - - - - +