mirror of
https://github.com/sourcegit-scm/sourcegit.git
synced 2024-11-01 13:13:21 -07:00
refactor: remove dependency on Avalonia.Controls.TreeDataGrid
This commit is contained in:
parent
7f228385f9
commit
1c204e72a1
10 changed files with 403 additions and 741 deletions
|
@ -20,7 +20,6 @@
|
|||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
<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="/Resources/Styles.axaml"/>
|
||||
</Application.Styles>
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1093,6 +1093,33 @@
|
|||
<Setter Property="Data" Value="M 0 4 L 8 4 L 4 8 Z" />
|
||||
</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">
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Setter Property="Padding" Value="0"/>
|
||||
|
@ -1272,67 +1299,6 @@
|
|||
<Setter Property="Data" Value="{StaticResource Icons.Folder.Fill}"/>
|
||||
</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="^ /template/ ButtonSpinner#PART_Spinner">
|
||||
<Setter Property="MinHeight" Value="0"/>
|
||||
|
|
|
@ -33,7 +33,6 @@
|
|||
<PackageReference Include="Avalonia.Desktop" 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.TreeDataGrid" Version="11.0.10" />
|
||||
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
|
||||
<PackageReference Include="Avalonia.Diagnostics" Version="11.0.10" Condition="'$(Configuration)' == 'Debug'" />
|
||||
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
|
||||
|
|
|
@ -5,31 +5,111 @@
|
|||
xmlns:m="using:SourceGit.Models"
|
||||
xmlns:v="using:SourceGit.Views"
|
||||
xmlns:c="using:SourceGit.Converters"
|
||||
xmlns:ac="using:Avalonia.Controls.Converters"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="SourceGit.Views.ChangeCollectionView"
|
||||
x:Name="ThisControl">
|
||||
<UserControl.Resources>
|
||||
<DataTemplate x:Key="TreeModeTemplate" DataType="v:ChangeTreeNode">
|
||||
<Grid HorizontalAlignment="Stretch" Height="24" ColumnDefinitions="Auto,*">
|
||||
<Path Grid.Column="0" Classes="folder_icon" Width="14" Height="14" Margin="0,2,0,0" IsVisible="{Binding IsFolder}" Fill="Goldenrod" VerticalAlignment="Center"/>
|
||||
<v:ChangeStatusIcon Grid.Column="0" Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding Change}" IsVisible="{Binding !IsFolder}"/>
|
||||
<TextBlock Grid.Column="1" Classes="monospace" Text="{Binding FullPath, Converter={x:Static c:PathConverters.PureFileName}}" Margin="6,0,0,0"/>
|
||||
</Grid>
|
||||
<UserControl.Styles>
|
||||
<Style Selector="ListBox">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto"/>
|
||||
<Setter Property="ItemsPanel">
|
||||
<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 x:Key="ListModeTemplate" DataType="m:Change">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding}" Margin="4,0,0,0"/>
|
||||
<TextBlock Classes="monospace" Text="{Binding Path}" Margin="4,0"/>
|
||||
</StackPanel>
|
||||
<DataTemplate DataType="v:ChangeCollectionAsGrid">
|
||||
<v:ChangeCollectionContainer ItemsSource="{Binding Changes}"
|
||||
SelectionMode="{Binding #ThisControl.SelectionMode}"
|
||||
SelectionChanged="OnRowSelectionChanged">
|
||||
<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 x:Key="GridModeTemplate" DataType="m:Change">
|
||||
<StackPanel Orientation="Horizontal">
|
||||
<v:ChangeStatusIcon Width="14" Height="14" IsWorkingCopyChange="{Binding #ThisControl.IsWorkingCopyChange}" Change="{Binding}" Margin="4,0,0,0"/>
|
||||
<TextBlock Classes="monospace" Text="{Binding Path, Converter={x:Static c:PathConverters.PureFileName}}" Margin="4,0"/>
|
||||
<TextBlock Classes="monospace" Text="{Binding Path, Converter={x:Static c:PathConverters.PureDirectoryName}}" Foreground="{DynamicResource Brush.FG2}"/>
|
||||
</StackPanel>
|
||||
<DataTemplate DataType="v:ChangeCollectionAsList">
|
||||
<v:ChangeCollectionContainer ItemsSource="{Binding Changes}"
|
||||
SelectionMode="{Binding #ThisControl.SelectionMode}"
|
||||
SelectionChanged="OnRowSelectionChanged">
|
||||
<ListBox.ItemTemplate>
|
||||
<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>
|
||||
</UserControl.Resources>
|
||||
</UserControl.DataTemplates>
|
||||
</UserControl>
|
||||
|
|
|
@ -2,22 +2,51 @@ using System;
|
|||
using System.Collections.Generic;
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Collections;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.Models.TreeDataGrid;
|
||||
using Avalonia.Controls.Templates;
|
||||
using Avalonia.Controls.Primitives;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Interactivity;
|
||||
using Avalonia.VisualTree;
|
||||
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
|
||||
namespace SourceGit.Views
|
||||
{
|
||||
public class ChangeTreeNode
|
||||
public class ChangeTreeNode : ObservableObject
|
||||
{
|
||||
public string FullPath { get; set; } = string.Empty;
|
||||
public bool IsFolder { get; set; } = false;
|
||||
public bool IsExpanded { get; set; } = false;
|
||||
public int Depth { get; private set; } = 0;
|
||||
public Models.Change Change { get; set; } = null;
|
||||
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 folders = new Dictionary<string, ChangeTreeNode>();
|
||||
|
@ -27,18 +56,13 @@ namespace SourceGit.Views
|
|||
var sepIdx = c.Path.IndexOf('/', StringComparison.Ordinal);
|
||||
if (sepIdx == -1)
|
||||
{
|
||||
nodes.Add(new ChangeTreeNode()
|
||||
{
|
||||
FullPath = c.Path,
|
||||
Change = c,
|
||||
IsFolder = false,
|
||||
IsExpanded = false
|
||||
});
|
||||
nodes.Add(new ChangeTreeNode(c, 0));
|
||||
}
|
||||
else
|
||||
{
|
||||
ChangeTreeNode lastFolder = null;
|
||||
var start = 0;
|
||||
var depth = 0;
|
||||
|
||||
while (sepIdx != -1)
|
||||
{
|
||||
|
@ -49,42 +73,29 @@ namespace SourceGit.Views
|
|||
}
|
||||
else if (lastFolder == null)
|
||||
{
|
||||
lastFolder = new ChangeTreeNode()
|
||||
{
|
||||
FullPath = folder,
|
||||
IsFolder = true,
|
||||
IsExpanded = expanded
|
||||
};
|
||||
lastFolder = new ChangeTreeNode(folder, !folded.Contains(folder), depth);
|
||||
folders.Add(folder, lastFolder);
|
||||
InsertFolder(nodes, lastFolder);
|
||||
}
|
||||
else
|
||||
{
|
||||
var cur = new ChangeTreeNode()
|
||||
{
|
||||
FullPath = folder,
|
||||
IsFolder = true,
|
||||
IsExpanded = expanded
|
||||
};
|
||||
var cur = new ChangeTreeNode(folder, !folded.Contains(folder), depth);
|
||||
folders.Add(folder, cur);
|
||||
InsertFolder(lastFolder.Children, cur);
|
||||
lastFolder = cur;
|
||||
}
|
||||
|
||||
start = sepIdx + 1;
|
||||
depth++;
|
||||
sepIdx = c.Path.IndexOf('/', start);
|
||||
}
|
||||
|
||||
lastFolder.Children.Add(new ChangeTreeNode()
|
||||
{
|
||||
FullPath = c.Path,
|
||||
Change = c,
|
||||
IsFolder = false,
|
||||
IsExpanded = false
|
||||
});
|
||||
lastFolder.Children.Add(new ChangeTreeNode(c, depth));
|
||||
}
|
||||
}
|
||||
|
||||
Sort(nodes);
|
||||
|
||||
folders.Clear();
|
||||
return nodes;
|
||||
}
|
||||
|
@ -102,6 +113,68 @@ namespace SourceGit.Views
|
|||
|
||||
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
|
||||
|
@ -115,13 +188,13 @@ namespace SourceGit.Views
|
|||
set => SetValue(IsWorkingCopyChangeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<bool> SingleSelectProperty =
|
||||
AvaloniaProperty.Register<ChangeCollectionView, bool>(nameof(SingleSelect), true);
|
||||
public static readonly StyledProperty<SelectionMode> SelectionModeProperty =
|
||||
AvaloniaProperty.Register<ChangeCollectionView, SelectionMode>(nameof(SelectionMode), SelectionMode.Single);
|
||||
|
||||
public bool SingleSelect
|
||||
public SelectionMode SelectionMode
|
||||
{
|
||||
get => GetValue(SingleSelectProperty);
|
||||
set => SetValue(SingleSelectProperty, value);
|
||||
get => GetValue(SelectionModeProperty);
|
||||
set => SetValue(SelectionModeProperty, value);
|
||||
}
|
||||
|
||||
public static readonly StyledProperty<Models.ChangeViewMode> ViewModeProperty =
|
||||
|
@ -160,171 +233,193 @@ namespace SourceGit.Views
|
|||
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()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
private void UpdateSource()
|
||||
public void ToggleNodeIsExpanded(ChangeTreeNode node)
|
||||
{
|
||||
if (Content is TreeDataGrid tree && tree.Source is IDisposable disposable)
|
||||
disposable.Dispose();
|
||||
|
||||
Content = null;
|
||||
|
||||
var changes = Changes;
|
||||
if (changes == null || changes.Count == 0)
|
||||
return;
|
||||
|
||||
var viewMode = ViewMode;
|
||||
if (viewMode == Models.ChangeViewMode.Tree)
|
||||
if (_displayContext is ChangeCollectionAsTree tree)
|
||||
{
|
||||
var filetree = ChangeTreeNode.Build(changes, true);
|
||||
var template = this.FindResource("TreeModeTemplate") as IDataTemplate;
|
||||
var source = new HierarchicalTreeDataGridSource<ChangeTreeNode>(filetree)
|
||||
_disableSelectionChangingEvent = true;
|
||||
node.IsExpanded = !node.IsExpanded;
|
||||
|
||||
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>(
|
||||
new TemplateColumn<ChangeTreeNode>(null, template, null, GridLength.Auto),
|
||||
x => x.Children,
|
||||
x => x.Children.Count > 0,
|
||||
x => x.IsExpanded)
|
||||
var row = tree.Rows[i];
|
||||
if (row.Depth <= depth)
|
||||
break;
|
||||
|
||||
removeCount++;
|
||||
}
|
||||
};
|
||||
tree.Rows.RemoveRange(idx + 1, removeCount);
|
||||
}
|
||||
|
||||
var selection = new Models.TreeDataGridSelectionModel<ChangeTreeNode>(source, x => x.Children);
|
||||
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);
|
||||
_disableSelectionChangingEvent = false;
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateSelected()
|
||||
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||
{
|
||||
if (_isSelecting || Content == null)
|
||||
return;
|
||||
base.OnPropertyChanged(change);
|
||||
|
||||
var tree = Content as TreeDataGrid;
|
||||
if (tree == null)
|
||||
return;
|
||||
|
||||
_isSelecting = true;
|
||||
var selected = SelectedChanges;
|
||||
if (tree.Source.Selection is Models.TreeDataGridSelectionModel<Models.Change> changeSelection)
|
||||
if (change.Property == ViewModeProperty || change.Property == ChangesProperty)
|
||||
{
|
||||
if (selected == null || selected.Count == 0)
|
||||
changeSelection.Clear();
|
||||
else
|
||||
changeSelection.Select(selected);
|
||||
}
|
||||
else if (tree.Source.Selection is Models.TreeDataGridSelectionModel<ChangeTreeNode> treeSelection)
|
||||
{
|
||||
if (selected == null || selected.Count == 0)
|
||||
_disableSelectionChangingEvent = change.Property == ChangesProperty;
|
||||
var changes = Changes;
|
||||
if (changes == null || changes.Count == 0)
|
||||
{
|
||||
treeSelection.Clear();
|
||||
_isSelecting = false;
|
||||
Content = null;
|
||||
_displayContext = null;
|
||||
_disableSelectionChangingEvent = false;
|
||||
return;
|
||||
}
|
||||
|
||||
var set = new HashSet<object>();
|
||||
foreach (var c in selected)
|
||||
set.Add(c);
|
||||
if (ViewMode == Models.ChangeViewMode.Tree)
|
||||
{
|
||||
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>();
|
||||
foreach (var node in tree.Source.Items)
|
||||
CollectSelectedNodeByChange(nodes, node as ChangeTreeNode, set);
|
||||
var tree = new ChangeCollectionAsTree();
|
||||
tree.Tree = ChangeTreeNode.Build(changes, oldFolded);
|
||||
|
||||
if (nodes.Count == 0)
|
||||
treeSelection.Clear();
|
||||
var rows = new List<ChangeTreeNode>();
|
||||
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
|
||||
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,
|
||||
ShowColumnHeaders = false,
|
||||
CanUserResizeColumns = false,
|
||||
CanUserSortColumns = false,
|
||||
Source = source,
|
||||
};
|
||||
if (node.IsFolder)
|
||||
{
|
||||
var posX = e.GetPosition(this).X;
|
||||
if (posX < node.Depth * 16 + 16)
|
||||
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)
|
||||
|
@ -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)
|
||||
{
|
||||
var old = SelectedChanges;
|
||||
|
||||
if (old == null && changes.Count == 0)
|
||||
return;
|
||||
|
||||
|
@ -379,11 +457,12 @@ namespace SourceGit.Views
|
|||
return;
|
||||
}
|
||||
|
||||
_isSelecting = true;
|
||||
_disableSelectionChangingEvent = true;
|
||||
SetCurrentValue(SelectedChangesProperty, changes);
|
||||
_isSelecting = false;
|
||||
_disableSelectionChangingEvent = false;
|
||||
}
|
||||
|
||||
private bool _isSelecting = false;
|
||||
private bool _disableSelectionChangingEvent = false;
|
||||
private object _displayContext = null;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -47,6 +47,7 @@
|
|||
<!-- Changes -->
|
||||
<Border Grid.Row="1" Margin="0,4,0,0" BorderBrush="{DynamicResource Brush.Border2}" BorderThickness="1" Background="{DynamicResource Brush.Contents}">
|
||||
<v:ChangeCollectionView IsWorkingCopyChange="False"
|
||||
SelectionMode="Single"
|
||||
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=CommitChangeViewMode}"
|
||||
Changes="{Binding VisibleChanges}"
|
||||
SelectedChanges="{Binding SelectedChanges, Mode=TwoWay}"
|
||||
|
|
|
@ -10,7 +10,7 @@ namespace SourceGit.Views
|
|||
public class EnhancedTextBox : TextBox
|
||||
{
|
||||
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
|
||||
{
|
||||
|
|
|
@ -170,7 +170,7 @@ namespace SourceGit.Views
|
|||
if (subtree != null && subtree.Count > 0)
|
||||
{
|
||||
var subrows = new List<RevisionFileTreeNode>();
|
||||
MakeRows(subrows, node.Children, depth + 1);
|
||||
MakeRows(subrows, subtree, depth + 1);
|
||||
_rows.InsertRange(idx + 1, subrows);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -65,7 +65,7 @@
|
|||
<!-- Unstaged Changes -->
|
||||
<v:ChangeCollectionView Grid.Row="1"
|
||||
IsWorkingCopyChange="True"
|
||||
SingleSelect="False"
|
||||
SelectionMode="Multiple"
|
||||
Background="{DynamicResource Brush.Contents}"
|
||||
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=UnstagedChangeViewMode}"
|
||||
Changes="{Binding Unstaged}"
|
||||
|
@ -99,7 +99,7 @@
|
|||
<!-- Staged Changes -->
|
||||
<v:ChangeCollectionView Grid.Row="3"
|
||||
IsWorkingCopyChange="False"
|
||||
SingleSelect="False"
|
||||
SelectionMode="Multiple"
|
||||
Background="{DynamicResource Brush.Contents}"
|
||||
ViewMode="{Binding Source={x:Static vm:Preference.Instance}, Path=StagedChangeViewMode}"
|
||||
Changes="{Binding Staged}"
|
||||
|
|
Loading…
Reference in a new issue