Skip to content

Commit 6d8bbc3

Browse files
committed
feat: add drag-to-reorder for sidebar view switcher sections
Allow users to reorder History, Working Copy, and Stashes sections in the sidebar via drag-and-drop. Order persists across sessions.
1 parent 4f11be1 commit 6d8bbc3

File tree

5 files changed

+251
-22
lines changed

5 files changed

+251
-22
lines changed

src/App.JsonCodeGen.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ public override void Write(Utf8JsonWriter writer, DataGridLength value, JsonSeri
6666
[JsonSerializable(typeof(Models.ThemeOverrides))]
6767
[JsonSerializable(typeof(Models.Version))]
6868
[JsonSerializable(typeof(Models.RepositorySettings))]
69+
[JsonSerializable(typeof(List<int>))]
6970
[JsonSerializable(typeof(List<Models.ConventionalCommitType>))]
7071
[JsonSerializable(typeof(List<Models.LFSLock>))]
7172
[JsonSerializable(typeof(List<Models.VisualStudioInstance>))]

src/Resources/Icons.axaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
<ResourceDictionary xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
2+
<StreamGeometry x:Key="Icons.GripVertical">M384 192a64 64 0 110 128 64 64 0 010-128zm256 0a64 64 0 110 128 64 64 0 010-128zM384 448a64 64 0 110 128 64 64 0 010-128zm256 0a64 64 0 110 128 64 64 0 010-128zM384 704a64 64 0 110 128 64 64 0 010-128zm256 0a64 64 0 110 128 64 64 0 010-128z</StreamGeometry>
23
<StreamGeometry x:Key="Icons.Action">M41 512c0-128 46-241 138-333C271 87 384 41 512 41s241 46 333 138c92 92 138 205 138 333s-46 241-138 333c-92 92-205 138-333 138s-241-46-333-138C87 753 41 640 41 512zm87 0c0 108 36 195 113 271s164 113 271 113c108 0 195-36 271-113s113-164 113-271-36-195-113-271c-77-77-164-113-271-113-108 0-195 36-271 113C164 317 128 404 128 512zm256 148V292l195 113L768 512l-195 113-195 113v-77zm148-113-61 36V440l61 36 61 36-61 36z</StreamGeometry>
34
<StreamGeometry x:Key="Icons.AIAssist">M304 464a128 128 0 01128-128c71 0 128 57 128 128v224a32 32 0 01-64 0V592h-128v95a32 32 0 01-64 0v-224zm64 1v64h128v-64a64 64 0 00-64-64c-35 0-64 29-64 64zM688 337c18 0 32 14 32 32v319a32 32 0 01-32 32c-18 0-32-14-32-32v-319a32 32 0 0132-32zM84 911l60-143A446 446 0 0164 512C64 265 265 64 512 64s448 201 448 448-201 448-448 448c-54 0-105-9-153-27l-242 22a32 32 0 01-32-44zm133-150-53 126 203-18 13 5c41 15 85 23 131 23 212 0 384-172 384-384S724 128 512 128 128 300 128 512c0 82 26 157 69 220l20 29z</StreamGeometry>
45
<StreamGeometry x:Key="Icons.Archive">M366 146l293 0 0-73-293 0 0 73zm658 366 0 274q0 38-27 65t-65 27l-841 0q-38 0-65-27t-27-65l0-274 384 0 0 91q0 15 11 26t26 11l183 0q15 0 26-11t11-26l0-91 384 0zm-439 0 0 73-146 0 0-73 146 0zm439-274 0 219-1024 0 0-219q0-38 27-65t65-27l201 0 0-91q0-23 16-39t39-16l329 0q23 0 39 16t16 39l0 91 201 0q38 0 65 27t27 65z</StreamGeometry>

src/ViewModels/LayoutInfo.cs

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
using Avalonia.Controls;
1+
using System.Collections.Generic;
2+
3+
using Avalonia.Controls;
24
using CommunityToolkit.Mvvm.ComponentModel;
35

46
namespace SourceGit.ViewModels
@@ -76,6 +78,13 @@ public DataGridLength AuthorColumnWidth
7678
private GridLength _stashesLeftWidth = new GridLength(300, GridUnitType.Pixel);
7779
private GridLength _commitDetailChangesLeftWidth = new GridLength(256, GridUnitType.Pixel);
7880
private GridLength _commitDetailFilesLeftWidth = new GridLength(256, GridUnitType.Pixel);
81+
public List<int> SidebarViewOrder
82+
{
83+
get => _sidebarViewOrder;
84+
set => SetProperty(ref _sidebarViewOrder, value);
85+
}
86+
7987
private DataGridLength _authorColumnWidth = new DataGridLength(120, DataGridLengthUnitType.Pixel, 120, 120);
88+
private List<int> _sidebarViewOrder = new List<int> { 0, 1, 2 };
8089
}
8190
}

src/Views/Repository.axaml

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,7 @@
5454
<!-- Page Switcher for Right Panel -->
5555
<Border Grid.Row="0" Margin="8,0,4,0" BorderThickness="1" BorderBrush="{DynamicResource Brush.Border2}" CornerRadius="6">
5656
<Border CornerRadius="6" ClipToBounds="True">
57-
<ListBox Background="Transparent" SelectedIndex="{Binding SelectedViewIndex, Mode=TwoWay}" SelectionMode="AlwaysSelected">
57+
<ListBox x:Name="ViewSwitcher" Background="Transparent" SelectionMode="AlwaysSelected" SelectionChanged="OnViewSwitcherSelectionChanged">
5858
<ListBox.Styles>
5959
<Style Selector="Path.icon">
6060
<Setter Property="Width" Value="12"/>
@@ -96,28 +96,37 @@
9696
</ItemsPanelTemplate>
9797
</ListBox.ItemsPanel>
9898

99-
<ListBoxItem>
100-
<Grid Classes="view_mode" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
101-
<Path Grid.Column="0" Classes="icon" Data="{StaticResource Icons.Histories}"/>
102-
<TextBlock Grid.Column="1" Classes="header" Text="{DynamicResource Text.Histories}"/>
99+
<ListBoxItem Tag="0"
100+
DragDrop.AllowDrop="True"
101+
DragDrop.Drop="OnViewSwitcherItemDrop">
102+
<Grid Classes="view_mode" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
103+
<Border Grid.Column="0"
104+
Width="16" Background="Transparent" Cursor="SizeAll"
105+
PointerPressed="OnViewSwitcherItemPointerPressed"
106+
PointerMoved="OnViewSwitcherItemPointerMoved"
107+
PointerReleased="OnViewSwitcherItemPointerReleased">
108+
<Path Width="8" Height="12" Stretch="Uniform" Data="{StaticResource Icons.GripVertical}" Fill="{DynamicResource Brush.FG2}" Opacity="0.5"/>
109+
</Border>
110+
<Path Grid.Column="1" Classes="icon" Data="{StaticResource Icons.Histories}"/>
111+
<TextBlock Grid.Column="2" Classes="header" Text="{DynamicResource Text.Histories}"/>
103112

104-
<ToggleButton Grid.Column="2"
113+
<ToggleButton Grid.Column="3"
105114
Classes="line_path"
106115
Width="26" Height="26"
107116
Background="Transparent"
108117
IsChecked="{Binding OnlyHighlightCurrentBranchInHistories, Mode=TwoWay}"
109118
ToolTip.Tip="{DynamicResource Text.Repository.OnlyHighlightCurrentBranchInGraph}">
110119
<Path Width="12" Height="12" Data="{StaticResource Icons.LightOn}"/>
111120
</ToggleButton>
112-
<ToggleButton Grid.Column="3"
121+
<ToggleButton Grid.Column="4"
113122
Classes="line_path"
114123
Width="26" Height="26"
115124
Background="Transparent"
116125
IsChecked="{Binding Source={x:Static vm:Preferences.Instance}, Path=DisplayTimeAsPeriodInHistories, Mode=TwoWay}"
117126
ToolTip.Tip="{DynamicResource Text.Repository.UseRelativeTimeInGraph}">
118127
<Path Width="12" Height="12" Data="{StaticResource Icons.Stopwatch}"/>
119128
</ToggleButton>
120-
<Button Grid.Column="4"
129+
<Button Grid.Column="5"
121130
Classes="icon_button"
122131
Width="26" Height="26"
123132
Click="OnOpenAdvancedHistoriesOption"
@@ -127,11 +136,21 @@
127136
</Grid>
128137
</ListBoxItem>
129138

130-
<ListBoxItem IsVisible="{Binding !IsBare}">
131-
<Grid Classes="view_mode" ColumnDefinitions="Auto,*,Auto,Auto,Auto">
132-
<Path Grid.Column="0" Classes="icon" Data="{StaticResource Icons.Changes}"/>
133-
<TextBlock Grid.Column="1" Classes="header" Text="{DynamicResource Text.WorkingCopy}"/>
134-
<Border Grid.Column="2"
139+
<ListBoxItem Tag="1"
140+
IsVisible="{Binding !IsBare}"
141+
DragDrop.AllowDrop="True"
142+
DragDrop.Drop="OnViewSwitcherItemDrop">
143+
<Grid Classes="view_mode" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
144+
<Border Grid.Column="0"
145+
Width="16" Background="Transparent" Cursor="SizeAll"
146+
PointerPressed="OnViewSwitcherItemPointerPressed"
147+
PointerMoved="OnViewSwitcherItemPointerMoved"
148+
PointerReleased="OnViewSwitcherItemPointerReleased">
149+
<Path Width="8" Height="12" Stretch="Uniform" Data="{StaticResource Icons.GripVertical}" Fill="{DynamicResource Brush.FG2}" Opacity="0.5"/>
150+
</Border>
151+
<Path Grid.Column="1" Classes="icon" Data="{StaticResource Icons.Changes}"/>
152+
<TextBlock Grid.Column="2" Classes="header" Text="{DynamicResource Text.WorkingCopy}"/>
153+
<Border Grid.Column="3"
135154
Height="18"
136155
Margin="6,0" Padding="9,0"
137156
CornerRadius="9"
@@ -143,13 +162,13 @@
143162
FontFamily="{DynamicResource Fonts.Monospace}"
144163
FontSize="10"/>
145164
</Border>
146-
<Path Grid.Column="3"
165+
<Path Grid.Column="4"
147166
Width="12" Height="12"
148167
Margin="0,0,6,0"
149168
Data="{StaticResource Icons.Info}"
150169
Fill="DarkOrange"
151170
IsVisible="{Binding InProgressContext, Converter={x:Static ObjectConverters.IsNotNull}}"/>
152-
<Button Grid.Column="4"
171+
<Button Grid.Column="5"
153172
Classes="icon_button"
154173
Width="26" Height="26"
155174
Command="{Binding DiscardAllChanges}"
@@ -159,11 +178,21 @@
159178
</Grid>
160179
</ListBoxItem>
161180

162-
<ListBoxItem IsVisible="{Binding !IsBare}">
163-
<Grid Classes="view_mode" ColumnDefinitions="Auto,*,Auto,Auto">
164-
<Path Grid.Column="0" Classes="icon" Data="{StaticResource Icons.Stashes}"/>
165-
<TextBlock Grid.Column="1" Classes="header" Text="{DynamicResource Text.Stashes}"/>
166-
<Border Grid.Column="2"
181+
<ListBoxItem Tag="2"
182+
IsVisible="{Binding !IsBare}"
183+
DragDrop.AllowDrop="True"
184+
DragDrop.Drop="OnViewSwitcherItemDrop">
185+
<Grid Classes="view_mode" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
186+
<Border Grid.Column="0"
187+
Width="16" Background="Transparent" Cursor="SizeAll"
188+
PointerPressed="OnViewSwitcherItemPointerPressed"
189+
PointerMoved="OnViewSwitcherItemPointerMoved"
190+
PointerReleased="OnViewSwitcherItemPointerReleased">
191+
<Path Width="8" Height="12" Stretch="Uniform" Data="{StaticResource Icons.GripVertical}" Fill="{DynamicResource Brush.FG2}" Opacity="0.5"/>
192+
</Border>
193+
<Path Grid.Column="1" Classes="icon" Data="{StaticResource Icons.Stashes}"/>
194+
<TextBlock Grid.Column="2" Classes="header" Text="{DynamicResource Text.Stashes}"/>
195+
<Border Grid.Column="3"
167196
Height="18"
168197
Margin="6,0" Padding="9,0"
169198
CornerRadius="9"
@@ -175,7 +204,7 @@
175204
FontFamily="{DynamicResource Fonts.Monospace}"
176205
FontSize="10"/>
177206
</Border>
178-
<Button Grid.Column="3"
207+
<Button Grid.Column="4"
179208
Classes="icon_button"
180209
Width="26" Height="26"
181210
Command="{Binding ClearStashes}"

src/Views/Repository.axaml.cs

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
24

35
using Avalonia;
46
using Avalonia.Controls;
@@ -17,9 +19,196 @@ public Repository()
1719
protected override void OnLoaded(RoutedEventArgs e)
1820
{
1921
base.OnLoaded(e);
22+
ApplySidebarViewOrder();
23+
SyncViewSwitcherSelection();
2024
UpdateLeftSidebarLayout();
25+
26+
if (DataContext is ViewModels.Repository repo)
27+
repo.PropertyChanged += OnRepositoryPropertyChanged;
2128
}
2229

30+
protected override void OnUnloaded(RoutedEventArgs e)
31+
{
32+
base.OnUnloaded(e);
33+
34+
if (DataContext is ViewModels.Repository repo)
35+
repo.PropertyChanged -= OnRepositoryPropertyChanged;
36+
}
37+
38+
private void OnRepositoryPropertyChanged(object sender, System.ComponentModel.PropertyChangedEventArgs e)
39+
{
40+
if (e.PropertyName == nameof(ViewModels.Repository.SelectedViewIndex))
41+
SyncViewSwitcherSelection();
42+
}
43+
44+
#region View Switcher Drag-Drop Reorder
45+
46+
private static readonly DataFormat<string> _dndViewSwitcherFormat =
47+
DataFormat.CreateStringApplicationFormat("sourcegit-dnd-view-switcher");
48+
49+
private bool _pressedViewItem = false;
50+
private bool _startDragViewItem = false;
51+
private Point _pressedViewItemPosition;
52+
53+
private void ApplySidebarViewOrder()
54+
{
55+
var layout = ViewModels.Preferences.Instance.Layout;
56+
var order = layout.SidebarViewOrder;
57+
if (order == null || order.Count != 3)
58+
return;
59+
60+
// Collect items by their Tag
61+
var itemsByTag = new Dictionary<int, ListBoxItem>();
62+
foreach (var obj in ViewSwitcher.Items)
63+
{
64+
if (obj is ListBoxItem item && item.Tag is string tagStr && int.TryParse(tagStr, out var tag))
65+
itemsByTag[tag] = item;
66+
}
67+
68+
if (itemsByTag.Count != 3)
69+
return;
70+
71+
// Reorder
72+
ViewSwitcher.Items.Clear();
73+
foreach (var tag in order)
74+
{
75+
if (itemsByTag.TryGetValue(tag, out var item))
76+
ViewSwitcher.Items.Add(item);
77+
}
78+
}
79+
80+
private void SyncViewSwitcherSelection()
81+
{
82+
if (DataContext is not ViewModels.Repository repo)
83+
return;
84+
85+
// Find the ListBoxItem whose Tag matches the current SelectedViewIndex
86+
foreach (var obj in ViewSwitcher.Items)
87+
{
88+
if (obj is ListBoxItem item && item.Tag is string tagStr &&
89+
int.TryParse(tagStr, out var tag) && tag == repo.SelectedViewIndex)
90+
{
91+
_suppressSelectionChanged = true;
92+
ViewSwitcher.SelectedItem = item;
93+
_suppressSelectionChanged = false;
94+
break;
95+
}
96+
}
97+
}
98+
99+
private bool _suppressSelectionChanged = false;
100+
101+
private void OnViewSwitcherSelectionChanged(object sender, SelectionChangedEventArgs e)
102+
{
103+
if (_suppressSelectionChanged)
104+
return;
105+
106+
if (DataContext is ViewModels.Repository repo &&
107+
ViewSwitcher.SelectedItem is ListBoxItem item &&
108+
item.Tag is string tagStr &&
109+
int.TryParse(tagStr, out var viewIndex))
110+
{
111+
repo.SelectedViewIndex = viewIndex;
112+
}
113+
}
114+
115+
private static ListBoxItem FindParentListBoxItem(Control control)
116+
{
117+
var parent = control.Parent;
118+
while (parent != null)
119+
{
120+
if (parent is ListBoxItem item)
121+
return item;
122+
parent = (parent as Control)?.Parent;
123+
}
124+
return null;
125+
}
126+
127+
private void OnViewSwitcherItemPointerPressed(object sender, PointerPressedEventArgs e)
128+
{
129+
if (sender is Border border)
130+
{
131+
_pressedViewItem = true;
132+
_startDragViewItem = false;
133+
_pressedViewItemPosition = e.GetPosition(border);
134+
e.Handled = true;
135+
}
136+
}
137+
138+
private void OnViewSwitcherItemPointerReleased(object sender, PointerReleasedEventArgs e)
139+
{
140+
_pressedViewItem = false;
141+
_startDragViewItem = false;
142+
}
143+
144+
private async void OnViewSwitcherItemPointerMoved(object sender, PointerEventArgs e)
145+
{
146+
if (!_pressedViewItem || _startDragViewItem || sender is not Border border)
147+
return;
148+
149+
var delta = e.GetPosition(border) - _pressedViewItemPosition;
150+
var sizeSquared = delta.X * delta.X + delta.Y * delta.Y;
151+
if (sizeSquared < 64)
152+
return;
153+
154+
_startDragViewItem = true;
155+
156+
var listBoxItem = FindParentListBoxItem(border);
157+
if (listBoxItem == null)
158+
return;
159+
160+
var tag = listBoxItem.Tag as string ?? "";
161+
var data = new DataTransfer();
162+
data.Add(DataTransferItem.Create(_dndViewSwitcherFormat, tag));
163+
await DragDrop.DoDragDropAsync(e, data, DragDropEffects.Move);
164+
}
165+
166+
private void OnViewSwitcherItemDrop(object sender, DragEventArgs e)
167+
{
168+
if (e.DataTransfer.TryGetValue(_dndViewSwitcherFormat) is not { Length: > 0 } sourceTag)
169+
return;
170+
171+
if (sender is not ListBoxItem targetItem || targetItem.Tag is not string targetTag)
172+
return;
173+
174+
if (sourceTag == targetTag)
175+
return;
176+
177+
// Find source and target indices in current Items list
178+
var items = ViewSwitcher.Items.Cast<ListBoxItem>().ToList();
179+
var sourceIdx = items.FindIndex(i => (i.Tag as string) == sourceTag);
180+
var targetIdx = items.FindIndex(i => (i.Tag as string) == targetTag);
181+
182+
if (sourceIdx < 0 || targetIdx < 0)
183+
return;
184+
185+
// Swap
186+
var sourceItem = items[sourceIdx];
187+
items.RemoveAt(sourceIdx);
188+
items.Insert(targetIdx, sourceItem);
189+
190+
// Rebuild ListBox
191+
_suppressSelectionChanged = true;
192+
var selectedItem = ViewSwitcher.SelectedItem;
193+
ViewSwitcher.Items.Clear();
194+
foreach (var item in items)
195+
ViewSwitcher.Items.Add(item);
196+
ViewSwitcher.SelectedItem = selectedItem;
197+
_suppressSelectionChanged = false;
198+
199+
// Persist order
200+
var newOrder = items.Select(i => int.Parse(i.Tag as string ?? "0")).ToList();
201+
var layout = ViewModels.Preferences.Instance.Layout;
202+
layout.SidebarViewOrder = newOrder;
203+
ViewModels.Preferences.Instance.Save();
204+
205+
_pressedViewItem = false;
206+
_startDragViewItem = false;
207+
e.Handled = true;
208+
}
209+
210+
#endregion
211+
23212
private void OnSearchCommitPanelPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
24213
{
25214
if (e.Property == IsVisibleProperty && sender is Grid { IsVisible: true })

0 commit comments

Comments
 (0)