refactor: remove dependency on Avalonia.Controls.TreeDataGrid

This commit is contained in:
leo 2024-07-12 17:14:52 +08:00
parent 7f228385f9
commit 1c204e72a1
No known key found for this signature in database
10 changed files with 403 additions and 741 deletions

View file

@ -20,7 +20,6 @@
<Application.Styles> <Application.Styles>
<FluentTheme /> <FluentTheme />
<StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/> <StyleInclude Source="avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml"/>
<StyleInclude Source="avares://Avalonia.Controls.TreeDataGrid/Themes/Fluent.axaml"/>
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" /> <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="/Resources/Styles.axaml"/> <StyleInclude Source="/Resources/Styles.axaml"/>
</Application.Styles> </Application.Styles>

View file

@ -1,462 +0,0 @@
using System;
using System.Collections;
using System.Collections.Generic;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Selection;
using Avalonia.Input;
namespace SourceGit.Models
{
public class TreeDataGridSelectionModel<TModel> : TreeSelectionModelBase<TModel>,
ITreeDataGridRowSelectionModel<TModel>,
ITreeDataGridSelectionInteraction
where TModel : class
{
private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity);
private readonly ITreeDataGridSource<TModel> _source;
private EventHandler _viewSelectionChanged;
private EventHandler _rowDoubleTapped;
private Point _pressedPoint = s_InvalidPoint;
private bool _raiseViewSelectionChanged;
private Func<TModel, IEnumerable<TModel>> _childrenGetter;
public TreeDataGridSelectionModel(ITreeDataGridSource<TModel> source, Func<TModel, IEnumerable<TModel>> childrenGetter)
: base(source.Items)
{
_source = source;
_childrenGetter = childrenGetter;
SelectionChanged += (s, e) =>
{
if (!IsSourceCollectionChanging)
_viewSelectionChanged?.Invoke(this, e);
else
_raiseViewSelectionChanged = true;
};
}
public void Select(IEnumerable<TModel> items)
{
using (BatchUpdate())
{
Clear();
foreach (var selected in items)
{
var idx = GetModelIndex(_source.Items, selected, IndexPath.Unselected);
if (!idx.Equals(IndexPath.Unselected))
Select(idx);
}
}
}
event EventHandler ITreeDataGridSelectionInteraction.SelectionChanged
{
add => _viewSelectionChanged += value;
remove => _viewSelectionChanged -= value;
}
public event EventHandler RowDoubleTapped
{
add => _rowDoubleTapped += value;
remove => _rowDoubleTapped -= value;
}
IEnumerable ITreeDataGridSelection.Source
{
get => Source;
set => Source = value;
}
bool ITreeDataGridSelectionInteraction.IsRowSelected(IRow rowModel)
{
if (rowModel is IModelIndexableRow indexable)
return IsSelected(indexable.ModelIndexPath);
return false;
}
bool ITreeDataGridSelectionInteraction.IsRowSelected(int rowIndex)
{
if (rowIndex >= 0 && rowIndex < _source.Rows.Count)
{
if (_source.Rows[rowIndex] is IModelIndexableRow indexable)
return IsSelected(indexable.ModelIndexPath);
}
return false;
}
void ITreeDataGridSelectionInteraction.OnKeyDown(TreeDataGrid sender, KeyEventArgs e)
{
if (sender.RowsPresenter is null)
return;
if (!e.Handled)
{
var ctrl = e.KeyModifiers.HasFlag(KeyModifiers.Control);
if (e.Key == Key.A && ctrl && !SingleSelect)
{
using (BatchUpdate())
{
Clear();
int num = _source.Rows.Count;
for (int i = 0; i < num; ++i)
{
var m = _source.Rows.RowIndexToModelIndex(i);
Select(m);
}
}
e.Handled = true;
}
var direction = e.Key.ToNavigationDirection();
var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
if (direction.HasValue)
{
var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(AnchorIndex);
sender.RowsPresenter.BringIntoView(anchorRowIndex);
var anchor = sender.TryGetRow(anchorRowIndex);
if (anchor is not null && !ctrl)
{
e.Handled = TryKeyExpandCollapse(sender, direction.Value, anchor);
}
if (!e.Handled && (!ctrl || shift))
{
e.Handled = MoveSelection(sender, direction.Value, shift, anchor);
}
if (!e.Handled && direction == NavigationDirection.Left
&& anchor?.Rows is HierarchicalRows<TModel> hierarchicalRows && anchorRowIndex > 0)
{
var newIndex = hierarchicalRows.GetParentRowIndex(AnchorIndex);
UpdateSelection(sender, newIndex, true);
FocusRow(sender, sender.RowsPresenter.BringIntoView(newIndex));
}
if (!e.Handled && direction == NavigationDirection.Right
&& anchor?.Rows is HierarchicalRows<TModel> hierarchicalRows2 && hierarchicalRows2[anchorRowIndex].IsExpanded)
{
var newIndex = anchorRowIndex + 1;
UpdateSelection(sender, newIndex, true);
sender.RowsPresenter.BringIntoView(newIndex);
}
}
}
}
void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e)
{
if (!e.Handled &&
e.Pointer.Type == PointerType.Mouse &&
e.Source is Control source &&
sender.TryGetRow(source, out var row) &&
_source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex)
{
if (!IsSelected(modelIndex))
{
PointerSelect(sender, row, e);
_pressedPoint = s_InvalidPoint;
}
else
{
var point = e.GetCurrentPoint(sender);
if (point.Properties.IsRightButtonPressed)
{
_pressedPoint = s_InvalidPoint;
return;
}
if (e.KeyModifiers == KeyModifiers.Control)
{
Deselect(modelIndex);
}
else if (e.ClickCount % 2 == 0)
{
var focus = _source.Rows[row.RowIndex];
if (focus is IExpander expander && HasChildren(focus))
expander.IsExpanded = !expander.IsExpanded;
else
_rowDoubleTapped?.Invoke(this, e);
e.Handled = true;
}
else if (sender.RowSelection.Count > 1)
{
using (BatchUpdate())
{
Clear();
Select(modelIndex);
}
}
_pressedPoint = s_InvalidPoint;
}
}
else
{
if (!sender.TryGetRow(e.Source as Control, out var test))
Clear();
_pressedPoint = e.GetPosition(sender);
}
}
void ITreeDataGridSelectionInteraction.OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e)
{
if (!e.Handled &&
_pressedPoint != s_InvalidPoint &&
e.Source is Control source &&
sender.TryGetRow(source, out var row) &&
_source.Rows.RowIndexToModelIndex(row.RowIndex) is { } modelIndex)
{
if (!IsSelected(modelIndex))
{
var p = e.GetPosition(sender);
if (Math.Abs(p.X - _pressedPoint.X) <= 3 || Math.Abs(p.Y - _pressedPoint.Y) <= 3)
PointerSelect(sender, row, e);
}
}
}
protected override void OnSourceCollectionChangeFinished()
{
if (_raiseViewSelectionChanged)
{
_viewSelectionChanged?.Invoke(this, EventArgs.Empty);
_raiseViewSelectionChanged = false;
}
}
private void PointerSelect(TreeDataGrid sender, TreeDataGridRow row, PointerEventArgs e)
{
var point = e.GetCurrentPoint(sender);
var commandModifiers = TopLevel.GetTopLevel(sender)?.PlatformSettings?.HotkeyConfiguration.CommandModifiers;
var toggleModifier = commandModifiers is not null && e.KeyModifiers.HasFlag(commandModifiers);
var isRightButton = point.Properties.PointerUpdateKind is PointerUpdateKind.RightButtonPressed or
PointerUpdateKind.RightButtonReleased;
UpdateSelection(
sender,
row.RowIndex,
select: true,
rangeModifier: e.KeyModifiers.HasFlag(KeyModifiers.Shift),
toggleModifier: toggleModifier,
rightButton: isRightButton);
e.Handled = true;
}
private void UpdateSelection(TreeDataGrid treeDataGrid, int rowIndex, bool select = true, bool rangeModifier = false, bool toggleModifier = false, bool rightButton = false)
{
var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex);
if (modelIndex == default)
return;
var mode = SingleSelect ? SelectionMode.Single : SelectionMode.Multiple;
var multi = (mode & SelectionMode.Multiple) != 0;
var toggle = (toggleModifier || (mode & SelectionMode.Toggle) != 0);
var range = multi && rangeModifier;
if (!select)
{
if (IsSelected(modelIndex) && !treeDataGrid.QueryCancelSelection())
Deselect(modelIndex);
}
else if (rightButton)
{
if (IsSelected(modelIndex) == false && !treeDataGrid.QueryCancelSelection())
SelectedIndex = modelIndex;
}
else if (range)
{
if (!treeDataGrid.QueryCancelSelection())
{
var anchor = RangeAnchorIndex;
var i = Math.Max(_source.Rows.ModelIndexToRowIndex(anchor), 0);
var step = i < rowIndex ? 1 : -1;
using (BatchUpdate())
{
Clear();
while (true)
{
var m = _source.Rows.RowIndexToModelIndex(i);
Select(m);
anchor = m;
if (i == rowIndex)
break;
i += step;
}
}
}
}
else if (multi && toggle)
{
if (!treeDataGrid.QueryCancelSelection())
{
if (IsSelected(modelIndex) == true)
Deselect(modelIndex);
else
Select(modelIndex);
}
}
else if (toggle)
{
if (!treeDataGrid.QueryCancelSelection())
SelectedIndex = (SelectedIndex == modelIndex) ? -1 : modelIndex;
}
else if (SelectedIndex != modelIndex || Count > 1)
{
if (!treeDataGrid.QueryCancelSelection())
SelectedIndex = modelIndex;
}
}
private bool TryKeyExpandCollapse(TreeDataGrid treeDataGrid, NavigationDirection direction, TreeDataGridRow focused)
{
if (treeDataGrid.RowsPresenter is null || focused.RowIndex < 0)
return false;
var row = _source.Rows[focused.RowIndex];
if (row is IExpander expander)
{
if (direction == NavigationDirection.Right && !expander.IsExpanded)
{
expander.IsExpanded = true;
return true;
}
else if (direction == NavigationDirection.Left && expander.IsExpanded)
{
expander.IsExpanded = false;
return true;
}
}
return false;
}
private bool MoveSelection(TreeDataGrid treeDataGrid, NavigationDirection direction, bool rangeModifier, TreeDataGridRow focused)
{
if (treeDataGrid.RowsPresenter is null || _source.Columns.Count == 0 || _source.Rows.Count == 0)
return false;
var currentRowIndex = focused?.RowIndex ?? _source.Rows.ModelIndexToRowIndex(SelectedIndex);
int newRowIndex;
if (direction == NavigationDirection.First || direction == NavigationDirection.Last)
{
newRowIndex = direction == NavigationDirection.First ? 0 : _source.Rows.Count - 1;
}
else
{
(var x, var y) = direction switch
{
NavigationDirection.Up => (0, -1),
NavigationDirection.Down => (0, 1),
NavigationDirection.Left => (-1, 0),
NavigationDirection.Right => (1, 0),
_ => (0, 0)
};
newRowIndex = Math.Max(0, Math.Min(currentRowIndex + y, _source.Rows.Count - 1));
}
if (newRowIndex != currentRowIndex)
UpdateSelection(treeDataGrid, newRowIndex, true, rangeModifier);
if (newRowIndex != currentRowIndex)
{
treeDataGrid.RowsPresenter?.BringIntoView(newRowIndex);
FocusRow(treeDataGrid, treeDataGrid.TryGetRow(newRowIndex));
return true;
}
else
{
return false;
}
}
private static void FocusRow(TreeDataGrid owner, Control control)
{
if (!owner.TryGetRow(control, out var row) || row.CellsPresenter is null)
return;
// Get the column index of the currently focused cell if possible: we'll try to focus the
// same column in the new row.
if (TopLevel.GetTopLevel(owner)?.FocusManager is { } focusManager &&
focusManager.GetFocusedElement() is Control currentFocus &&
owner.TryGetCell(currentFocus, out var currentCell) &&
row.TryGetCell(currentCell.ColumnIndex) is { } newCell &&
newCell.Focusable)
{
newCell.Focus();
}
else
{
// Otherwise, just focus the first focusable cell in the row.
foreach (var cell in row.CellsPresenter.GetRealizedElements())
{
if (cell.Focusable)
{
cell.Focus();
break;
}
}
}
}
protected override IEnumerable<TModel> GetChildren(TModel node)
{
if (node == null)
return null;
return _childrenGetter?.Invoke(node);
}
private IndexPath GetModelIndex(IEnumerable<TModel> collection, TModel model, IndexPath parent)
{
int i = 0;
foreach (var item in collection)
{
var index = parent.Append(i);
if (item != null && item == model)
return index;
var children = GetChildren(item);
if (children != null)
{
var findInChildren = GetModelIndex(children, model, index);
if (!findInChildren.Equals(IndexPath.Unselected))
return findInChildren;
}
i++;
}
return IndexPath.Unselected;
}
private bool HasChildren(IRow row)
{
var children = GetChildren(row.Model as TModel);
if (children != null)
{
foreach (var c in children)
return true;
}
return false;
}
}
}

View file

@ -1093,6 +1093,33 @@
<Setter Property="Data" Value="M 0 4 L 8 4 L 4 8 Z" /> <Setter Property="Data" Value="M 0 4 L 8 4 L 4 8 Z" />
</Style> </Style>
</Style> </Style>
<Style Selector="ToggleButton.change_tree_folder">
<Setter Property="Margin" Value="0" />
<Setter Property="Background" Value="Transparent"/>
<Setter Property="IsHitTestVisible" Value="False"/>
<Setter Property="Template">
<ControlTemplate>
<Border Background="Transparent"
Width="{TemplateBinding Width}"
Height="{TemplateBinding Height}"
HorizontalAlignment="Left"
VerticalAlignment="Center">
<Path x:Name="ChevronPath"
Margin="0,2,0,0"
Data="{StaticResource Icons.Folder.Fill}"
Fill="Goldenrod"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^:checked /template/ Path#ChevronPath">
<Setter Property="Data" Value="{StaticResource Icons.Folder.Open}" />
</Style>
</Style>
<Style Selector="ToggleButton.layout_direction"> <Style Selector="ToggleButton.layout_direction">
<Setter Property="Margin" Value="0"/> <Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/> <Setter Property="Padding" Value="0"/>
@ -1272,67 +1299,6 @@
<Setter Property="Data" Value="{StaticResource Icons.Folder.Fill}"/> <Setter Property="Data" Value="{StaticResource Icons.Folder.Fill}"/>
</Style> </Style>
<Style Selector="TreeDataGrid">
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<Setter Property="Template">
<ControlTemplate>
<Border x:Name="RootBorder"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
CornerRadius="{TemplateBinding CornerRadius}">
<DockPanel>
<ScrollViewer Name="PART_HeaderScrollViewer"
DockPanel.Dock="Top"
IsVisible="{TemplateBinding ShowColumnHeaders}"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Disabled"
BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
<Border x:Name="ColumnHeadersPresenterBorder">
<TreeDataGridColumnHeadersPresenter Name="PART_ColumnHeadersPresenter"
ElementFactory="{TemplateBinding ElementFactory}"
Items="{TemplateBinding Columns}" />
</Border>
</ScrollViewer>
<ScrollViewer Name="PART_ScrollViewer"
HorizontalScrollBarVisibility="{TemplateBinding (ScrollViewer.HorizontalScrollBarVisibility)}"
BringIntoViewOnFocusChange="{TemplateBinding (ScrollViewer.BringIntoViewOnFocusChange)}">
<TreeDataGridRowsPresenter Name="PART_RowsPresenter"
Columns="{TemplateBinding Columns}"
ElementFactory="{TemplateBinding ElementFactory}"
Items="{TemplateBinding Rows}" />
</ScrollViewer>
</DockPanel>
</Border>
</ControlTemplate>
</Setter>
<Style Selector="^/template/ Border#ColumnHeadersPresenterBorder">
<Setter Property="BorderThickness" Value="0 0 0 1" />
<Setter Property="BorderBrush" Value="{DynamicResource TreeDataGridGridLinesBrush}" />
</Style>
</Style>
<Style Selector="TreeDataGridRow">
<Style.Resources>
<SolidColorBrush x:Key="TreeDataGridSelectedCellBackgroundBrush" Color="{DynamicResource SystemAccentColor}" Opacity="0.4" />
</Style.Resources>
<Setter Property="Background" Value="Transparent"/>
</Style>
<Style Selector="TreeDataGridColumnHeader">
<Setter Property="HorizontalContentAlignment" Value="Center"/>
</Style>
<Style Selector="TreeDataGridTextCell TextBlock">
<Setter Property="FontFamily" Value="{Binding Source={x:Static vm:Preference.Instance}, Path=MonospaceFont}"/>
</Style>
<Style Selector="TreeDataGridExpanderCell[IsExpanded=True] Path.folder_icon">
<Setter Property="Data" Value="{StaticResource Icons.Folder.Open}"/>
</Style>
<Style Selector="TreeDataGridExpanderCell[IsExpanded=False] Path.folder_icon">
<Setter Property="Data" Value="{StaticResource Icons.Folder.Fill}"/>
</Style>
<Style Selector="NumericUpDown"> <Style Selector="NumericUpDown">
<Style Selector="^ /template/ ButtonSpinner#PART_Spinner"> <Style Selector="^ /template/ ButtonSpinner#PART_Spinner">
<Setter Property="MinHeight" Value="0"/> <Setter Property="MinHeight" Value="0"/>

View file

@ -33,7 +33,6 @@
<PackageReference Include="Avalonia.Desktop" Version="11.0.10" /> <PackageReference Include="Avalonia.Desktop" Version="11.0.10" />
<PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" /> <PackageReference Include="Avalonia.Themes.Fluent" Version="11.0.10" />
<PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.10" /> <PackageReference Include="Avalonia.Controls.DataGrid" Version="11.0.10" />
<PackageReference Include="Avalonia.Controls.TreeDataGrid" Version="11.0.10" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" /> <PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.10" Condition="'$(Configuration)' == 'Debug'" /> <PackageReference Include="Avalonia.Diagnostics" Version="11.0.10" Condition="'$(Configuration)' == 'Debug'" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" /> <PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />

View file

@ -5,31 +5,111 @@
xmlns:m="using:SourceGit.Models" xmlns:m="using:SourceGit.Models"
xmlns:v="using:SourceGit.Views" xmlns:v="using:SourceGit.Views"
xmlns:c="using:SourceGit.Converters" xmlns:c="using:SourceGit.Converters"
xmlns:ac="using:Avalonia.Controls.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="SourceGit.Views.ChangeCollectionView" x:Class="SourceGit.Views.ChangeCollectionView"
x:Name="ThisControl"> x:Name="ThisControl">
<UserControl.Resources> <UserControl.Styles>
<DataTemplate x:Key="TreeModeTemplate" DataType="v:ChangeTreeNode"> <Style Selector="ListBox">
<Grid HorizontalAlignment="Stretch" Height="24" ColumnDefinitions="Auto,*"> <Setter Property="Background" Value="Transparent"/>
<Path Grid.Column="0" Classes="folder_icon" Width="14" Height="14" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" Fill="Goldenrod" VerticalAlignment="Center"/> <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
<v:ChangeStatusIcon Grid.Column="0" Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding Change}" IsVisible="{Binding !IsFolder}"/> <Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="6,0,0,0"/> <Setter Property="ItemsPanel">
</Grid> <ItemsPanelTemplate>
<VirtualizingStackPanel Orientation="Vertical"/>
</ItemsPanelTemplate>
</Setter>
</Style>
<Style Selector="ListBoxItem">
<Setter Property="Height" Value="24"/>
<Setter Property="Margin" Value="0"/>
<Setter Property="Padding" Value="0"/>
</Style>
</UserControl.Styles>
<UserControl.DataTemplates>
<DataTemplate DataType="v:ChangeCollectionAsTree">
<v:ChangeCollectionContainer ItemsSource="{Binding Rows}"
SelectionMode="{Binding #ThisControl.SelectionMode}"
SelectionChanged="OnRowSelectionChanged">
<ListBox.ItemTemplate>
<DataTemplate DataType="v:ChangeTreeNode">
<Grid ColumnDefinitions="16,Auto,Auto,*"
Margin="{Binding Depth, Converter={x:Static c:IntConverters.ToTreeMargin}}"
Background="Transparent"
DoubleTapped="OnRowDoubleTapped">
<v:ChangeTreeNodeToggleButton Grid.Column="0"
Classes="tree_expander"
Focusable="False"
HorizontalAlignment="Center"
IsChecked="{Binding IsExpanded, Mode=OneWay}"
IsVisible="{Binding IsFolder}"/>
<ToggleButton Grid.Column="1"
Classes="change_tree_folder"
Focusable="False"
Width="14" Height="14"
IsChecked="{Binding IsExpanded}"
IsVisible="{Binding IsFolder}"/>
<v:ChangeStatusIcon Grid.Column="1" Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding Change}" IsVisible="{Binding !IsFolder}"/>
<TextBlock Grid.Column="2" Classes="monospace" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="6,0,0,0"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</v:ChangeCollectionContainer>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="ListModeTemplate" DataType="m:Change"> <DataTemplate DataType="v:ChangeCollectionAsGrid">
<StackPanel Orientation="Horizontal"> <v:ChangeCollectionContainer ItemsSource="{Binding Changes}"
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding}" Margin="4,0,0,0"/> SelectionMode="{Binding #ThisControl.SelectionMode}"
<TextBlock Classes="monospace" Text="{Binding Path}" Margin="4,0"/> SelectionChanged="OnRowSelectionChanged">
</StackPanel> <ListBox.ItemTemplate>
<DataTemplate DataType="m:Change">
<Grid ColumnDefinitions="Auto,Auto,Auto,*" Background="Transparent" DoubleTapped="OnRowDoubleTapped">
<v:ChangeStatusIcon Grid.Column="0"
Width="14" Height="14"
Margin="4,0,0,0"
IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}"
Change="{Binding}" />
<TextBlock Grid.Column="1"
Classes="monospace"
Text="{Binding Path, Converter={x:Static c:PathConverters.PureFileName}}"
Margin="4,0"/>
<TextBlock Grid.Column="2"
Classes="monospace"
Text="{Binding Path, Converter={x:Static c:PathConverters.PureDirectoryName}}"
Foreground="{DynamicResource Brush.FG2}"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</v:ChangeCollectionContainer>
</DataTemplate> </DataTemplate>
<DataTemplate x:Key="GridModeTemplate" DataType="m:Change"> <DataTemplate DataType="v:ChangeCollectionAsList">
<StackPanel Orientation="Horizontal"> <v:ChangeCollectionContainer ItemsSource="{Binding Changes}"
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding}" Margin="4,0,0,0"/> SelectionMode="{Binding #ThisControl.SelectionMode}"
<TextBlock Classes="monospace" Text="{Binding Path, Converter={x:Static c:PathConverters.PureFileName}}" Margin="4,0"/> SelectionChanged="OnRowSelectionChanged">
<TextBlock Classes="monospace" Text="{Binding Path, Converter={x:Static c:PathConverters.PureDirectoryName}}" Foreground="{DynamicResource Brush.FG2}"/> <ListBox.ItemTemplate>
</StackPanel> <DataTemplate DataType="m:Change">
<Grid ColumnDefinitions="Auto,Auto,*" Background="Transparent" DoubleTapped="OnRowDoubleTapped">
<v:ChangeStatusIcon Grid.Column="0"
Width="14" Height="14"
Margin="4,0,0,0"
IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}"
Change="{Binding}" />
<TextBlock Grid.Column="1"
Classes="monospace"
Text="{Binding Path}"
Margin="4,0"/>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</v:ChangeCollectionContainer>
</DataTemplate> </DataTemplate>
</UserControl.Resources> </UserControl.DataTemplates>
</UserControl> </UserControl>

View file

@ -2,22 +2,51 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia; using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.VisualTree;
using CommunityToolkit.Mvvm.ComponentModel;
namespace SourceGit.Views namespace SourceGit.Views
{ {
public class ChangeTreeNode public class ChangeTreeNode : ObservableObject
{ {
public string FullPath { get; set; } = string.Empty; public string FullPath { get; set; } = string.Empty;
public bool IsFolder { get; set; } = false; public int Depth { get; private set; } = 0;
public bool IsExpanded { get; set; } = false;
public Models.Change Change { get; set; } = null; public Models.Change Change { get; set; } = null;
public List<ChangeTreeNode> Children { get; set; } = new List<ChangeTreeNode>(); public List<ChangeTreeNode> Children { get; set; } = new List<ChangeTreeNode>();
public static List<ChangeTreeNode> Build(IList<Models.Change> changes, bool expanded) public bool IsFolder
{
get => Change == null;
}
public bool IsExpanded
{
get => _isExpanded;
set => SetProperty(ref _isExpanded, value);
}
public ChangeTreeNode(Models.Change c, int depth)
{
FullPath = c.Path;
Depth = depth;
Change = c;
IsExpanded = false;
}
public ChangeTreeNode(string path, bool isExpanded, int depth)
{
FullPath = path;
Depth = depth;
IsExpanded = isExpanded;
}
public static List<ChangeTreeNode> Build(IList<Models.Change> changes, HashSet<string> folded)
{ {
var nodes = new List<ChangeTreeNode>(); var nodes = new List<ChangeTreeNode>();
var folders = new Dictionary<string, ChangeTreeNode>(); var folders = new Dictionary<string, ChangeTreeNode>();
@ -27,18 +56,13 @@ namespace SourceGit.Views
var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal); var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal);
if (sepIdx == -1) if (sepIdx == -1)
{ {
nodes.Add(new ChangeTreeNode() nodes.Add(new ChangeTreeNode(c, 0));
{
FullPath = c.Path,
Change = c,
IsFolder = false,
IsExpanded = false
});
} }
else else
{ {
ChangeTreeNode lastFolder = null; ChangeTreeNode lastFolder = null;
var start = 0; var start = 0;
var depth = 0;
while (sepIdx != -1) while (sepIdx != -1)
{ {
@ -49,42 +73,29 @@ namespace SourceGit.Views
} }
else if (lastFolder == null) else if (lastFolder == null)
{ {
lastFolder = new ChangeTreeNode() lastFolder = new ChangeTreeNode(folder, !folded.Contains(folder), depth);
{
FullPath = folder,
IsFolder = true,
IsExpanded = expanded
};
folders.Add(folder, lastFolder); folders.Add(folder, lastFolder);
InsertFolder(nodes, lastFolder); InsertFolder(nodes, lastFolder);
} }
else else
{ {
var cur = new ChangeTreeNode() var cur = new ChangeTreeNode(folder, !folded.Contains(folder), depth);
{
FullPath = folder,
IsFolder = true,
IsExpanded = expanded
};
folders.Add(folder, cur); folders.Add(folder, cur);
InsertFolder(lastFolder.Children, cur); InsertFolder(lastFolder.Children, cur);
lastFolder = cur; lastFolder = cur;
} }
start = sepIdx + 1; start = sepIdx + 1;
depth++;
sepIdx = c.Path.IndexOf('/', start); sepIdx = c.Path.IndexOf('/', start);
} }
lastFolder.Children.Add(new ChangeTreeNode() lastFolder.Children.Add(new ChangeTreeNode(c, depth));
{
FullPath = c.Path,
Change = c,
IsFolder = false,
IsExpanded = false
});
} }
} }
Sort(nodes);
folders.Clear(); folders.Clear();
return nodes; return nodes;
} }
@ -102,6 +113,68 @@ namespace SourceGit.Views
collection.Add(subFolder); collection.Add(subFolder);
} }
private static void Sort(List<ChangeTreeNode> nodes)
{
foreach (var node in nodes)
{
if (node.IsFolder)
Sort(node.Children);
}
nodes.Sort((l, r) =>
{
if (l.IsFolder)
return r.IsFolder ? string.Compare(l.FullPath, r.FullPath, StringComparison.Ordinal) : -1;
return r.IsFolder ? 1 : string.Compare(l.FullPath, r.FullPath, StringComparison.Ordinal);
});
}
private bool _isExpanded = true;
}
public class ChangeCollectionAsTree
{
public List<ChangeTreeNode> Tree { get; set; } = new List<ChangeTreeNode>();
public AvaloniaList<ChangeTreeNode> Rows { get; set; } = new AvaloniaList<ChangeTreeNode>();
}
public class ChangeCollectionAsGrid
{
public AvaloniaList<Models.Change> Changes { get; set; } = new AvaloniaList<Models.Change>();
}
public class ChangeCollectionAsList
{
public AvaloniaList<Models.Change> Changes { get; set; } = new AvaloniaList<Models.Change>();
}
public class ChangeTreeNodeToggleButton : ToggleButton
{
protected override Type StyleKeyOverride => typeof(ToggleButton);
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed &&
DataContext is ChangeTreeNode { IsFolder: true } node)
{
var tree = this.FindAncestorOfType<ChangeCollectionView>();
tree.ToggleNodeIsExpanded(node);
}
e.Handled = true;
}
}
public class ChangeCollectionContainer : ListBox
{
protected override Type StyleKeyOverride => typeof(ListBox);
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key != Key.Space)
base.OnKeyDown(e);
}
} }
public partial class ChangeCollectionView : UserControl public partial class ChangeCollectionView : UserControl
@ -115,13 +188,13 @@ namespace SourceGit.Views
set => SetValue(IsWorkingCopyChangeProperty, value); set => SetValue(IsWorkingCopyChangeProperty, value);
} }
public static readonly StyledProperty<bool> SingleSelectProperty = public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
AvaloniaProperty.Register<ChangeCollectionView, bool>(nameof(SingleSelect), true); AvaloniaProperty.Register<ChangeCollectionView, SelectionMode>(nameof(SelectionMode), SelectionMode.Single);
public bool SingleSelect public SelectionMode SelectionMode
{ {
get => GetValue(SingleSelectProperty); get => GetValue(SelectionModeProperty);
set => SetValue(SingleSelectProperty, value); set => SetValue(SelectionModeProperty, value);
} }
public static readonly StyledProperty<Models.ChangeViewMode> ViewModeProperty = public static readonly StyledProperty<Models.ChangeViewMode> ViewModeProperty =
@ -160,171 +233,193 @@ namespace SourceGit.Views
remove { RemoveHandler(ChangeDoubleTappedEvent, value); } remove { RemoveHandler(ChangeDoubleTappedEvent, value); }
} }
static ChangeCollectionView()
{
ViewModeProperty.Changed.AddClassHandler<ChangeCollectionView>((c, e) => c.UpdateSource());
ChangesProperty.Changed.AddClassHandler<ChangeCollectionView>((c, e) => c.UpdateSource());
SelectedChangesProperty.Changed.AddClassHandler<ChangeCollectionView>((c, e) => c.UpdateSelected());
}
public ChangeCollectionView() public ChangeCollectionView()
{ {
InitializeComponent(); InitializeComponent();
} }
private void UpdateSource() public void ToggleNodeIsExpanded(ChangeTreeNode node)
{ {
if (Content is TreeDataGrid tree && tree.Source is IDisposable disposable) if (_displayContext is ChangeCollectionAsTree tree)
disposable.Dispose();
Content = null;
var changes = Changes;
if (changes == null || changes.Count == 0)
return;
var viewMode = ViewMode;
if (viewMode == Models.ChangeViewMode.Tree)
{ {
var filetree = ChangeTreeNode.Build(changes, true); _disableSelectionChangingEvent = true;
var template = this.FindResource("TreeModeTemplate") as IDataTemplate; node.IsExpanded = !node.IsExpanded;
var source = new HierarchicalTreeDataGridSource<ChangeTreeNode>(filetree)
var depth = node.Depth;
var idx = tree.Rows.IndexOf(node);
if (idx == -1)
return;
if (node.IsExpanded)
{ {
Columns = var subrows = new List<ChangeTreeNode>();
MakeTreeRows(subrows, node.Children);
tree.Rows.InsertRange(idx + 1, subrows);
}
else
{
var removeCount = 0;
for (int i = idx + 1; i < tree.Rows.Count; i++)
{ {
new HierarchicalExpanderColumn<ChangeTreeNode>( var row = tree.Rows[i];
new TemplateColumn<ChangeTreeNode>(null, template, null, GridLength.Auto), if (row.Depth <= depth)
x => x.Children, break;
x => x.Children.Count > 0,
x => x.IsExpanded) removeCount++;
} }
}; tree.Rows.RemoveRange(idx + 1, removeCount);
}
var selection = new Models.TreeDataGridSelectionModel<ChangeTreeNode>(source, x => x.Children); _disableSelectionChangingEvent = false;
selection.SingleSelect = SingleSelect;
selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
selection.SelectionChanged += (s, _) =>
{
if (!_isSelecting && s is Models.TreeDataGridSelectionModel<ChangeTreeNode> model)
{
var selected = new List<Models.Change>();
foreach (var c in model.SelectedItems)
CollectChangesInNode(selected, c);
TrySetSelected(selected);
}
};
source.Selection = selection;
CreateTreeDataGrid(source);
}
else if (viewMode == Models.ChangeViewMode.List)
{
var template = this.FindResource("ListModeTemplate") as IDataTemplate;
var source = new FlatTreeDataGridSource<Models.Change>(changes)
{
Columns = { new TemplateColumn<Models.Change>(null, template, null, GridLength.Auto) }
};
var selection = new Models.TreeDataGridSelectionModel<Models.Change>(source, null);
selection.SingleSelect = SingleSelect;
selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
selection.SelectionChanged += (s, _) =>
{
if (!_isSelecting && s is Models.TreeDataGridSelectionModel<Models.Change> model)
{
var selected = new List<Models.Change>();
foreach (var c in model.SelectedItems)
selected.Add(c);
TrySetSelected(selected);
}
};
source.Selection = selection;
CreateTreeDataGrid(source);
}
else
{
var template = this.FindResource("GridModeTemplate") as IDataTemplate;
var source = new FlatTreeDataGridSource<Models.Change>(changes)
{
Columns = { new TemplateColumn<Models.Change>(null, template, null, GridLength.Auto) },
};
var selection = new Models.TreeDataGridSelectionModel<Models.Change>(source, null);
selection.SingleSelect = SingleSelect;
selection.RowDoubleTapped += (_, e) => RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
selection.SelectionChanged += (s, _) =>
{
if (!_isSelecting && s is Models.TreeDataGridSelectionModel<Models.Change> model)
{
var selected = new List<Models.Change>();
foreach (var c in model.SelectedItems)
selected.Add(c);
TrySetSelected(selected);
}
};
source.Selection = selection;
CreateTreeDataGrid(source);
} }
} }
private void UpdateSelected() protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
if (_isSelecting || Content == null) base.OnPropertyChanged(change);
return;
var tree = Content as TreeDataGrid; if (change.Property == ViewModeProperty || change.Property == ChangesProperty)
if (tree == null)
return;
_isSelecting = true;
var selected = SelectedChanges;
if (tree.Source.Selection is Models.TreeDataGridSelectionModel<Models.Change> changeSelection)
{ {
if (selected == null || selected.Count == 0) _disableSelectionChangingEvent = change.Property == ChangesProperty;
changeSelection.Clear(); var changes = Changes;
else if (changes == null || changes.Count == 0)
changeSelection.Select(selected);
}
else if (tree.Source.Selection is Models.TreeDataGridSelectionModel<ChangeTreeNode> treeSelection)
{
if (selected == null || selected.Count == 0)
{ {
treeSelection.Clear(); Content = null;
_isSelecting = false; _displayContext = null;
_disableSelectionChangingEvent = false;
return; return;
} }
var set = new HashSet<object>(); if (ViewMode == Models.ChangeViewMode.Tree)
foreach (var c in selected) {
set.Add(c); HashSet<string> oldFolded = new HashSet<string>();
if (_displayContext is ChangeCollectionAsTree oldTree)
{
foreach (var row in oldTree.Rows)
{
if (row.IsFolder && !row.IsExpanded)
oldFolded.Add(row.FullPath);
}
}
var nodes = new List<ChangeTreeNode>(); var tree = new ChangeCollectionAsTree();
foreach (var node in tree.Source.Items) tree.Tree = ChangeTreeNode.Build(changes, oldFolded);
CollectSelectedNodeByChange(nodes, node as ChangeTreeNode, set);
if (nodes.Count == 0) var rows = new List<ChangeTreeNode>();
treeSelection.Clear(); MakeTreeRows(rows, tree.Tree);
tree.Rows.AddRange(rows);
_displayContext = tree;
}
else if (ViewMode == Models.ChangeViewMode.Grid)
{
var grid = new ChangeCollectionAsGrid();
grid.Changes.AddRange(changes);
_displayContext = grid;
}
else else
treeSelection.Select(nodes); {
var list = new ChangeCollectionAsList();
list.Changes.AddRange(changes);
_displayContext = list;
}
Content = _displayContext;
_disableSelectionChangingEvent = false;
}
else if (change.Property == SelectedChangesProperty)
{
if (_disableSelectionChangingEvent)
return;
var list = this.FindDescendantOfType<ChangeCollectionContainer>();
if (list == null)
return;
_disableSelectionChangingEvent = true;
var selected = SelectedChanges;
if (selected == null || selected.Count == 0)
{
list.SelectedItem = null;
}
else if (_displayContext is ChangeCollectionAsTree tree)
{
var sets = new HashSet<Models.Change>();
foreach (var c in selected)
sets.Add(c);
var nodes = new List<ChangeTreeNode>();
foreach (var row in tree.Rows)
{
if (row.Change != null && sets.Contains(row.Change))
nodes.Add(row);
}
list.SelectedItems = nodes;
}
else
{
list.SelectedItems = selected;
}
_disableSelectionChangingEvent = false;
} }
_isSelecting = false;
} }
private void CreateTreeDataGrid(ITreeDataGridSource source) private void OnRowDoubleTapped(object sender, TappedEventArgs e)
{ {
Content = new TreeDataGrid() var grid = sender as Grid;
if (grid.DataContext is ChangeTreeNode node)
{ {
AutoDragDropRows = false, if (node.IsFolder)
ShowColumnHeaders = false, {
CanUserResizeColumns = false, var posX = e.GetPosition(this).X;
CanUserSortColumns = false, if (posX < node.Depth * 16 + 16)
Source = source, return;
};
ToggleNodeIsExpanded(node);
}
else
{
RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
}
}
else if (grid.DataContext is Models.Change)
{
RaiseEvent(new RoutedEventArgs(ChangeDoubleTappedEvent));
}
}
private void OnRowSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_disableSelectionChangingEvent)
return;
_disableSelectionChangingEvent = true;
var selected = new List<Models.Change>();
var list = sender as ListBox;
foreach (var item in list.SelectedItems)
{
if (item is Models.Change c)
selected.Add(c);
else if (item is ChangeTreeNode node)
CollectChangesInNode(selected, node);
}
TrySetSelected(selected);
_disableSelectionChangingEvent = false;
}
private void MakeTreeRows(List<ChangeTreeNode> rows, List<ChangeTreeNode> nodes)
{
foreach (var node in nodes)
{
rows.Add(node);
if (!node.IsExpanded || !node.IsFolder)
continue;
MakeTreeRows(rows, node.Children);
}
} }
private void CollectChangesInNode(List<Models.Change> outs, ChangeTreeNode node) private void CollectChangesInNode(List<Models.Change> outs, ChangeTreeNode node)
@ -340,26 +435,9 @@ namespace SourceGit.Views
} }
} }
private void CollectSelectedNodeByChange(List<ChangeTreeNode> outs, ChangeTreeNode node, HashSet<object> selected)
{
if (node == null)
return;
if (node.IsFolder)
{
foreach (var child in node.Children)
CollectSelectedNodeByChange(outs, child, selected);
}
else if (node.Change != null && selected.Contains(node.Change))
{
outs.Add(node);
}
}
private void TrySetSelected(List<Models.Change> changes) private void TrySetSelected(List<Models.Change> changes)
{ {
var old = SelectedChanges; var old = SelectedChanges;
if (old == null && changes.Count == 0) if (old == null && changes.Count == 0)
return; return;
@ -379,11 +457,12 @@ namespace SourceGit.Views
return; return;
} }
_isSelecting = true; _disableSelectionChangingEvent = true;
SetCurrentValue(SelectedChangesProperty, changes); SetCurrentValue(SelectedChangesProperty, changes);
_isSelecting = false; _disableSelectionChangingEvent = false;
} }
private bool _isSelecting = false; private bool _disableSelectionChangingEvent = false;
private object _displayContext = null;
} }
} }

View file

@ -47,6 +47,7 @@
<!-- Changes --> <!-- Changes -->
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}"> <Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
<v:ChangeCollectionView IsWorkingCopyChange="False" <v:ChangeCollectionView IsWorkingCopyChange="False"
SelectionMode="Single"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}" ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}"
Changes="{Binding VisibleChanges}" Changes="{Binding VisibleChanges}"
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}" SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"

View file

@ -10,7 +10,7 @@ namespace SourceGit.Views
public class EnhancedTextBox : TextBox public class EnhancedTextBox : TextBox
{ {
public static readonly RoutedEvent<KeyEventArgs> PreviewKeyDownEvent = public static readonly RoutedEvent<KeyEventArgs> PreviewKeyDownEvent =
RoutedEvent.Register<ChangeCollectionView, KeyEventArgs>(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble); RoutedEvent.Register<EnhancedTextBox, KeyEventArgs>(nameof(KeyEventArgs), RoutingStrategies.Tunnel | RoutingStrategies.Bubble);
public event EventHandler<KeyEventArgs> PreviewKeyDown public event EventHandler<KeyEventArgs> PreviewKeyDown
{ {

View file

@ -170,7 +170,7 @@ namespace SourceGit.Views
if (subtree != null && subtree.Count > 0) if (subtree != null && subtree.Count > 0)
{ {
var subrows = new List<RevisionFileTreeNode>(); var subrows = new List<RevisionFileTreeNode>();
MakeRows(subrows, node.Children, depth + 1); MakeRows(subrows, subtree, depth + 1);
_rows.InsertRange(idx + 1, subrows); _rows.InsertRange(idx + 1, subrows);
} }
} }

View file

@ -65,7 +65,7 @@
<!-- Unstaged Changes --> <!-- Unstaged Changes -->
<v:ChangeCollectionView Grid.Row="1" <v:ChangeCollectionView Grid.Row="1"
IsWorkingCopyChange="True" IsWorkingCopyChange="True"
SingleSelect="False" SelectionMode="Multiple"
Background="{DynamicResource Brush.Contents}" Background="{DynamicResource Brush.Contents}"
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=UnstagedChangeViewMode}" ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=UnstagedChangeViewMode}"
Changes="{Binding Unstaged}" Changes="{Binding Unstaged}"
@ -99,7 +99,7 @@
<!-- Staged Changes --> <!-- Staged Changes -->
<v:ChangeCollectionView Grid.Row="3" <v:ChangeCollectionView Grid.Row="3"
IsWorkingCopyChange="False" IsWorkingCopyChange="False"
SingleSelect="False" 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}"
Changes="{Binding Staged}" Changes="{Binding Staged}"