Skip to content

Commit ea3a6a4

Browse files
committed
feature: auto focus the next change after stage/unstage selected changes (#464)
1 parent dcddc5a commit ea3a6a4

File tree

4 files changed

+122
-22
lines changed

4 files changed

+122
-22
lines changed

src/ViewModels/WorkingCopy.cs

Lines changed: 18 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -325,23 +325,24 @@ public void StashAll(bool autoStart)
325325
PopupHost.ShowPopup(new StashChanges(_repo, _cached, true));
326326
}
327327

328-
public void StageSelected()
328+
public void StageSelected(Models.Change next)
329329
{
330-
StageChanges(_selectedUnstaged);
331-
SelectedUnstaged = [];
330+
StageChanges(_selectedUnstaged, next);
332331
}
333332

334333
public void StageAll()
335334
{
336-
StageChanges(_unstaged);
337-
SelectedUnstaged = [];
335+
StageChanges(_unstaged, null);
338336
}
339337

340-
public async void StageChanges(List<Models.Change> changes)
338+
public async void StageChanges(List<Models.Change> changes, Models.Change next)
341339
{
342340
if (_unstaged.Count == 0 || changes.Count == 0)
343341
return;
344342

343+
// Use `_selectedUnstaged` instead of `SelectedUnstaged` to avoid UI refresh.
344+
_selectedUnstaged = next != null ? [next] : [];
345+
345346
IsStaging = true;
346347
_repo.SetWatcherEnabled(false);
347348
if (changes.Count == _unstaged.Count)
@@ -362,23 +363,24 @@ public async void StageChanges(List<Models.Change> changes)
362363
IsStaging = false;
363364
}
364365

365-
public void UnstageSelected()
366+
public void UnstageSelected(Models.Change next)
366367
{
367-
UnstageChanges(_selectedStaged);
368-
SelectedStaged = [];
368+
UnstageChanges(_selectedStaged, next);
369369
}
370370

371371
public void UnstageAll()
372372
{
373-
UnstageChanges(_staged);
374-
SelectedStaged = [];
373+
UnstageChanges(_staged, null);
375374
}
376375

377-
public async void UnstageChanges(List<Models.Change> changes)
376+
public async void UnstageChanges(List<Models.Change> changes, Models.Change next)
378377
{
379378
if (_staged.Count == 0 || changes.Count == 0)
380379
return;
381380

381+
// Use `_selectedStaged` instead of `SelectedStaged` to avoid UI refresh.
382+
_selectedStaged = next != null ? [next] : [];
383+
382384
IsUnstaging = true;
383385
_repo.SetWatcherEnabled(false);
384386
if (_useAmend)
@@ -499,7 +501,7 @@ public ContextMenu CreateContextMenuForUnstagedChanges()
499501
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
500502
stage.Click += (_, e) =>
501503
{
502-
StageChanges(_selectedUnstaged);
504+
StageChanges(_selectedUnstaged, null);
503505
e.Handled = true;
504506
};
505507

@@ -823,7 +825,7 @@ public ContextMenu CreateContextMenuForUnstagedChanges()
823825
stage.Icon = App.CreateMenuIcon("Icons.File.Add");
824826
stage.Click += (_, e) =>
825827
{
826-
StageChanges(_selectedUnstaged);
828+
StageChanges(_selectedUnstaged, null);
827829
e.Handled = true;
828830
};
829831

@@ -917,7 +919,7 @@ public ContextMenu CreateContextMenuForStagedChanges()
917919
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
918920
unstage.Click += (_, e) =>
919921
{
920-
UnstageChanges(_selectedStaged);
922+
UnstageChanges(_selectedStaged, null);
921923
e.Handled = true;
922924
};
923925

@@ -1086,7 +1088,7 @@ public ContextMenu CreateContextMenuForStagedChanges()
10861088
unstage.Icon = App.CreateMenuIcon("Icons.File.Remove");
10871089
unstage.Click += (_, e) =>
10881090
{
1089-
UnstageChanges(_selectedStaged);
1091+
UnstageChanges(_selectedStaged, null);
10901092
e.Handled = true;
10911093
};
10921094

src/Views/ChangeCollectionView.axaml.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,7 @@ public event EventHandler<RoutedEventArgs> ChangeDoubleTapped
9696

9797
public ChangeCollectionView()
9898
{
99+
Focusable = true;
99100
InitializeComponent();
100101
}
101102

@@ -132,6 +133,69 @@ public void ToggleNodeIsExpanded(ViewModels.ChangeTreeNode node)
132133
}
133134
}
134135

136+
public Models.Change GetNextChangeWithoutSelection()
137+
{
138+
var selected = SelectedChanges;
139+
var changes = Changes;
140+
if (selected == null || selected.Count == 0)
141+
return changes.Count > 0 ? changes[0] : null;
142+
if (selected.Count == changes.Count)
143+
return null;
144+
145+
var set = new HashSet<string>();
146+
foreach (var c in selected)
147+
set.Add(c.Path);
148+
149+
if (Content is ViewModels.ChangeCollectionAsTree tree)
150+
{
151+
var lastUnselected = -1;
152+
for (int i = tree.Rows.Count - 1; i >= 0; i--)
153+
{
154+
var row = tree.Rows[i];
155+
if (!row.IsFolder)
156+
{
157+
if (set.Contains(row.FullPath))
158+
{
159+
if (lastUnselected == -1)
160+
continue;
161+
else
162+
break;
163+
}
164+
else
165+
{
166+
lastUnselected = i;
167+
}
168+
}
169+
}
170+
171+
if (lastUnselected != -1)
172+
return tree.Rows[lastUnselected].Change;
173+
}
174+
else
175+
{
176+
var lastUnselected = -1;
177+
for (int i = changes.Count - 1; i >= 0; i--)
178+
{
179+
if (set.Contains(changes[i].Path))
180+
{
181+
if (lastUnselected == -1)
182+
continue;
183+
else
184+
break;
185+
}
186+
else
187+
{
188+
lastUnselected = i;
189+
}
190+
}
191+
192+
if (lastUnselected != -1)
193+
return changes[lastUnselected];
194+
}
195+
196+
return null;
197+
}
198+
135199
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
136200
{
137201
base.OnPropertyChanged(change);

src/Views/WorkingCopy.axaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
Classes="icon_button"
4343
Width="26" Height="14"
4444
Padding="0"
45-
Command="{Binding StageSelected}">
45+
Click="OnStageSelectedButtonClicked">
4646
<ToolTip.Tip>
4747
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
4848
<TextBlock Text="{DynamicResource Text.WorkingCopy.Unstaged.Stage}" VerticalAlignment="Center"/>
@@ -64,6 +64,7 @@
6464

6565
<!-- Unstaged Changes -->
6666
<v:ChangeCollectionView Grid.Row="1"
67+
x:Name="UnstagedChangesView"
6768
IsUnstagedChange="True"
6869
SelectionMode="Multiple"
6970
Background="{DynamicResource Brush.Contents}"
@@ -81,7 +82,7 @@
8182
<TextBlock Grid.Column="1" Text="{DynamicResource Text.WorkingCopy.Staged}" Foreground="{DynamicResource Brush.FG2}" FontWeight="Bold" Margin="8,0,0,0"/>
8283
<TextBlock Grid.Column="2" FontWeight="Bold" Foreground="{DynamicResource Brush.FG2}" Text="{Binding Staged, Converter={x:Static c:ListConverters.ToCount}}"/>
8384
<v:LoadingIcon Grid.Column="3" Width="14" Height="14" Margin="8,0,0,0" IsVisible="{Binding IsUnstaging}"/>
84-
<Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" Command="{Binding UnstageSelected}">
85+
<Button Grid.Column="5" Classes="icon_button" Width="26" Height="14" Padding="0" Click="OnUnstageSelectedButtonClicked">
8586
<ToolTip.Tip>
8687
<StackPanel Orientation="Horizontal" VerticalAlignment="Center">
8788
<TextBlock Text="{DynamicResource Text.WorkingCopy.Staged.Unstage}" VerticalAlignment="Center"/>
@@ -98,6 +99,7 @@
9899

99100
<!-- Staged Changes -->
100101
<v:ChangeCollectionView Grid.Row="3"
102+
x:Name="StagedChangesView"
101103
SelectionMode="Multiple"
102104
Background="{DynamicResource Brush.Contents}"
103105
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=StagedChangeViewMode}"

src/Views/WorkingCopy.axaml.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,9 @@ private void OnUnstagedChangeDoubleTapped(object _, RoutedEventArgs e)
4646
{
4747
if (DataContext is ViewModels.WorkingCopy vm)
4848
{
49-
vm.StageSelected();
49+
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
50+
vm.StageSelected(next);
51+
UnstagedChangesView.Focus();
5052
e.Handled = true;
5153
}
5254
}
@@ -55,7 +57,9 @@ private void OnStagedChangeDoubleTapped(object _, RoutedEventArgs e)
5557
{
5658
if (DataContext is ViewModels.WorkingCopy vm)
5759
{
58-
vm.UnstageSelected();
60+
var next = StagedChangesView.GetNextChangeWithoutSelection();
61+
vm.UnstageSelected(next);
62+
StagedChangesView.Focus();
5963
e.Handled = true;
6064
}
6165
}
@@ -66,7 +70,9 @@ private void OnUnstagedKeyDown(object _, KeyEventArgs e)
6670
{
6771
if (e.Key is Key.Space or Key.Enter)
6872
{
69-
vm.StageSelected();
73+
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
74+
vm.StageSelected(next);
75+
UnstagedChangesView.Focus();
7076
e.Handled = true;
7177
return;
7278
}
@@ -84,11 +90,37 @@ private void OnStagedKeyDown(object _, KeyEventArgs e)
8490
{
8591
if (DataContext is ViewModels.WorkingCopy vm && e.Key is Key.Space or Key.Enter)
8692
{
87-
vm.UnstageSelected();
93+
var next = StagedChangesView.GetNextChangeWithoutSelection();
94+
vm.UnstageSelected(next);
95+
StagedChangesView.Focus();
8896
e.Handled = true;
8997
}
9098
}
9199

100+
private void OnStageSelectedButtonClicked(object _, RoutedEventArgs e)
101+
{
102+
if (DataContext is ViewModels.WorkingCopy vm)
103+
{
104+
var next = UnstagedChangesView.GetNextChangeWithoutSelection();
105+
vm.StageSelected(next);
106+
UnstagedChangesView.Focus();
107+
}
108+
109+
e.Handled = true;
110+
}
111+
112+
private void OnUnstageSelectedButtonClicked(object _, RoutedEventArgs e)
113+
{
114+
if (DataContext is ViewModels.WorkingCopy vm)
115+
{
116+
var next = StagedChangesView.GetNextChangeWithoutSelection();
117+
vm.UnstageSelected(next);
118+
StagedChangesView.Focus();
119+
}
120+
121+
e.Handled = true;
122+
}
123+
92124
private void OnOpenAIAssist(object _, RoutedEventArgs e)
93125
{
94126
if (!Models.OpenAI.IsValid)

0 commit comments

Comments
 (0)