feature: auto focus the next change after stage/unstage selected changes (#464)

This commit is contained in:
leo 2024-09-12 16:33:56 +08:00
parent dcddc5a2f2
commit ea3a6a4755
No known key found for this signature in database
4 changed files with 122 additions and 22 deletions

View file

@ -325,23 +325,24 @@ namespace SourceGit.ViewModels
PopupHost.ShowPopup(new StashChanges(_repo, _cached, true)); PopupHost.ShowPopup(new StashChanges(_repo, _cached, true));
} }
public void StageSelected() public void StageSelected(Models.Change next)
{ {
StageChanges(_selectedUnstaged); StageChanges(_selectedUnstaged, next);
SelectedUnstaged = [];
} }
public void StageAll() public void StageAll()
{ {
StageChanges(_unstaged); StageChanges(_unstaged, null);
SelectedUnstaged = [];
} }
public async void StageChanges(List<Models.Change> changes) public async void StageChanges(List<Models.Change> changes, Models.Change next)
{ {
if (_unstaged.Count == 0 || changes.Count == 0) if (_unstaged.Count == 0 || changes.Count == 0)
return; return;
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
_selectedUnstaged = next != null ? [next] : [];
IsStaging = true; IsStaging = true;
_repo.SetWatcherEnabled(false); _repo.SetWatcherEnabled(false);
if (changes.Count == _unstaged.Count) if (changes.Count == _unstaged.Count)
@ -362,23 +363,24 @@ namespace SourceGit.ViewModels
IsStaging = false; IsStaging = false;
} }
public void UnstageSelected() public void UnstageSelected(Models.Change next)
{ {
UnstageChanges(_selectedStaged); UnstageChanges(_selectedStaged, next);
SelectedStaged = [];
} }
public void UnstageAll() public void UnstageAll()
{ {
UnstageChanges(_staged); UnstageChanges(_staged, null);
SelectedStaged = [];
} }
public async void UnstageChanges(List<Models.Change> changes) public async void UnstageChanges(List<Models.Change> changes, Models.Change next)
{ {
if (_staged.Count == 0 || changes.Count == 0) if (_staged.Count == 0 || changes.Count == 0)
return; return;
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
_selectedStaged = next != null ? [next] : [];
IsUnstaging = true; IsUnstaging = true;
_repo.SetWatcherEnabled(false); _repo.SetWatcherEnabled(false);
if (_useAmend) if (_useAmend)
@ -499,7 +501,7 @@ namespace SourceGit.ViewModels
stage.Icon = App.CreateMenuIcon("Icons.File.Add"); stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) => stage.Click += (_, e) =>
{ {
StageChanges(_selectedUnstaged); StageChanges(_selectedUnstaged, null);
e.Handled = true; e.Handled = true;
}; };
@ -823,7 +825,7 @@ namespace SourceGit.ViewModels
stage.Icon = App.CreateMenuIcon("Icons.File.Add"); stage.Icon = App.CreateMenuIcon("Icons.File.Add");
stage.Click += (_, e) => stage.Click += (_, e) =>
{ {
StageChanges(_selectedUnstaged); StageChanges(_selectedUnstaged, null);
e.Handled = true; e.Handled = true;
}; };
@ -917,7 +919,7 @@ namespace SourceGit.ViewModels
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (_, e) => unstage.Click += (_, e) =>
{ {
UnstageChanges(_selectedStaged); UnstageChanges(_selectedStaged, null);
e.Handled = true; e.Handled = true;
}; };
@ -1086,7 +1088,7 @@ namespace SourceGit.ViewModels
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove"); unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
unstage.Click += (_, e) => unstage.Click += (_, e) =>
{ {
UnstageChanges(_selectedStaged); UnstageChanges(_selectedStaged, null);
e.Handled = true; e.Handled = true;
}; };

View file

@ -96,6 +96,7 @@ namespace SourceGit.Views
public ChangeCollectionView() public ChangeCollectionView()
{ {
Focusable = true;
InitializeComponent(); InitializeComponent();
} }
@ -132,6 +133,69 @@ namespace SourceGit.Views
} }
} }
public Models.Change GetNextChangeWithoutSelection()
{
var selected = SelectedChanges;
var changes = Changes;
if (selected == null || selected.Count == 0)
return changes.Count > 0 ? changes[0] : null;
if (selected.Count == changes.Count)
return null;
var set = new HashSet<string>();
foreach (var c in selected)
set.Add(c.Path);
if (Content is ViewModels.ChangeCollectionAsTree tree)
{
var lastUnselected = -1;
for (int i = tree.Rows.Count - 1; i >= 0; i--)
{
var row = tree.Rows[i];
if (!row.IsFolder)
{
if (set.Contains(row.FullPath))
{
if (lastUnselected == -1)
continue;
else
break;
}
else
{
lastUnselected = i;
}
}
}
if (lastUnselected != -1)
return tree.Rows[lastUnselected].Change;
}
else
{
var lastUnselected = -1;
for (int i = changes.Count - 1; i >= 0; i--)
{
if (set.Contains(changes[i].Path))
{
if (lastUnselected == -1)
continue;
else
break;
}
else
{
lastUnselected = i;
}
}
if (lastUnselected != -1)
return changes[lastUnselected];
}
return null;
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change); base.OnPropertyChanged(change);

View file

@ -42,7 +42,7 @@
Classes="icon_button" Classes="icon_button"
Width="26" Height="14" Width="26" Height="14"
Padding="0" Padding="0"
Command="{Binding StageSelected}"> Click="OnStageSelectedButtonClicked">
<ToolTip.Tip> <ToolTip.Tip>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.WorkingCopy.Unstaged.Stage}" VerticalAlignment="Center"/> <TextBlock Text="{DynamicResource Text.WorkingCopy.Unstaged.Stage}" VerticalAlignment="Center"/>
@ -64,6 +64,7 @@
<!-- Unstaged Changes --> <!-- Unstaged Changes -->
<v:ChangeCollectionView Grid.Row="1" <v:ChangeCollectionView Grid.Row="1"
x:Name="UnstagedChangesView"
IsUnstagedChange="True" IsUnstagedChange="True"
SelectionMode="Multiple" SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}" Background="{DynamicResource Brush.Contents}"
@ -81,7 +82,7 @@
<TextBlock Grid.Column="1" Text="{DynamicResource Text.WorkingCopy.Staged}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="8,0,0,0"/> <TextBlock Grid.Column="1" Text="{DynamicResource Text.WorkingCopy.Staged}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="8,0,0,0"/>
<TextBlock Grid.Column="2" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Staged, Converter={x:Static c:ListConverters.ToCount}}"/> <TextBlock Grid.Column="2" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Staged, Converter={x:Static c:ListConverters.ToCount}}"/>
<v:LoadingIcon Grid.Column="3" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsUnstaging}"/> <v:LoadingIcon Grid.Column="3" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsUnstaging}"/>
<Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" Command="{Binding UnstageSelected}"> <Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" Click="OnUnstageSelectedButtonClicked">
<ToolTip.Tip> <ToolTip.Tip>
<StackPanel Orientation="Horizontal" VerticalAlignment="Center"> <StackPanel Orientation="Horizontal" VerticalAlignment="Center">
<TextBlock Text="{DynamicResource Text.WorkingCopy.Staged.Unstage}" VerticalAlignment="Center"/> <TextBlock Text="{DynamicResource Text.WorkingCopy.Staged.Unstage}" VerticalAlignment="Center"/>
@ -98,6 +99,7 @@
<!-- Staged Changes --> <!-- Staged Changes -->
<v:ChangeCollectionView Grid.Row="3" <v:ChangeCollectionView Grid.Row="3"
x:Name="StagedChangesView"
SelectionMode="Multiple" SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}" Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=StagedChangeViewMode}" ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=StagedChangeViewMode}"

View file

@ -46,7 +46,9 @@ namespace SourceGit.Views
{ {
if (DataContext is ViewModels.WorkingCopy vm) if (DataContext is ViewModels.WorkingCopy vm)
{ {
vm.StageSelected(); var next = UnstagedChangesView.GetNextChangeWithoutSelection();
vm.StageSelected(next);
UnstagedChangesView.Focus();
e.Handled = true; e.Handled = true;
} }
} }
@ -55,7 +57,9 @@ namespace SourceGit.Views
{ {
if (DataContext is ViewModels.WorkingCopy vm) if (DataContext is ViewModels.WorkingCopy vm)
{ {
vm.UnstageSelected(); var next = StagedChangesView.GetNextChangeWithoutSelection();
vm.UnstageSelected(next);
StagedChangesView.Focus();
e.Handled = true; e.Handled = true;
} }
} }
@ -66,7 +70,9 @@ namespace SourceGit.Views
{ {
if (e.Key is Key.Space or Key.Enter) if (e.Key is Key.Space or Key.Enter)
{ {
vm.StageSelected(); var next = UnstagedChangesView.GetNextChangeWithoutSelection();
vm.StageSelected(next);
UnstagedChangesView.Focus();
e.Handled = true; e.Handled = true;
return; return;
} }
@ -84,11 +90,37 @@ namespace SourceGit.Views
{ {
if (DataContext is ViewModels.WorkingCopy vm && e.Key is Key.Space or Key.Enter) if (DataContext is ViewModels.WorkingCopy vm && e.Key is Key.Space or Key.Enter)
{ {
vm.UnstageSelected(); var next = StagedChangesView.GetNextChangeWithoutSelection();
vm.UnstageSelected(next);
StagedChangesView.Focus();
e.Handled = true; e.Handled = true;
} }
} }
private void OnStageSelectedButtonClicked(object _, RoutedEventArgs e)
{
if (DataContext is ViewModels.WorkingCopy vm)
{
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
vm.StageSelected(next);
UnstagedChangesView.Focus();
}
e.Handled = true;
}
private void OnUnstageSelectedButtonClicked(object _, RoutedEventArgs e)
{
if (DataContext is ViewModels.WorkingCopy vm)
{
var next = StagedChangesView.GetNextChangeWithoutSelection();
vm.UnstageSelected(next);
StagedChangesView.Focus();
}
e.Handled = true;
}
private void OnOpenAIAssist(object _, RoutedEventArgs e) private void OnOpenAIAssist(object _, RoutedEventArgs e)
{ {
if (!Models.OpenAI.IsValid) if (!Models.OpenAI.IsValid)