mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-21 20:24:59 +00:00
refactor: use TreeDataGrid instead of TreeView/DataGrid to improve performance (#148)
This commit is contained in:
parent
3160f1d142
commit
b192a1c423
24 changed files with 1333 additions and 1330 deletions
421
src/Models/TreeDataGridSelectionModel.cs
Normal file
421
src/Models/TreeDataGridSelectionModel.cs
Normal file
|
@ -0,0 +1,421 @@
|
|||
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)
|
||||
{
|
||||
var sets = new HashSet<TModel>();
|
||||
foreach (var item in items)
|
||||
sets.Add(item);
|
||||
|
||||
using (BatchUpdate())
|
||||
{
|
||||
Clear();
|
||||
|
||||
int num = _source.Rows.Count;
|
||||
for (int i = 0; i < num; ++i)
|
||||
{
|
||||
var m = _source.Rows[i].Model as TModel;
|
||||
if (m != null && sets.Contains(m))
|
||||
{
|
||||
var idx = _source.Rows.RowIndexToModelIndex(i);
|
||||
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);
|
||||
}
|
||||
else
|
||||
{
|
||||
var point = e.GetCurrentPoint(sender);
|
||||
if (point.Properties.IsRightButtonPressed)
|
||||
return;
|
||||
|
||||
if (e.KeyModifiers == KeyModifiers.Control)
|
||||
{
|
||||
Deselect(modelIndex);
|
||||
}
|
||||
else if (e.ClickCount == 2)
|
||||
{
|
||||
_rowDoubleTapped?.Invoke(this, e);
|
||||
}
|
||||
else
|
||||
{
|
||||
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)
|
||||
{
|
||||
return _childrenGetter?.Invoke(node);
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue