mirror of
https://github.com/sourcegit-scm/sourcegit
synced 2025-05-21 12:15:00 +00:00
421 lines
15 KiB
C#
421 lines
15 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|