From 62cef2215a8a3bf0eee367d50a81e46d02c1eb52 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 16:09:26 +0200 Subject: [PATCH 01/23] Skeleton of cell selection model. --- .../ITreeDataGridCellSelectionModel.cs | 34 +++++++++++++++++++ .../ITreeDataGridColumnSelectionModel.cs | 11 ++++++ .../TreeDataGridCellSelectionModel.cs | 34 +++++++++++++++++++ .../TreeDataGridColumnSelectionModel.cs | 13 +++++++ 4 files changed, 92 insertions(+) create mode 100644 src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs create mode 100644 src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridColumnSelectionModel.cs create mode 100644 src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs create mode 100644 src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridColumnSelectionModel.cs diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs new file mode 100644 index 00000000..e115794c --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Models.TreeDataGrid; + +namespace Avalonia.Controls.Selection +{ + public interface ITreeDataGridCellSelectionModel : ITreeDataGridSelection + { + } + + public interface ITreeDataGridCellSelectionModel : ITreeDataGridCellSelectionModel + where T : class + { + /// + /// Gets or sets a value indicating whether only a single cell can be selected at a time. + /// + bool SingleSelect { get; set; } + + /// + /// Gets the currently selected cells. + /// + IReadOnlyList SelectedCells { get; } + + /// + /// Gets the currently selected columns. + /// + ITreeDataGridColumnSelectionModel SelectedColumns { get; } + + /// + /// Gets the currently selected rows. + /// + ITreeDataGridRowSelectionModel SelectedRows { get; } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridColumnSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridColumnSelectionModel.cs new file mode 100644 index 00000000..49a01cf7 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridColumnSelectionModel.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using Avalonia.Controls.Models.TreeDataGrid; + +namespace Avalonia.Controls.Selection +{ + public interface ITreeDataGridColumnSelectionModel : ISelectionModel + { + new IReadOnlyList SelectedItems { get; } + new IColumn? SelectedItem { get; set; } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs new file mode 100644 index 00000000..3bf0067f --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -0,0 +1,34 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using Avalonia.Controls.Models.TreeDataGrid; + +namespace Avalonia.Controls.Selection +{ + public class TreeDataGridCellSelectionModel : ITreeDataGridCellSelectionModel + where TModel : class + { + public TreeDataGridCellSelectionModel(ITreeDataGridSource source) + { + SelectedCells = Array.Empty(); + SelectedColumns = new TreeDataGridColumnSelectionModel(source.Columns); + SelectedRows = new TreeDataGridRowSelectionModel(source); + } + + public bool SingleSelect + { + get => SelectedRows.SingleSelect; + set => SelectedColumns.SingleSelect = SelectedRows.SingleSelect = value; + } + + public IReadOnlyList SelectedCells { get; } + public ITreeDataGridColumnSelectionModel SelectedColumns { get; } + public ITreeDataGridRowSelectionModel SelectedRows { get; } + + IEnumerable? ITreeDataGridSelection.Source + { + get => ((ITreeDataGridSelection)SelectedRows).Source; + set => ((ITreeDataGridSelection)SelectedRows).Source = value; + } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridColumnSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridColumnSelectionModel.cs new file mode 100644 index 00000000..f24d6200 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridColumnSelectionModel.cs @@ -0,0 +1,13 @@ +using Avalonia.Controls.Models.TreeDataGrid; + +namespace Avalonia.Controls.Selection +{ + public class TreeDataGridColumnSelectionModel : SelectionModel, + ITreeDataGridColumnSelectionModel + { + public TreeDataGridColumnSelectionModel(IColumns columns) + : base(columns) + { + } + } +} From c0b1772826dd957376f8f4bba21cc66d740a0dc1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 16:59:54 +0200 Subject: [PATCH 02/23] Refactored how selection gets passed to rows. --- .../Primitives/TreeDataGridRow.cs | 18 ++++-- .../Primitives/TreeDataGridRowsPresenter.cs | 64 ++++--------------- .../Themes/FluentControls.axaml | 3 +- .../TreeDataGrid.cs | 38 ++++++----- 4 files changed, 49 insertions(+), 74 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs index e544b830..2c16cd52 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs @@ -1,12 +1,14 @@ using System; using Avalonia.Controls.Metadata; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; using Avalonia.Input; +using Avalonia.LogicalTree; namespace Avalonia.Controls.Primitives { [PseudoClasses(":selected")] - public class TreeDataGridRow : TemplatedControl, ISelectable + public class TreeDataGridRow : TemplatedControl { private const double DragDistance = 3; private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity); @@ -25,8 +27,7 @@ public class TreeDataGridRow : TemplatedControl, ISelectable public static readonly DirectProperty IsSelectedProperty = AvaloniaProperty.RegisterDirect( nameof(IsSelected), - o => o.IsSelected, - (o, v) => o.IsSelected = v); + o => o.IsSelected); public static readonly DirectProperty RowsProperty = AvaloniaProperty.RegisterDirect( @@ -54,7 +55,7 @@ public TreeDataGridElementFactory? ElementFactory public bool IsSelected { get => _isSelected; - set => SetAndRaise(IsSelectedProperty, ref _isSelected, value); + private set => SetAndRaise(IsSelectedProperty, ref _isSelected, value); } public object? Model => DataContext; @@ -70,6 +71,7 @@ public IRows? Rows public void Realize( TreeDataGridElementFactory? elementFactory, + ITreeDataGridSelectionInteraction? selection, IColumns? columns, IRows? rows, int rowIndex) @@ -78,7 +80,9 @@ public void Realize( Columns = columns; Rows = rows; DataContext = rows?[rowIndex].Model; + IsSelected = selection?.IsRowSelected(rowIndex) ?? false; UpdateIndex(rowIndex); + UpdateSelection(selection); } public Control? TryGetCell(int columnIndex) @@ -96,6 +100,7 @@ public void Unrealize() { RowIndex = -1; DataContext = null; + IsSelected = false; CellsPresenter?.Unrealize(); } @@ -142,5 +147,10 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang base.OnPropertyChanged(change); } + + internal void UpdateSelection(ITreeDataGridSelectionInteraction? selection) + { + IsSelected = selection?.IsRowSelected(RowIndex) ?? false; + } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs index 9adf6b21..6a1f8222 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs @@ -1,7 +1,6 @@ using System; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Selection; -using Avalonia.Data; using Avalonia.Layout; using Avalonia.LogicalTree; @@ -15,14 +14,7 @@ public class TreeDataGridRowsPresenter : TreeDataGridPresenterBase, IChild o => o.Columns, (o, v) => o.Columns = v); - public static readonly DirectProperty SelectionProperty = - AvaloniaProperty.RegisterDirect( - nameof(Selection), - o => o.Selection, - (o, v) => o.Selection = v); - private IColumns? _columns; - private ITreeDataGridSelectionInteraction? _selection; public event EventHandler? ChildIndexChanged; @@ -32,35 +24,6 @@ public IColumns? Columns set => SetAndRaise(ColumnsProperty, ref _columns, value); } - public ITreeDataGridSelectionInteraction? Selection - { - get => _selection; - set - { - if (_selection != value) - { - var oldValue = _selection; - - if (_selection is object) - { - _selection.SelectionChanged -= OnSelectionChanged; - } - - _selection = value; - - if (_selection is object) - { - _selection.SelectionChanged += OnSelectionChanged; - } - - RaisePropertyChanged( - SelectionProperty, - oldValue, - _selection); - } - } - } - protected override Orientation Orientation => Orientation.Vertical; protected override (int index, double position) GetElementAt(double position) @@ -71,8 +34,7 @@ protected override (int index, double position) GetElementAt(double position) protected override void RealizeElement(Control element, IRow rowModel, int index) { var row = (TreeDataGridRow)element; - row.Realize(ElementFactory, Columns, (IRows?)Items, index); - row.IsSelected = _selection?.IsRowSelected(rowModel) == true; + row.Realize(ElementFactory, GetSelection(), Columns, (IRows?)Items, index); ChildIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index)); } @@ -88,17 +50,6 @@ protected override void UnrealizeElement(Control element) ChildIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, ((TreeDataGridRow)element).RowIndex)); } - private void UpdateSelection() - { - foreach (var element in VisualChildren) - { - if (element is TreeDataGridRow { RowIndex: >= 0 } row) - { - row.IsSelected = _selection?.IsRowSelected(row.RowIndex) == true; - } - } - } - protected override Size ArrangeOverride(Size finalSize) { Columns?.CommitActualWidths(); @@ -127,6 +78,15 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang base.OnPropertyChanged(change); } + internal void UpdateSelection(ITreeDataGridSelectionInteraction? selection) + { + foreach (var element in VisualChildren) + { + if (element is TreeDataGridRow { RowIndex: >= 0 } row) + row.UpdateSelection(selection); + } + } + private void OnColumnLayoutInvalidated(object? sender, EventArgs e) { InvalidateMeasure(); @@ -138,9 +98,9 @@ private void OnColumnLayoutInvalidated(object? sender, EventArgs e) } } - private void OnSelectionChanged(object? sender, EventArgs e) + private ITreeDataGridSelectionInteraction? GetSelection() { - UpdateSelection(); + return (TemplatedParent as TreeDataGrid)?.SelectionInteraction ?? null; } public int GetChildIndex(ILogical child) diff --git a/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml b/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml index 1ff97953..fea15f12 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml +++ b/src/Avalonia.Controls.TreeDataGrid/Themes/FluentControls.axaml @@ -33,8 +33,7 @@ + Items="{TemplateBinding Rows}"/> diff --git a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs index 5b97beb6..bff13329 100644 --- a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs +++ b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs @@ -180,31 +180,26 @@ public ITreeDataGridSource? Source { if (_source != value) { - if (value != null) - { - value.Sorted += Source_Sorted; - } - + if (SelectionInteraction != null) + SelectionInteraction.SelectionChanged -= OnSelectionInteractionChanged; if (_source != null) - { - _source.Sorted -= Source_Sorted; - } - - void Source_Sorted() - { - RowsPresenter?.RecycleAllElements(); - RowsPresenter?.InvalidateMeasure(); - } + _source.Sorted -= OnSourceSorted; var oldSource = _source; _source = value; Columns = _source?.Columns; Rows = _source?.Rows; - SelectionInteraction = value?.Selection as ITreeDataGridSelectionInteraction; + SelectionInteraction = _source?.Selection as ITreeDataGridSelectionInteraction; + + if (_source != null) + _source.Sorted += OnSourceSorted; + if (SelectionInteraction != null) + SelectionInteraction.SelectionChanged += OnSelectionInteractionChanged; + RaisePropertyChanged( SourceProperty, oldSource, - oldSource); + _source); } } } @@ -681,6 +676,17 @@ private void OnAutoScrollTick(object? sender, EventArgs e) } } + private void OnSelectionInteractionChanged(object? sender, EventArgs e) + { + RowsPresenter?.UpdateSelection(SelectionInteraction); + } + + private void OnSourceSorted() + { + RowsPresenter?.RecycleAllElements(); + RowsPresenter?.InvalidateMeasure(); + } + private static TreeDataGridRowDropPosition GetDropPosition( ITreeDataGridSource source, DragEventArgs e, From f36045f7e7d041268d0c8dbc5d34c745af85d11d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 17:19:57 +0200 Subject: [PATCH 03/23] Implemented cell visual selection. --- .../Primitives/ITreeDataGridCell.cs | 9 ++++- .../Primitives/TreeDataGridCell.cs | 36 ++++++++++++++++++- .../Primitives/TreeDataGridCellsPresenter.cs | 20 +++++++++-- .../Primitives/TreeDataGridCheckBoxCell.cs | 10 ++++-- .../Primitives/TreeDataGridExpanderCell.cs | 18 ++++++++-- .../Primitives/TreeDataGridRow.cs | 1 + .../Primitives/TreeDataGridRowsPresenter.cs | 5 +-- .../Primitives/TreeDataGridTemplateCell.cs | 10 ++++-- .../Primitives/TreeDataGridTextCell.cs | 10 ++++-- .../ITreeDataGridSelectionInteraction.cs | 5 +-- .../TreeDataGridCellSelectionModel.cs | 11 +++++- .../Themes/Fluent.axaml | 6 +++- 12 files changed, 122 insertions(+), 19 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/ITreeDataGridCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/ITreeDataGridCell.cs index 6c512efa..67e574c4 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/ITreeDataGridCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/ITreeDataGridCell.cs @@ -1,4 +1,5 @@ using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; namespace Avalonia.Controls.Primitives { @@ -6,7 +7,13 @@ internal interface ITreeDataGridCell { int ColumnIndex { get; } - void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex); + void Realize( + TreeDataGridElementFactory factory, + ITreeDataGridSelectionInteraction? selection, + ICell model, + int columnIndex, + int rowIndex); + void Unrealize(); } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs index 11d63235..44fb05b1 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs @@ -2,6 +2,7 @@ using System.ComponentModel; using Avalonia.Controls.Metadata; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; using Avalonia.Input; using Avalonia.LogicalTree; @@ -10,7 +11,13 @@ namespace Avalonia.Controls.Primitives [PseudoClasses(":editing")] public abstract class TreeDataGridCell : TemplatedControl, ITreeDataGridCell { + public static readonly DirectProperty IsSelectedProperty = + AvaloniaProperty.RegisterDirect( + nameof(IsSelected), + o => o.IsSelected); + private bool _isEditing; + private bool _isSelected; private TreeDataGrid? _treeDataGrid; private Point _pressedPoint; @@ -24,7 +31,18 @@ static TreeDataGridCell() public int RowIndex { get; private set; } = -1; public ICell? Model { get; private set; } - public virtual void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex) + public bool IsSelected + { + get => _isSelected; + private set => SetAndRaise(IsSelectedProperty, ref _isSelected, value); + } + + public virtual void Realize( + TreeDataGridElementFactory factory, + ITreeDataGridSelectionInteraction? selection, + ICell model, + int columnIndex, + int rowIndex) { if (columnIndex < 0) throw new IndexOutOfRangeException("Invalid column index."); @@ -32,6 +50,7 @@ public virtual void Realize(TreeDataGridElementFactory factory, ICell model, int ColumnIndex = columnIndex; RowIndex = rowIndex; Model = model; + IsSelected = selection?.IsCellSelected(columnIndex, rowIndex) ?? false; _treeDataGrid?.RaiseCellPrepared(this, columnIndex, RowIndex); } @@ -167,6 +186,21 @@ protected override void OnPointerReleased(PointerReleasedEventArgs e) } } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + if (change.Property == IsSelectedProperty) + { + PseudoClasses.Set(":selected", IsSelected); + } + + base.OnPropertyChanged(change); + } + + internal void UpdateSelection(ITreeDataGridSelectionInteraction? selection) + { + IsSelected = selection?.IsCellSelected(ColumnIndex, RowIndex) ?? false; + } + private bool EndEditCore() { if (_isEditing) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs index 434a7619..c6f90f7a 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs @@ -1,8 +1,10 @@ using System; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; using Avalonia.Layout; using Avalonia.LogicalTree; using Avalonia.Media; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -61,7 +63,7 @@ protected override Control GetElementFromFactory(IColumn column, int index) { var model = _rows!.RealizeCell(column, index, RowIndex); var cell = (TreeDataGridCell)GetElementFromFactory(model, index, this); - cell.Realize(ElementFactory!, model, index, RowIndex); + cell.Realize(ElementFactory!, GetSelection(), model, index, RowIndex); return cell; } @@ -76,7 +78,7 @@ protected override void RealizeElement(Control element, IColumn column, int inde else if (cell.ColumnIndex == -1 && cell.RowIndex == -1) { var model = _rows!.RealizeCell(column, index, RowIndex); - ((TreeDataGridCell)element).Realize(ElementFactory!, model, index, RowIndex); + ((TreeDataGridCell)element).Realize(ElementFactory!, GetSelection(), model, index, RowIndex); ChildIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element, index)); } else @@ -105,6 +107,20 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang if (change.Property == BackgroundProperty) InvalidateVisual(); } + + internal void UpdateSelection(ITreeDataGridSelectionInteraction? selection) + { + foreach (var element in RealizedElements) + { + if (element is TreeDataGridCell { RowIndex: >= 0, ColumnIndex: >= 0 } cell) + cell.UpdateSelection(selection); + } + } + + private ITreeDataGridSelectionInteraction? GetSelection() + { + return this.FindAncestorOfType()?.SelectionInteraction; + } public int GetChildIndex(ILogical child) { diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCheckBoxCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCheckBoxCell.cs index 5538c87f..3ba575ac 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCheckBoxCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCheckBoxCell.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; using Avalonia.Input; namespace Avalonia.Controls.Primitives @@ -50,7 +51,12 @@ public bool? Value } } - public override void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex) + public override void Realize( + TreeDataGridElementFactory factory, + ITreeDataGridSelectionInteraction? selection, + ICell model, + int columnIndex, + int rowIndex) { if (model is CheckBoxCell cell) { @@ -63,7 +69,7 @@ public override void Realize(TreeDataGridElementFactory factory, ICell model, in throw new InvalidOperationException("Invalid cell model."); } - base.Realize(factory, model, columnIndex, rowIndex); + base.Realize(factory, selection, model, columnIndex, rowIndex); } protected override void OnPointerPressed(PointerPressedEventArgs e) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs index 3ed53072..016922b9 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs @@ -1,6 +1,8 @@ using System; using System.ComponentModel; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -48,7 +50,12 @@ public bool ShowExpander private set => SetAndRaise(ShowExpanderProperty, ref _showExpander, value); } - public override void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex) + public override void Realize( + TreeDataGridElementFactory factory, + ITreeDataGridSelectionInteraction? selection, + ICell model, + int columnIndex, + int rowIndex) { if (_model is object) throw new InvalidOperationException("Cell is already realized."); @@ -73,7 +80,7 @@ public override void Realize(TreeDataGridElementFactory factory, ICell model, in throw new InvalidOperationException("Invalid cell model."); } - base.Realize(factory, model, columnIndex, rowIndex); + base.Realize(factory, selection, model, columnIndex, rowIndex); UpdateContent(_factory); } @@ -112,7 +119,7 @@ private void UpdateContent(TreeDataGridElementFactory factory) } if (_contentContainer.Child is ITreeDataGridCell innerCell) - innerCell.Realize(factory, innerModel, ColumnIndex, RowIndex); + innerCell.Realize(factory, GetSelection(), innerModel, ColumnIndex, RowIndex); } else if (_contentContainer.Child is ITreeDataGridCell innerCell) { @@ -120,6 +127,11 @@ private void UpdateContent(TreeDataGridElementFactory factory) } } + private ITreeDataGridSelectionInteraction? GetSelection() + { + return this.FindAncestorOfType()?.SelectionInteraction; + } + private void ModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (_model is null) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs index 2c16cd52..8c2f6d5e 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRow.cs @@ -151,6 +151,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang internal void UpdateSelection(ITreeDataGridSelectionInteraction? selection) { IsSelected = selection?.IsRowSelected(RowIndex) ?? false; + CellsPresenter?.UpdateSelection(selection); } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs index 6a1f8222..1808542b 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridRowsPresenter.cs @@ -3,6 +3,7 @@ using Avalonia.Controls.Selection; using Avalonia.Layout; using Avalonia.LogicalTree; +using Avalonia.VisualTree; namespace Avalonia.Controls.Primitives { @@ -80,7 +81,7 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang internal void UpdateSelection(ITreeDataGridSelectionInteraction? selection) { - foreach (var element in VisualChildren) + foreach (var element in RealizedElements) { if (element is TreeDataGridRow { RowIndex: >= 0 } row) row.UpdateSelection(selection); @@ -100,7 +101,7 @@ private void OnColumnLayoutInvalidated(object? sender, EventArgs e) private ITreeDataGridSelectionInteraction? GetSelection() { - return (TemplatedParent as TreeDataGrid)?.SelectionInteraction ?? null; + return this.FindAncestorOfType()?.SelectionInteraction; } public int GetChildIndex(ILogical child) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTemplateCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTemplateCell.cs index 9ca4500f..75cadc5b 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTemplateCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTemplateCell.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; @@ -31,10 +32,15 @@ public IDataTemplate? ContentTemplate set => SetAndRaise(ContentTemplateProperty, ref _contentTemplate, value); } - public override void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex) + public override void Realize( + TreeDataGridElementFactory factory, + ITreeDataGridSelectionInteraction? selection, + ICell model, + int columnIndex, + int rowIndex) { DataContext = model; - base.Realize(factory, model, columnIndex, rowIndex); + base.Realize(factory, selection, model, columnIndex, rowIndex); } public override void Unrealize() diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs index edf4a6d0..baf96e41 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridTextCell.cs @@ -1,5 +1,6 @@ using System.ComponentModel; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Selection; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Media; @@ -42,12 +43,17 @@ public string? Value protected override bool CanEdit => _canEdit; - public override void Realize(TreeDataGridElementFactory factory, ICell model, int columnIndex, int rowIndex) + public override void Realize( + TreeDataGridElementFactory factory, + ITreeDataGridSelectionInteraction? selection, + ICell model, + int columnIndex, + int rowIndex) { _canEdit = model.CanEdit; Value = model.Value?.ToString(); TextTrimming = (model as ITextCell)?.TextTrimming ?? TextTrimming.CharacterEllipsis; - base.Realize(factory, model, columnIndex, rowIndex); + base.Realize(factory, selection, model, columnIndex, rowIndex); SubscribeToModelChanges(); } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs index c2200050..e0bdd0df 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridSelectionInteraction.cs @@ -12,8 +12,9 @@ public interface ITreeDataGridSelectionInteraction { public event EventHandler? SelectionChanged; - bool IsRowSelected(IRow rowModel); - bool IsRowSelected(int rowIndex); + bool IsCellSelected(int columnIndex, int rowIndex) => false; + bool IsRowSelected(IRow rowModel) => false; + bool IsRowSelected(int rowIndex) => false; public void OnKeyDown(TreeDataGrid sender, KeyEventArgs e) { } public void OnPreviewKeyDown(TreeDataGrid sender, KeyEventArgs e) { } public void OnKeyUp(TreeDataGrid sender, KeyEventArgs e) { } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 3bf0067f..4bac4d2c 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -5,9 +5,12 @@ namespace Avalonia.Controls.Selection { - public class TreeDataGridCellSelectionModel : ITreeDataGridCellSelectionModel + public class TreeDataGridCellSelectionModel : ITreeDataGridCellSelectionModel, + ITreeDataGridSelectionInteraction where TModel : class { + private EventHandler? _viewSelectionChanged; + public TreeDataGridCellSelectionModel(ITreeDataGridSource source) { SelectedCells = Array.Empty(); @@ -30,5 +33,11 @@ public bool SingleSelect get => ((ITreeDataGridSelection)SelectedRows).Source; set => ((ITreeDataGridSelection)SelectedRows).Source = value; } + + event EventHandler? ITreeDataGridSelectionInteraction.SelectionChanged + { + add => _viewSelectionChanged += value; + remove => _viewSelectionChanged -= value; + } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Themes/Fluent.axaml b/src/Avalonia.Controls.TreeDataGrid/Themes/Fluent.axaml index 26ff2884..847c6a33 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Themes/Fluent.axaml +++ b/src/Avalonia.Controls.TreeDataGrid/Themes/Fluent.axaml @@ -13,5 +13,9 @@ - + + + From 227281d5436f0878f67c9a36ac7115efd7ffeafe Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 17:48:09 +0200 Subject: [PATCH 04/23] Initial single cell selection. --- .../TreeDataGridCellSelectionModel.cs | 45 +++++++++++++++++++ .../TreeDataGrid.cs | 14 ++++++ 2 files changed, 59 insertions(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 4bac4d2c..159df809 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Input; namespace Avalonia.Controls.Selection { @@ -9,10 +10,14 @@ public class TreeDataGridCellSelectionModel : ITreeDataGridCellSelection ITreeDataGridSelectionInteraction where TModel : class { + private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity); + private readonly ITreeDataGridSource _source; private EventHandler? _viewSelectionChanged; + private Point _pressedPoint = s_InvalidPoint; public TreeDataGridCellSelectionModel(ITreeDataGridSource source) { + _source = source; SelectedCells = Array.Empty(); SelectedColumns = new TreeDataGridColumnSelectionModel(source.Columns); SelectedRows = new TreeDataGridRowSelectionModel(source); @@ -39,5 +44,45 @@ event EventHandler? ITreeDataGridSelectionInteraction.SelectionChanged add => _viewSelectionChanged += value; remove => _viewSelectionChanged -= value; } + + public void Select(int columnIndex, IndexPath rowIndex) + { + SelectedColumns.Select(columnIndex); + SelectedRows.Select(rowIndex); + _viewSelectionChanged?.Invoke(this, EventArgs.Empty); + } + + bool ITreeDataGridSelectionInteraction.IsCellSelected(int columnIndex, int rowIndex) + { + return IsSelected(columnIndex, rowIndex); + } + + void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e) + { + // Select a cell on pointer pressed if: + // + // - It's a mouse click, not touch: we don't want to select on touch scroll gesture start + // + // Otherwise select on pointer release. + if (!e.Handled && + e.Pointer.Type == PointerType.Mouse && + e.Source is Control source && + sender.TryGetCell(source, out var cell) && + _source.Rows.RowIndexToModelIndex(cell.RowIndex) is { } modelIndex) + { + Select(cell.ColumnIndex, modelIndex); + } + else + { + _pressedPoint = e.GetPosition(sender); + } + } + + private bool IsSelected(int columnIndex, int rowIndex) + { + if (_source.Rows.RowIndexToModelIndex(rowIndex) is { } modelIndex) + return SelectedColumns.IsSelected(columnIndex) && SelectedRows.IsSelected(rowIndex); + return false; + } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs index bff13329..75be16e5 100644 --- a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs +++ b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs @@ -243,6 +243,20 @@ public event EventHandler? RowDrop return RowsPresenter?.TryGetElement(rowIndex) as TreeDataGridRow; } + public bool TryGetCell(Control? element, [MaybeNullWhen(false)] out TreeDataGridCell result) + { + if (element.FindAncestorOfType(true) is { } cell && + cell.ColumnIndex >= 0 && + cell.RowIndex >= 0) + { + result = cell; + return true; + } + + result = null; + return false; + } + public bool TryGetRow(Control? element, [MaybeNullWhen(false)] out TreeDataGridRow result) { if (element is TreeDataGridRow row && row.RowIndex >= 0) From 24c3649f4090343c086a835e87c59d3b11a51de1 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 18:36:37 +0200 Subject: [PATCH 05/23] Initial multiple cell selection. --- .../TreeDataGridCellSelectionModel.cs | 108 ++++++++++++++++-- 1 file changed, 101 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 159df809..980e86ee 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -2,6 +2,7 @@ using System.Collections; using System.Collections.Generic; using Avalonia.Controls.Models.TreeDataGrid; +using Avalonia.Controls.Primitives; using Avalonia.Input; namespace Avalonia.Controls.Selection @@ -23,6 +24,8 @@ public TreeDataGridCellSelectionModel(ITreeDataGridSource source) SelectedRows = new TreeDataGridRowSelectionModel(source); } + public int Count => SelectedColumns.Count * SelectedRows.Count; + public bool SingleSelect { get => SelectedRows.SingleSelect; @@ -45,10 +48,15 @@ event EventHandler? ITreeDataGridSelectionInteraction.SelectionChanged remove => _viewSelectionChanged -= value; } + private bool IsSelected(int columnIndex, IndexPath rowIndex) + { + return SelectedColumns.IsSelected(columnIndex) && SelectedRows.IsSelected(rowIndex); + } + public void Select(int columnIndex, IndexPath rowIndex) { - SelectedColumns.Select(columnIndex); - SelectedRows.Select(rowIndex); + SelectedColumns.SelectedIndex = columnIndex; + SelectedRows.SelectedIndex = rowIndex; _viewSelectionChanged?.Invoke(this, EventArgs.Empty); } @@ -62,15 +70,17 @@ void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, Poi // Select a cell on pointer pressed if: // // - It's a mouse click, not touch: we don't want to select on touch scroll gesture start + // - The cell isn't already selected: we don't want to deselect an existing multiple selection + // if the user is trying to drag multiple cells // // Otherwise select on pointer release. if (!e.Handled && e.Pointer.Type == PointerType.Mouse && e.Source is Control source && sender.TryGetCell(source, out var cell) && - _source.Rows.RowIndexToModelIndex(cell.RowIndex) is { } modelIndex) + !IsSelected(cell.ColumnIndex, cell.RowIndex)) { - Select(cell.ColumnIndex, modelIndex); + PointerSelect(sender, cell, e); } else { @@ -78,11 +88,95 @@ e.Source is Control source && } } + void ITreeDataGridSelectionInteraction.OnPointerReleased(TreeDataGrid sender, PointerReleasedEventArgs e) + { + if (!e.Handled && + _pressedPoint != s_InvalidPoint && + e.Source is Control source && + sender.TryGetCell(source, out var cell)) + { + var p = e.GetPosition(sender); + if (Math.Abs(p.X - _pressedPoint.X) <= 3 || Math.Abs(p.Y - _pressedPoint.Y) <= 3) + PointerSelect(sender, cell, e); + } + } + + private void PointerSelect(TreeDataGrid sender, TreeDataGridCell cell, PointerEventArgs e) + { + var point = e.GetCurrentPoint(sender); + var isRightButton = point.Properties.PointerUpdateKind is PointerUpdateKind.RightButtonPressed or + PointerUpdateKind.RightButtonReleased; + + UpdateSelection( + sender, + cell.ColumnIndex, + cell.RowIndex, + rangeModifier: e.KeyModifiers.HasFlag(KeyModifiers.Shift), + rightButton: isRightButton); + e.Handled = true; + } + + private void UpdateSelection( + TreeDataGrid treeDataGrid, + int columnIndex, + int rowIndex, + bool rangeModifier = false, + bool rightButton = false) + { + var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); + + if (modelIndex == default) + return; + + var multi = !SingleSelect; + var range = multi && rangeModifier; + + if (rightButton) + { + if (IsSelected(columnIndex, modelIndex) == false && !treeDataGrid.QueryCancelSelection()) + Select(columnIndex, modelIndex); + } + else if (range) + { + if (!treeDataGrid.QueryCancelSelection()) + SelectFromAnchorTo(columnIndex, rowIndex); + } + else if (SelectedColumns.SelectedIndex != columnIndex || + SelectedRows.SelectedIndex != modelIndex || + Count > 1) + { + if (!treeDataGrid.QueryCancelSelection()) + Select(columnIndex, modelIndex); + } + } + + private void SelectFromAnchorTo(int columnIndex, int rowIndex) + { + var anchorColumnIndex = SelectedColumns.AnchorIndex; + var anchorModelIndex = SelectedRows.AnchorIndex; + var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(anchorModelIndex); + + SelectedColumns.BeginBatchUpdate(); + SelectedColumns.Clear(); + SelectedColumns.SelectRange(anchorColumnIndex, columnIndex); + SelectedColumns.EndBatchUpdate(); + + SelectedRows.BeginBatchUpdate(); + SelectedRows.Clear(); + for (var i = Math.Min(anchorRowIndex, rowIndex); i <= Math.Max(anchorRowIndex, rowIndex); ++i) + { + SelectedRows.Select(_source.Rows.RowIndexToModelIndex(i)); + } + SelectedRows.AnchorIndex = anchorRowIndex; + SelectedRows.EndBatchUpdate(); + + _viewSelectionChanged?.Invoke(this, EventArgs.Empty); + } + private bool IsSelected(int columnIndex, int rowIndex) { - if (_source.Rows.RowIndexToModelIndex(rowIndex) is { } modelIndex) - return SelectedColumns.IsSelected(columnIndex) && SelectedRows.IsSelected(rowIndex); - return false; + var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); + return SelectedColumns.IsSelected(columnIndex) && SelectedRows.IsSelected(modelIndex); } } } From a3dc5d75ee6413c20495fd889f6db63591ae94b9 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 21:04:00 +0200 Subject: [PATCH 06/23] Don't expose selected rows and columns. --- .../ITreeDataGridCellSelectionModel.cs | 10 ---- .../TreeDataGridCellSelectionModel.cs | 59 +++++++++++-------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs index e115794c..55739aa6 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs @@ -20,15 +20,5 @@ public interface ITreeDataGridCellSelectionModel : ITreeDataGridCellSelection /// Gets the currently selected cells. /// IReadOnlyList SelectedCells { get; } - - /// - /// Gets the currently selected columns. - /// - ITreeDataGridColumnSelectionModel SelectedColumns { get; } - - /// - /// Gets the currently selected rows. - /// - ITreeDataGridRowSelectionModel SelectedRows { get; } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 980e86ee..95616fb2 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -12,6 +12,8 @@ public class TreeDataGridCellSelectionModel : ITreeDataGridCellSelection where TModel : class { private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity); + private readonly ITreeDataGridColumnSelectionModel _selectedColumns; + private ITreeDataGridRowSelectionModel _selectedRows; private readonly ITreeDataGridSource _source; private EventHandler? _viewSelectionChanged; private Point _pressedPoint = s_InvalidPoint; @@ -20,26 +22,31 @@ public TreeDataGridCellSelectionModel(ITreeDataGridSource source) { _source = source; SelectedCells = Array.Empty(); - SelectedColumns = new TreeDataGridColumnSelectionModel(source.Columns); - SelectedRows = new TreeDataGridRowSelectionModel(source); + _selectedColumns = new TreeDataGridColumnSelectionModel(source.Columns); + _selectedRows = new TreeDataGridRowSelectionModel(source); + _selectedRows.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(_selectedRows.AnchorIndex)) + { + System.Diagnostics.Debug.WriteLine($"Row anchor changed to {_selectedRows.AnchorIndex}"); + } + }; } - public int Count => SelectedColumns.Count * SelectedRows.Count; + public int Count => _selectedColumns.Count * _selectedRows.Count; public bool SingleSelect { - get => SelectedRows.SingleSelect; - set => SelectedColumns.SingleSelect = SelectedRows.SingleSelect = value; + get => _selectedRows.SingleSelect; + set => _selectedColumns.SingleSelect = _selectedRows.SingleSelect = value; } public IReadOnlyList SelectedCells { get; } - public ITreeDataGridColumnSelectionModel SelectedColumns { get; } - public ITreeDataGridRowSelectionModel SelectedRows { get; } IEnumerable? ITreeDataGridSelection.Source { - get => ((ITreeDataGridSelection)SelectedRows).Source; - set => ((ITreeDataGridSelection)SelectedRows).Source = value; + get => ((ITreeDataGridSelection)_selectedRows).Source; + set => ((ITreeDataGridSelection)_selectedRows).Source = value; } event EventHandler? ITreeDataGridSelectionInteraction.SelectionChanged @@ -50,13 +57,13 @@ event EventHandler? ITreeDataGridSelectionInteraction.SelectionChanged private bool IsSelected(int columnIndex, IndexPath rowIndex) { - return SelectedColumns.IsSelected(columnIndex) && SelectedRows.IsSelected(rowIndex); + return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(rowIndex); } public void Select(int columnIndex, IndexPath rowIndex) { - SelectedColumns.SelectedIndex = columnIndex; - SelectedRows.SelectedIndex = rowIndex; + _selectedColumns.SelectedIndex = columnIndex; + _selectedRows.SelectedIndex = rowIndex; _viewSelectionChanged?.Invoke(this, EventArgs.Empty); } @@ -141,8 +148,8 @@ private void UpdateSelection( if (!treeDataGrid.QueryCancelSelection()) SelectFromAnchorTo(columnIndex, rowIndex); } - else if (SelectedColumns.SelectedIndex != columnIndex || - SelectedRows.SelectedIndex != modelIndex || + else if (_selectedColumns.SelectedIndex != columnIndex || + _selectedRows.SelectedIndex != modelIndex || Count > 1) { if (!treeDataGrid.QueryCancelSelection()) @@ -152,23 +159,23 @@ private void UpdateSelection( private void SelectFromAnchorTo(int columnIndex, int rowIndex) { - var anchorColumnIndex = SelectedColumns.AnchorIndex; - var anchorModelIndex = SelectedRows.AnchorIndex; + var anchorColumnIndex = _selectedColumns.AnchorIndex; + var anchorModelIndex = _selectedRows.AnchorIndex; var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(anchorModelIndex); - SelectedColumns.BeginBatchUpdate(); - SelectedColumns.Clear(); - SelectedColumns.SelectRange(anchorColumnIndex, columnIndex); - SelectedColumns.EndBatchUpdate(); + _selectedColumns.BeginBatchUpdate(); + _selectedColumns.Clear(); + _selectedColumns.SelectRange(anchorColumnIndex, columnIndex); + _selectedColumns.EndBatchUpdate(); - SelectedRows.BeginBatchUpdate(); - SelectedRows.Clear(); + _selectedRows.BeginBatchUpdate(); + _selectedRows.Clear(); for (var i = Math.Min(anchorRowIndex, rowIndex); i <= Math.Max(anchorRowIndex, rowIndex); ++i) { - SelectedRows.Select(_source.Rows.RowIndexToModelIndex(i)); + _selectedRows.Select(_source.Rows.RowIndexToModelIndex(i)); } - SelectedRows.AnchorIndex = anchorRowIndex; - SelectedRows.EndBatchUpdate(); + _selectedRows.AnchorIndex = anchorModelIndex; + _selectedRows.EndBatchUpdate(); _viewSelectionChanged?.Invoke(this, EventArgs.Empty); } @@ -176,7 +183,7 @@ private void SelectFromAnchorTo(int columnIndex, int rowIndex) private bool IsSelected(int columnIndex, int rowIndex) { var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); - return SelectedColumns.IsSelected(columnIndex) && SelectedRows.IsSelected(modelIndex); + return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(modelIndex); } } } From ce15bed927967fc53746567750e7dcc786e71cf6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 22:21:46 +0200 Subject: [PATCH 07/23] Added cell SelectionChanged event. --- .../Avalonia.Controls.TreeDataGrid.csproj | 1 + .../CellIndex.cs | 13 ++++ .../ITreeDataGridCellSelectionModel.cs | 20 ++++- .../Selection/SelectedCellIndexes.cs | 46 ++++++++++++ ...eeDataGridCellSelectionChangedEventArgs.cs | 20 +++++ .../TreeDataGridCellSelectionModel.cs | 73 ++++++++++++++----- 6 files changed, 152 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.Controls.TreeDataGrid/CellIndex.cs create mode 100644 src/Avalonia.Controls.TreeDataGrid/Selection/SelectedCellIndexes.cs create mode 100644 src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionChangedEventArgs.cs diff --git a/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj b/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj index e1ca2c40..d67e9d68 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj +++ b/src/Avalonia.Controls.TreeDataGrid/Avalonia.Controls.TreeDataGrid.csproj @@ -2,6 +2,7 @@ net5.0 True + 10 Avalonia.Controls diff --git a/src/Avalonia.Controls.TreeDataGrid/CellIndex.cs b/src/Avalonia.Controls.TreeDataGrid/CellIndex.cs new file mode 100644 index 00000000..a6a457d9 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/CellIndex.cs @@ -0,0 +1,13 @@ +namespace Avalonia.Controls +{ + /// + /// Represents a cell in a . + /// + /// + /// The index of the cell in the collection. + /// + /// + /// The hierarchical index of the row model in the data source. + /// + public readonly record struct CellIndex(int ColumnIndex, IndexPath RowIndex); +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs index 55739aa6..adf241c5 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs @@ -1,13 +1,22 @@ using System; using System.Collections.Generic; -using Avalonia.Controls.Models.TreeDataGrid; namespace Avalonia.Controls.Selection { + /// + /// Maintains the cell selection state for an . + /// public interface ITreeDataGridCellSelectionModel : ITreeDataGridSelection { + /// + /// Occurs when the cell selection changes. + /// + event EventHandler? SelectionChanged; } + /// + /// Maintains the cell selection state for an . + /// public interface ITreeDataGridCellSelectionModel : ITreeDataGridCellSelectionModel where T : class { @@ -17,8 +26,13 @@ public interface ITreeDataGridCellSelectionModel : ITreeDataGridCellSelection bool SingleSelect { get; set; } /// - /// Gets the currently selected cells. + /// Gets the indexes of the currently selected cells. + /// + IReadOnlyList SelectedIndexes { get; } + + /// + /// Occurs when the cell selection changes. /// - IReadOnlyList SelectedCells { get; } + new event EventHandler>? SelectionChanged; } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/SelectedCellIndexes.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/SelectedCellIndexes.cs new file mode 100644 index 00000000..b9b4b9bf --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/SelectedCellIndexes.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Controls.Selection +{ + internal class SelectedCellIndexes : IReadOnlyList + { + private readonly ITreeDataGridColumnSelectionModel _selectedColumns; + private readonly ITreeDataGridRowSelectionModel _selectedRows; + + public SelectedCellIndexes( + ITreeDataGridColumnSelectionModel selectedColumns, + ITreeDataGridRowSelectionModel selectedRows) + { + _selectedColumns = selectedColumns; + _selectedRows = selectedRows; + } + + public CellIndex this[int index] + { + get + { + if (index < 0 || index >= Count) + throw new IndexOutOfRangeException("The index was out of range."); + var column = _selectedColumns.SelectedIndexes[index % _selectedColumns.Count]; + var row = _selectedRows.SelectedIndexes[index / _selectedColumns.Count]; + return new(column, row); + } + } + + public int Count => _selectedColumns.Count * _selectedRows.Count; + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; ++i) + yield return this[i]; + } + + IEnumerator IEnumerable.GetEnumerator() + { + for (var i = 0; i < Count; ++i) + yield return this[i]; + } + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionChangedEventArgs.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionChangedEventArgs.cs new file mode 100644 index 00000000..8533d3a0 --- /dev/null +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionChangedEventArgs.cs @@ -0,0 +1,20 @@ +using System; + +namespace Avalonia.Controls.Selection +{ + /// + /// Provides data for the event. + /// + public class TreeDataGridCellSelectionChangedEventArgs : EventArgs + { + } + + /// + /// Provides data for the event. + /// + /// The model type. + public class TreeDataGridCellSelectionChangedEventArgs : TreeDataGridCellSelectionChangedEventArgs + where T : class + { + } +} diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 95616fb2..6e69c114 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -1,7 +1,6 @@ using System; using System.Collections; using System.Collections.Generic; -using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -13,24 +12,23 @@ public class TreeDataGridCellSelectionModel : ITreeDataGridCellSelection { private static readonly Point s_InvalidPoint = new(double.NegativeInfinity, double.NegativeInfinity); private readonly ITreeDataGridColumnSelectionModel _selectedColumns; - private ITreeDataGridRowSelectionModel _selectedRows; + private readonly ITreeDataGridRowSelectionModel _selectedRows; + private readonly SelectedCellIndexes _selectedIndexes; private readonly ITreeDataGridSource _source; + private EventHandler? _untypedSelectionChanged; private EventHandler? _viewSelectionChanged; private Point _pressedPoint = s_InvalidPoint; + private bool _columnsChanged; + private bool _rowsChanged; public TreeDataGridCellSelectionModel(ITreeDataGridSource source) { _source = source; - SelectedCells = Array.Empty(); _selectedColumns = new TreeDataGridColumnSelectionModel(source.Columns); _selectedRows = new TreeDataGridRowSelectionModel(source); - _selectedRows.PropertyChanged += (s, e) => - { - if (e.PropertyName == nameof(_selectedRows.AnchorIndex)) - { - System.Diagnostics.Debug.WriteLine($"Row anchor changed to {_selectedRows.AnchorIndex}"); - } - }; + _selectedColumns.SelectionChanged += OnSelectedColumnsSelectionChanged; + _selectedRows.SelectionChanged += OnSelectedRowsSelectionChanged; + _selectedIndexes = new(_selectedColumns, _selectedRows); } public int Count => _selectedColumns.Count * _selectedRows.Count; @@ -41,7 +39,7 @@ public bool SingleSelect set => _selectedColumns.SingleSelect = _selectedRows.SingleSelect = value; } - public IReadOnlyList SelectedCells { get; } + public IReadOnlyList SelectedIndexes => _selectedIndexes; IEnumerable? ITreeDataGridSelection.Source { @@ -49,12 +47,20 @@ public bool SingleSelect set => ((ITreeDataGridSelection)_selectedRows).Source = value; } + public event EventHandler>? SelectionChanged; + event EventHandler? ITreeDataGridSelectionInteraction.SelectionChanged { add => _viewSelectionChanged += value; remove => _viewSelectionChanged -= value; } + event EventHandler? ITreeDataGridCellSelectionModel.SelectionChanged + { + add => _untypedSelectionChanged += value; + remove => _untypedSelectionChanged -= value; + } + private bool IsSelected(int columnIndex, IndexPath rowIndex) { return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(rowIndex); @@ -62,9 +68,10 @@ private bool IsSelected(int columnIndex, IndexPath rowIndex) public void Select(int columnIndex, IndexPath rowIndex) { + BeginBatchUpdate(); _selectedColumns.SelectedIndex = columnIndex; _selectedRows.SelectedIndex = rowIndex; - _viewSelectionChanged?.Invoke(this, EventArgs.Empty); + EndBatchUpdate(); } bool ITreeDataGridSelectionInteraction.IsCellSelected(int columnIndex, int rowIndex) @@ -108,6 +115,27 @@ e.Source is Control source && } } + private void BeginBatchUpdate() + { + _selectedColumns.BeginBatchUpdate(); + _selectedRows.BeginBatchUpdate(); + } + + private void EndBatchUpdate() + { + _columnsChanged = _rowsChanged = false; + _selectedColumns.EndBatchUpdate(); + _selectedRows.EndBatchUpdate(); + + if (_columnsChanged || _rowsChanged) + { + var e = new TreeDataGridCellSelectionChangedEventArgs(); + _viewSelectionChanged?.Invoke(this, EventArgs.Empty); + SelectionChanged?.Invoke(this, e); + _untypedSelectionChanged?.Invoke(this, e); + } + } + private void PointerSelect(TreeDataGrid sender, TreeDataGridCell cell, PointerEventArgs e) { var point = e.GetCurrentPoint(sender); @@ -163,21 +191,20 @@ private void SelectFromAnchorTo(int columnIndex, int rowIndex) var anchorModelIndex = _selectedRows.AnchorIndex; var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(anchorModelIndex); - _selectedColumns.BeginBatchUpdate(); + BeginBatchUpdate(); + _selectedColumns.Clear(); _selectedColumns.SelectRange(anchorColumnIndex, columnIndex); - _selectedColumns.EndBatchUpdate(); - - _selectedRows.BeginBatchUpdate(); _selectedRows.Clear(); + for (var i = Math.Min(anchorRowIndex, rowIndex); i <= Math.Max(anchorRowIndex, rowIndex); ++i) { _selectedRows.Select(_source.Rows.RowIndexToModelIndex(i)); } + _selectedRows.AnchorIndex = anchorModelIndex; - _selectedRows.EndBatchUpdate(); - _viewSelectionChanged?.Invoke(this, EventArgs.Empty); + EndBatchUpdate(); } private bool IsSelected(int columnIndex, int rowIndex) @@ -185,5 +212,15 @@ private bool IsSelected(int columnIndex, int rowIndex) var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(modelIndex); } + + private void OnSelectedColumnsSelectionChanged(object? sender, SelectionModelSelectionChangedEventArgs e) + { + _columnsChanged = true; + } + + private void OnSelectedRowsSelectionChanged(object? sender, TreeSelectionModelSelectionChangedEventArgs e) + { + _rowsChanged = true; + } } } From 2645fc3595bad986b3122d7a51b422ccd43860e2 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 22:51:12 +0200 Subject: [PATCH 08/23] Implemented keyboard cell selection. --- .../TreeDataGridCellSelectionModel.cs | 59 ++++++++++++++++--- .../TreeDataGridRowSelectionModel.cs | 1 - 2 files changed, 52 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 6e69c114..537e305c 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -1,6 +1,7 @@ using System; using System.Collections; using System.Collections.Generic; +using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Primitives; using Avalonia.Input; @@ -18,6 +19,7 @@ public class TreeDataGridCellSelectionModel : ITreeDataGridCellSelection private EventHandler? _untypedSelectionChanged; private EventHandler? _viewSelectionChanged; private Point _pressedPoint = s_InvalidPoint; + private (int x, int y) _rangeAnchor = (-1, -1); private bool _columnsChanged; private bool _rowsChanged; @@ -61,17 +63,15 @@ event EventHandler? ITreeDataGridCell remove => _untypedSelectionChanged -= value; } - private bool IsSelected(int columnIndex, IndexPath rowIndex) + public bool IsSelected(int columnIndex, IndexPath rowIndex) { return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(rowIndex); } public void Select(int columnIndex, IndexPath rowIndex) { - BeginBatchUpdate(); - _selectedColumns.SelectedIndex = columnIndex; - _selectedRows.SelectedIndex = rowIndex; - EndBatchUpdate(); + var ri = _source.Rows.ModelIndexToRowIndex(rowIndex); + Select(columnIndex, ri, rowIndex); } bool ITreeDataGridSelectionInteraction.IsCellSelected(int columnIndex, int rowIndex) @@ -79,6 +79,35 @@ bool ITreeDataGridSelectionInteraction.IsCellSelected(int columnIndex, int rowIn return IsSelected(columnIndex, rowIndex); } + void ITreeDataGridSelectionInteraction.OnKeyDown(TreeDataGrid sender, KeyEventArgs e) + { + var direction = e.Key.ToNavigationDirection(); + var shift = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + + if (sender.RowsPresenter is null || + sender.Columns is null || + sender.Rows is null || + e.Handled || !direction.HasValue) + return; + + var (x, y) = direction switch + { + NavigationDirection.Up => (0, -1), + NavigationDirection.Down => (0, 1), + NavigationDirection.Left => (-1, 0), + NavigationDirection.Right => (1, 0), + _ => (0, 0) + }; + + var columnIndex = Math.Clamp(_rangeAnchor.x + x, 0, sender.Columns.Count - 1); + var rowIndex = Math.Clamp(_rangeAnchor.y + y, 0, sender.Rows.Count - 1); + + if (!shift) + Select(columnIndex, rowIndex); + else + SelectFromAnchorTo(columnIndex, rowIndex); + } + void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e) { // Select a cell on pointer pressed if: @@ -169,7 +198,7 @@ private void UpdateSelection( if (rightButton) { if (IsSelected(columnIndex, modelIndex) == false && !treeDataGrid.QueryCancelSelection()) - Select(columnIndex, modelIndex); + Select(columnIndex, rowIndex, modelIndex); } else if (range) { @@ -181,10 +210,25 @@ private void UpdateSelection( Count > 1) { if (!treeDataGrid.QueryCancelSelection()) - Select(columnIndex, modelIndex); + Select(columnIndex, rowIndex, modelIndex); } } + private void Select(int columnIndex, int rowIndex) + { + var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); + Select(columnIndex, rowIndex, modelIndex); + } + + private void Select(int columnIndex, int rowIndex, IndexPath modelndex) + { + BeginBatchUpdate(); + _selectedColumns.SelectedIndex = columnIndex; + _selectedRows.SelectedIndex = modelndex; + _rangeAnchor = (columnIndex, rowIndex); + EndBatchUpdate(); + } + private void SelectFromAnchorTo(int columnIndex, int rowIndex) { var anchorColumnIndex = _selectedColumns.AnchorIndex; @@ -203,6 +247,7 @@ private void SelectFromAnchorTo(int columnIndex, int rowIndex) } _selectedRows.AnchorIndex = anchorModelIndex; + _rangeAnchor = (columnIndex, rowIndex); EndBatchUpdate(); } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs index d80f1ffc..3489b99d 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridRowSelectionModel.cs @@ -113,7 +113,6 @@ void ITreeDataGridSelectionInteraction.OnKeyDown(TreeDataGrid sender, KeyEventAr } } } - } protected void HandleTextInput(string? text, TreeDataGrid treeDataGrid, int selectedRowIndex) From 49067e71c3efba85e247246af7c4b8aefc7de628 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 22:57:26 +0200 Subject: [PATCH 09/23] Added `CellSelection` accessors. --- src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs | 1 + .../HierarchicalTreeDataGridSource.cs | 1 + 2 files changed, 2 insertions(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs b/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs index 852a5741..13cb8f09 100644 --- a/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs +++ b/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs @@ -66,6 +66,7 @@ public ITreeDataGridSelection? Selection } } + public ITreeDataGridCellSelectionModel? CellSelection => Selection as ITreeDataGridCellSelectionModel; public ITreeDataGridRowSelectionModel? RowSelection => Selection as ITreeDataGridRowSelectionModel; public bool IsHierarchical => false; public bool IsSorted => _comparer is not null; diff --git a/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs b/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs index 0c32897f..f183703c 100644 --- a/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs +++ b/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs @@ -77,6 +77,7 @@ public ITreeDataGridSelection? Selection } } + public ITreeDataGridCellSelectionModel? CellSelection => Selection as ITreeDataGridCellSelectionModel; public ITreeDataGridRowSelectionModel? RowSelection => Selection as ITreeDataGridRowSelectionModel; public bool IsHierarchical => true; public bool IsSorted => _comparison is not null; From e5e75495fb8d830383574ab3ccdb425ea7fd742c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 23:00:10 +0200 Subject: [PATCH 10/23] We don't need to select inner cell. --- .../Primitives/TreeDataGridExpanderCell.cs | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs index 016922b9..bffafe63 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridExpanderCell.cs @@ -119,7 +119,7 @@ private void UpdateContent(TreeDataGridElementFactory factory) } if (_contentContainer.Child is ITreeDataGridCell innerCell) - innerCell.Realize(factory, GetSelection(), innerModel, ColumnIndex, RowIndex); + innerCell.Realize(factory, null, innerModel, ColumnIndex, RowIndex); } else if (_contentContainer.Child is ITreeDataGridCell innerCell) { @@ -127,11 +127,6 @@ private void UpdateContent(TreeDataGridElementFactory factory) } } - private ITreeDataGridSelectionInteraction? GetSelection() - { - return this.FindAncestorOfType()?.SelectionInteraction; - } - private void ModelPropertyChanged(object? sender, PropertyChangedEventArgs e) { if (_model is null) From 91ca59b15b008b0078d0ad13af42f52e90b7f481 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 23:02:26 +0200 Subject: [PATCH 11/23] Fix compile error in tests. --- tests/Avalonia.Controls.TreeDataGrid.Tests/TestTemplates.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/TestTemplates.cs b/tests/Avalonia.Controls.TreeDataGrid.Tests/TestTemplates.cs index b988b391..ab8d7be7 100644 --- a/tests/Avalonia.Controls.TreeDataGrid.Tests/TestTemplates.cs +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/TestTemplates.cs @@ -79,7 +79,6 @@ public static IControlTemplate TreeDataGridTemplate() [!TreeDataGridRowsPresenter.ColumnsProperty] = x[!TreeDataGrid.ColumnsProperty], [!TreeDataGridRowsPresenter.ElementFactoryProperty] = x[!TreeDataGrid.ElementFactoryProperty], [!TreeDataGridRowsPresenter.ItemsProperty] = x[!TreeDataGrid.RowsProperty], - [!TreeDataGridRowsPresenter.SelectionProperty] = x[!TreeDataGrid.SelectionInteractionProperty], }.RegisterInNameScope(ns), }.RegisterInNameScope(ns) } From f5d2d0e5123ceb66c0aeec65824872838b7ffd0b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 23:12:42 +0200 Subject: [PATCH 12/23] Expose selected cell index as property. --- .../ITreeDataGridCellSelectionModel.cs | 12 +++++++++++ .../TreeDataGridCellSelectionModel.cs | 20 +++++++++++-------- 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs index adf241c5..a3f52c63 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs @@ -25,6 +25,11 @@ public interface ITreeDataGridCellSelectionModel : ITreeDataGridCellSelection /// bool SingleSelect { get; set; } + /// + /// Gets or sets the index of the currently selected cell. + /// + CellIndex SelectedIndex { get; set; } + /// /// Gets the indexes of the currently selected cells. /// @@ -34,5 +39,12 @@ public interface ITreeDataGridCellSelectionModel : ITreeDataGridCellSelection /// Occurs when the cell selection changes. /// new event EventHandler>? SelectionChanged; + + /// + /// Checks whether the specified cell is selected. + /// + /// The column index of the cell. + /// The model index of the cell. + public bool IsSelected(int columnIndex, IndexPath rowIndex); } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 537e305c..3b8e6405 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -41,6 +41,16 @@ public bool SingleSelect set => _selectedColumns.SingleSelect = _selectedRows.SingleSelect = value; } + public CellIndex SelectedIndex + { + get => new(_selectedColumns.SelectedIndex, _selectedRows.SelectedIndex); + set + { + var rowIndex = _source.Rows.ModelIndexToRowIndex(value.RowIndex); + Select(value.ColumnIndex, rowIndex, value.RowIndex); + } + } + public IReadOnlyList SelectedIndexes => _selectedIndexes; IEnumerable? ITreeDataGridSelection.Source @@ -68,12 +78,6 @@ public bool IsSelected(int columnIndex, IndexPath rowIndex) return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(rowIndex); } - public void Select(int columnIndex, IndexPath rowIndex) - { - var ri = _source.Rows.ModelIndexToRowIndex(rowIndex); - Select(columnIndex, ri, rowIndex); - } - bool ITreeDataGridSelectionInteraction.IsCellSelected(int columnIndex, int rowIndex) { return IsSelected(columnIndex, rowIndex); @@ -237,9 +241,9 @@ private void SelectFromAnchorTo(int columnIndex, int rowIndex) BeginBatchUpdate(); - _selectedColumns.Clear(); + _selectedColumns.SelectedIndex = anchorColumnIndex; _selectedColumns.SelectRange(anchorColumnIndex, columnIndex); - _selectedRows.Clear(); + _selectedRows.SelectedIndex = anchorModelIndex; for (var i = Math.Min(anchorRowIndex, rowIndex); i <= Math.Max(anchorRowIndex, rowIndex); ++i) { From b35e4a3c761cfa63d95acf1c725f13a7d771a6df Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 23:24:14 +0200 Subject: [PATCH 13/23] Update row index on cells. --- .../Primitives/TreeDataGridCell.cs | 2 ++ .../Primitives/TreeDataGridCellsPresenter.cs | 7 +++++++ 2 files changed, 9 insertions(+) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs index 44fb05b1..52b41f74 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCell.cs @@ -196,6 +196,8 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang base.OnPropertyChanged(change); } + public void UpdateRowIndex(int index) => RowIndex = index; + internal void UpdateSelection(ITreeDataGridSelectionInteraction? selection) { IsSelected = selection?.IsCellSelected(ColumnIndex, RowIndex) ?? false; diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs index c6f90f7a..8a6627be 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridCellsPresenter.cs @@ -1,4 +1,5 @@ using System; +using System.Xml.Linq; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Selection; using Avalonia.Layout; @@ -51,6 +52,12 @@ public void UpdateRowIndex(int index) if (index < 0 || Rows is null || index >= Rows.Count) throw new ArgumentOutOfRangeException(nameof(index)); RowIndex = index; + + foreach (var element in RealizedElements) + { + if (element is TreeDataGridCell { RowIndex: >= 0, ColumnIndex: >= 0 } cell) + cell.UpdateRowIndex(index); + } } protected override Size MeasureElement(int index, Control element, Size availableSize) From f2787ceebf97339485f1adfdaa814da4c257fb6a Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 11 May 2023 23:30:00 +0200 Subject: [PATCH 14/23] Non-range select should move from anchor. Not range anchor. --- .../TreeDataGridCellSelectionModel.cs | 77 ++++++++++--------- 1 file changed, 42 insertions(+), 35 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 3b8e6405..489d5b09 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -103,8 +103,9 @@ sender.Rows is null || _ => (0, 0) }; - var columnIndex = Math.Clamp(_rangeAnchor.x + x, 0, sender.Columns.Count - 1); - var rowIndex = Math.Clamp(_rangeAnchor.y + y, 0, sender.Rows.Count - 1); + var anchor = shift ? _rangeAnchor : GetAnchor(); + var columnIndex = Math.Clamp(anchor.x + x, 0, sender.Columns.Count - 1); + var rowIndex = Math.Clamp(anchor.y + y, 0, sender.Rows.Count - 1); if (!shift) Select(columnIndex, rowIndex); @@ -184,38 +185,16 @@ private void PointerSelect(TreeDataGrid sender, TreeDataGridCell cell, PointerEv e.Handled = true; } - private void UpdateSelection( - TreeDataGrid treeDataGrid, - int columnIndex, - int rowIndex, - bool rangeModifier = false, - bool rightButton = false) + private (int x, int y) GetAnchor() { - var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); - - if (modelIndex == default) - return; - - var multi = !SingleSelect; - var range = multi && rangeModifier; + var anchorModelIndex = _selectedRows.AnchorIndex; + return (_selectedColumns.AnchorIndex, _source.Rows.ModelIndexToRowIndex(anchorModelIndex)); + } - if (rightButton) - { - if (IsSelected(columnIndex, modelIndex) == false && !treeDataGrid.QueryCancelSelection()) - Select(columnIndex, rowIndex, modelIndex); - } - else if (range) - { - if (!treeDataGrid.QueryCancelSelection()) - SelectFromAnchorTo(columnIndex, rowIndex); - } - else if (_selectedColumns.SelectedIndex != columnIndex || - _selectedRows.SelectedIndex != modelIndex || - Count > 1) - { - if (!treeDataGrid.QueryCancelSelection()) - Select(columnIndex, rowIndex, modelIndex); - } + private bool IsSelected(int columnIndex, int rowIndex) + { + var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); + return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(modelIndex); } private void Select(int columnIndex, int rowIndex) @@ -255,11 +234,39 @@ private void SelectFromAnchorTo(int columnIndex, int rowIndex) EndBatchUpdate(); } - - private bool IsSelected(int columnIndex, int rowIndex) + + private void UpdateSelection( + TreeDataGrid treeDataGrid, + int columnIndex, + int rowIndex, + bool rangeModifier = false, + bool rightButton = false) { var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); - return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(modelIndex); + + if (modelIndex == default) + return; + + var multi = !SingleSelect; + var range = multi && rangeModifier; + + if (rightButton) + { + if (IsSelected(columnIndex, modelIndex) == false && !treeDataGrid.QueryCancelSelection()) + Select(columnIndex, rowIndex, modelIndex); + } + else if (range) + { + if (!treeDataGrid.QueryCancelSelection()) + SelectFromAnchorTo(columnIndex, rowIndex); + } + else if (_selectedColumns.SelectedIndex != columnIndex || + _selectedRows.SelectedIndex != modelIndex || + Count > 1) + { + if (!treeDataGrid.QueryCancelSelection()) + Select(columnIndex, rowIndex, modelIndex); + } } private void OnSelectedColumnsSelectionChanged(object? sender, SelectionModelSelectionChangedEventArgs e) From e732d9b6d0d436fae3040f5ae188b35128bf57f4 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 12 May 2023 03:06:30 -0400 Subject: [PATCH 15/23] Make Select/SelectFromAnchorTo protected --- .../Selection/TreeDataGridCellSelectionModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 489d5b09..9fd1d40a 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -197,7 +197,7 @@ private bool IsSelected(int columnIndex, int rowIndex) return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(modelIndex); } - private void Select(int columnIndex, int rowIndex) + protected void Select(int columnIndex, int rowIndex) { var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); Select(columnIndex, rowIndex, modelIndex); @@ -212,7 +212,7 @@ private void Select(int columnIndex, int rowIndex, IndexPath modelndex) EndBatchUpdate(); } - private void SelectFromAnchorTo(int columnIndex, int rowIndex) + protected void SelectFromAnchorTo(int columnIndex, int rowIndex) { var anchorColumnIndex = _selectedColumns.AnchorIndex; var anchorModelIndex = _selectedRows.AnchorIndex; From 25c20cbf7983f3de9f46faffe2b329d6af8d291e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 12:52:33 +0200 Subject: [PATCH 16/23] Revert "Make Select/SelectFromAnchorTo protected" This reverts commit e732d9b6d0d436fae3040f5ae188b35128bf57f4. --- .../Selection/TreeDataGridCellSelectionModel.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 9fd1d40a..489d5b09 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -197,7 +197,7 @@ private bool IsSelected(int columnIndex, int rowIndex) return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(modelIndex); } - protected void Select(int columnIndex, int rowIndex) + private void Select(int columnIndex, int rowIndex) { var modelIndex = _source.Rows.RowIndexToModelIndex(rowIndex); Select(columnIndex, rowIndex, modelIndex); @@ -212,7 +212,7 @@ private void Select(int columnIndex, int rowIndex, IndexPath modelndex) EndBatchUpdate(); } - protected void SelectFromAnchorTo(int columnIndex, int rowIndex) + private void SelectFromAnchorTo(int columnIndex, int rowIndex) { var anchorColumnIndex = _selectedColumns.AnchorIndex; var anchorModelIndex = _selectedRows.AnchorIndex; From 0cbee656b91af901bb0e37dde6f3a3f3e8ca2cc7 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 13:13:32 +0200 Subject: [PATCH 17/23] Add API to set selected cell range. --- .../ITreeDataGridCellSelectionModel.cs | 18 ++++++-- .../TreeDataGridCellSelectionModel.cs | 45 +++++++++++++++---- 2 files changed, 51 insertions(+), 12 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs index a3f52c63..6a16062a 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/ITreeDataGridCellSelectionModel.cs @@ -43,8 +43,20 @@ public interface ITreeDataGridCellSelectionModel : ITreeDataGridCellSelection /// /// Checks whether the specified cell is selected. /// - /// The column index of the cell. - /// The model index of the cell. - public bool IsSelected(int columnIndex, IndexPath rowIndex); + /// The index of the cell. + bool IsSelected(CellIndex index); + + /// + /// Sets the current selection to the specified range of cells. + /// + /// The index of the cell from which the selection should start. + /// The number of columns in the selection. + /// The number of rows in the selection. + /// + /// This method clears the current selection and selects the specified range of cells. + /// Note that if the is currently sorted then the + /// resulting selection may not be contiguous in the data source. + /// + void SetSelectedRange(CellIndex start, int columnCount, int rowCount); } } diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 489d5b09..4430a018 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -73,11 +73,27 @@ event EventHandler? ITreeDataGridCell remove => _untypedSelectionChanged -= value; } + public bool IsSelected(CellIndex index) => IsSelected(index.ColumnIndex, index.RowIndex); + + /// + /// Checks whether the specified cell is selected. + /// + /// The column index of the cell. + /// The row index of the cell. public bool IsSelected(int columnIndex, IndexPath rowIndex) { return _selectedColumns.IsSelected(columnIndex) && _selectedRows.IsSelected(rowIndex); } + public void SetSelectedRange(CellIndex start, int columnCount, int rowCount) + { + SetSelectedRange( + start.ColumnIndex, + _source.Rows.ModelIndexToRowIndex(start.RowIndex), + columnCount, + rowCount); + } + bool ITreeDataGridSelectionInteraction.IsCellSelected(int columnIndex, int rowIndex) { return IsSelected(columnIndex, rowIndex); @@ -215,26 +231,37 @@ private void Select(int columnIndex, int rowIndex, IndexPath modelndex) private void SelectFromAnchorTo(int columnIndex, int rowIndex) { var anchorColumnIndex = _selectedColumns.AnchorIndex; - var anchorModelIndex = _selectedRows.AnchorIndex; - var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(anchorModelIndex); + var anchorRowIndex = _source.Rows.ModelIndexToRowIndex(_selectedRows.AnchorIndex); + + SetSelectedRange( + anchorColumnIndex, + anchorRowIndex, + (columnIndex - anchorColumnIndex) + 1, + (rowIndex - anchorRowIndex) + 1); + } + + private void SetSelectedRange(int columnIndex, int rowIndex, int columnCount, int rowCount) + { + var endColumnIndex = columnIndex + columnCount - 1; + var endRowIndex = rowIndex + rowCount - 1; BeginBatchUpdate(); - _selectedColumns.SelectedIndex = anchorColumnIndex; - _selectedColumns.SelectRange(anchorColumnIndex, columnIndex); - _selectedRows.SelectedIndex = anchorModelIndex; + _selectedColumns.SelectedIndex = columnIndex; + _selectedColumns.SelectRange(columnIndex, endColumnIndex); + _selectedRows.SelectedIndex = rowIndex; - for (var i = Math.Min(anchorRowIndex, rowIndex); i <= Math.Max(anchorRowIndex, rowIndex); ++i) + for (var i = Math.Min(rowIndex, endRowIndex); i <= Math.Max(rowIndex, endRowIndex); ++i) { _selectedRows.Select(_source.Rows.RowIndexToModelIndex(i)); } - _selectedRows.AnchorIndex = anchorModelIndex; - _rangeAnchor = (columnIndex, rowIndex); + _selectedRows.AnchorIndex = rowIndex; + _rangeAnchor = (endColumnIndex, endRowIndex); EndBatchUpdate(); } - + private void UpdateSelection( TreeDataGrid treeDataGrid, int columnIndex, From 666ba5df707fc576be42b0a434807b8d1e9d55cb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 14:26:44 +0200 Subject: [PATCH 18/23] Scroll cells into view when navigating with keyboard. --- .../Primitives/TreeDataGridPresenterBase.cs | 20 ++++++++++++------- .../TreeDataGridCellSelectionModel.cs | 5 +++++ 2 files changed, 18 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs index 9124b811..726d7c55 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Primitives/TreeDataGridPresenterBase.cs @@ -82,14 +82,17 @@ public IReadOnlyList? Items protected abstract Orientation Orientation { get; } protected Rect Viewport { get; private set; } = s_invalidViewport; - public void BringIntoView(int index) + public void BringIntoView(int index, Rect? rect = null) { if (_items is null || index < 0 || index >= _items.Count) return; if (GetRealizedElement(index) is Control element) { - element.BringIntoView(); + if (rect.HasValue) + element.BringIntoView(rect.Value); + else + element.BringIntoView(); } else if (this.GetVisualRoot() is ILayoutRoot root) { @@ -101,16 +104,16 @@ public void BringIntoView(int index) // Get the expected position of the elment and put it in place. var anchorU = GetOrEstimateElementPosition(index); - var rect = Orientation == Orientation.Horizontal ? + var elementRect = Orientation == Orientation.Horizontal ? new Rect(anchorU, 0, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height) : new Rect(0, anchorU, _anchorElement.DesiredSize.Width, _anchorElement.DesiredSize.Height); - _anchorElement.Arrange(rect); + _anchorElement.Arrange(elementRect); // If the item being brought into view was added since the last layout pass then // our bounds won't be updated, so any containing scroll viewers will not have an // updated extent. Do a layout pass to ensure that the containing scroll viewers // will be able to scroll the new item into view. - if (!Bounds.Contains(rect) && !Viewport.Contains(rect)) + if (!Bounds.Contains(elementRect) && !Viewport.Contains(elementRect)) { _isWaitingForViewportUpdate = true; root.LayoutManager.ExecuteLayoutPass(); @@ -118,9 +121,12 @@ public void BringIntoView(int index) } // Try to bring the item into view and do a layout pass. - _anchorElement.BringIntoView(); + if (rect.HasValue) + _anchorElement.BringIntoView(rect.Value); + else + _anchorElement.BringIntoView(); - _isWaitingForViewportUpdate = !Viewport.Contains(rect); + _isWaitingForViewportUpdate = !Viewport.Contains(elementRect); root.LayoutManager.ExecuteLayoutPass(); _isWaitingForViewportUpdate = false; diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index 4430a018..daeb4508 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -127,6 +127,11 @@ sender.Rows is null || Select(columnIndex, rowIndex); else SelectFromAnchorTo(columnIndex, rowIndex); + + sender.ColumnHeadersPresenter?.BringIntoView(columnIndex); + sender.RowsPresenter?.BringIntoView( + rowIndex, + sender.ColumnHeadersPresenter?.TryGetElement(columnIndex)?.Bounds); } void ITreeDataGridSelectionInteraction.OnPointerPressed(TreeDataGrid sender, PointerPressedEventArgs e) From d5283683d45c728bc2bc34945e0809ed4ceed9fc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 15:01:43 +0200 Subject: [PATCH 19/23] Allow switching selection. And add option to switch to cell selection to sample. --- samples/TreeDataGridDemo/MainWindow.axaml | 8 ++- .../ViewModels/CountriesPageViewModel.cs | 25 ++++++- .../ViewModels/FilesPageViewModel.cs | 18 +++++ .../FlatTreeDataGridSource.cs | 20 ++++-- .../HierarchicalTreeDataGridSource.cs | 16 +++-- .../ITreeDataGridSource.cs | 2 +- .../TreeDataGrid.cs | 68 +++++++++++-------- 7 files changed, 114 insertions(+), 43 deletions(-) diff --git a/samples/TreeDataGridDemo/MainWindow.axaml b/samples/TreeDataGridDemo/MainWindow.axaml index 2dea9514..20d02aaf 100644 --- a/samples/TreeDataGridDemo/MainWindow.axaml +++ b/samples/TreeDataGridDemo/MainWindow.axaml @@ -11,7 +11,8 @@ - + + Cell Selection Sealand @@ -48,6 +49,11 @@ + + Cell Selection + _data; + private bool _cellSelection; public CountriesPageViewModel() { @@ -20,8 +22,8 @@ public CountriesPageViewModel() Columns = { new TextColumn("Country", x => x.Name, (r, v) => r.Name = v, new GridLength(6, GridUnitType.Star), new() - { - IsTextSearchEnabled = true + { + IsTextSearchEnabled = true }), new TextColumn("Region", x => x.Region, new GridLength(4, GridUnitType.Star)), new TextColumn("Population", x => x.Population, new GridLength(3, GridUnitType.Star)), @@ -35,6 +37,23 @@ public CountriesPageViewModel() Source.RowSelection!.SingleSelect = false; } + public bool CellSelection + { + get => _cellSelection; + set + { + if (_cellSelection != value) + { + _cellSelection = value; + if (_cellSelection) + Source.Selection = new TreeDataGridCellSelectionModel(Source) { SingleSelect = false }; + else + Source.Selection = new TreeDataGridRowSelectionModel(Source) { SingleSelect = false }; + this.RaisePropertyChanged(); + } + } + } + public FlatTreeDataGridSource Source { get; } public void AddCountry(Country country) => _data.Add(country); diff --git a/samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs b/samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs index 174b818a..2faf80a6 100644 --- a/samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs +++ b/samples/TreeDataGridDemo/ViewModels/FilesPageViewModel.cs @@ -20,6 +20,7 @@ namespace TreeDataGridDemo.ViewModels public class FilesPageViewModel : ReactiveObject { private static IconConverter? s_iconConverter; + private bool _cellSelection; private FileTreeNodeModel? _root; private string _selectedDrive; private string? _selectedPath; @@ -94,6 +95,23 @@ public FilesPageViewModel() }); } + public bool CellSelection + { + get => _cellSelection; + set + { + if (_cellSelection != value) + { + _cellSelection = value; + if (_cellSelection) + Source.Selection = new TreeDataGridCellSelectionModel(Source) { SingleSelect = false }; + else + Source.Selection = new TreeDataGridRowSelectionModel(Source) { SingleSelect = false }; + this.RaisePropertyChanged(); + } + } + } + public IList Drives { get; } public string SelectedDrive diff --git a/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs b/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs index 13cb8f09..a1e82667 100644 --- a/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs +++ b/src/Avalonia.Controls.TreeDataGrid/FlatTreeDataGridSource.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Linq; +using Avalonia.Controls.Models; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Selection; using Avalonia.Input; @@ -12,8 +13,10 @@ namespace Avalonia.Controls /// A data source for a which displays a flat grid. /// /// The model type. - public class FlatTreeDataGridSource : ITreeDataGridSource, IDisposable - where TModel: class + public class FlatTreeDataGridSource : NotifyingBase, + ITreeDataGridSource, + IDisposable + where TModel: class { private IEnumerable _items; private TreeDataGridItemsSourceView _itemsView; @@ -45,6 +48,7 @@ public IEnumerable Items _rows?.SetItems(_itemsView); if (_selection is object) _selection.Source = value; + RaisePropertyChanged(); } } } @@ -59,10 +63,14 @@ public ITreeDataGridSelection? Selection } set { - if (_selection is object) - throw new InvalidOperationException("Selection is already initialized."); - _selection = value; - _isSelectionSet = true; + if (_selection != value) + { + if (value?.Source != _items) + throw new InvalidOperationException("Selection source must be set to Items."); + _selection = value; + _isSelectionSet = true; + RaisePropertyChanged(); + } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs b/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs index f183703c..be06e991 100644 --- a/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs +++ b/src/Avalonia.Controls.TreeDataGrid/HierarchicalTreeDataGridSource.cs @@ -4,6 +4,7 @@ using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; +using Avalonia.Controls.Models; using Avalonia.Controls.Models.TreeDataGrid; using Avalonia.Controls.Selection; using Avalonia.Input; @@ -15,7 +16,8 @@ namespace Avalonia.Controls /// row may have multiple columns. /// /// The model type. - public class HierarchicalTreeDataGridSource : ITreeDataGridSource, + public class HierarchicalTreeDataGridSource : NotifyingBase, + ITreeDataGridSource, IDisposable, IExpanderRowController where TModel: class @@ -70,10 +72,14 @@ public ITreeDataGridSelection? Selection } set { - if (_selection is object) - throw new InvalidOperationException("Selection is already initialized."); - _selection = value; - _isSelectionSet = true; + if (_selection != value) + { + if (value?.Source != _items) + throw new InvalidOperationException("Selection source must be set to Items."); + _selection = value; + _isSelectionSet = true; + RaisePropertyChanged(); + } } } diff --git a/src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs b/src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs index f2fdb179..535b4c04 100644 --- a/src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs +++ b/src/Avalonia.Controls.TreeDataGrid/ITreeDataGridSource.cs @@ -10,7 +10,7 @@ namespace Avalonia.Controls /// /// Represents a data source for a control. /// - public interface ITreeDataGridSource + public interface ITreeDataGridSource : INotifyPropertyChanged { /// /// Gets the columns to be displayed. diff --git a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs index 75be16e5..069c69ac 100644 --- a/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs +++ b/src/Avalonia.Controls.TreeDataGrid/TreeDataGrid.cs @@ -43,13 +43,6 @@ public class TreeDataGrid : TemplatedControl o => o.Rows, (o, v) => o.Rows = v); - [Browsable(false)] - public static readonly DirectProperty SelectionInteractionProperty = - AvaloniaProperty.RegisterDirect( - nameof(SelectionInteraction), - o => o.SelectionInteraction, - (o, v) => o.SelectionInteraction = v); - public static readonly DirectProperty ScrollProperty = AvaloniaProperty.RegisterDirect( nameof(Scroll), @@ -149,13 +142,6 @@ public IRows? Rows private set => SetAndRaise(RowsProperty, ref _rows, value); } - [Browsable(false)] - public ITreeDataGridSelectionInteraction? SelectionInteraction - { - get => _selection; - private set => SetAndRaise(SelectionInteractionProperty, ref _selection, value); - } - public TreeDataGridColumnHeadersPresenter? ColumnHeadersPresenter { get; private set; } public TreeDataGridRowsPresenter? RowsPresenter { get; private set; } @@ -171,7 +157,8 @@ public bool ShowColumnHeaders set => SetValue(ShowColumnHeadersProperty, value); } - public ITreeSelectionModel? RowSelection => Source?.Selection as ITreeSelectionModel; + public ITreeDataGridCellSelectionModel? ColumnSelection => Source?.Selection as ITreeDataGridCellSelectionModel; + public ITreeDataGridRowSelectionModel? RowSelection => Source?.Selection as ITreeDataGridRowSelectionModel; public ITreeDataGridSource? Source { @@ -180,10 +167,11 @@ public ITreeDataGridSource? Source { if (_source != value) { - if (SelectionInteraction != null) - SelectionInteraction.SelectionChanged -= OnSelectionInteractionChanged; if (_source != null) + { + _source.PropertyChanged -= OnSourcePropertyChanged; _source.Sorted -= OnSourceSorted; + } var oldSource = _source; _source = value; @@ -192,9 +180,10 @@ public ITreeDataGridSource? Source SelectionInteraction = _source?.Selection as ITreeDataGridSelectionInteraction; if (_source != null) + { + _source.PropertyChanged += OnSourcePropertyChanged; _source.Sorted += OnSourceSorted; - if (SelectionInteraction != null) - SelectionInteraction.SelectionChanged += OnSelectionInteractionChanged; + } RaisePropertyChanged( SourceProperty, @@ -204,9 +193,25 @@ public ITreeDataGridSource? Source } } + internal ITreeDataGridSelectionInteraction? SelectionInteraction + { + get => _selection; + set + { + if (_selection != value) + { + if (_selection != null) + _selection.SelectionChanged -= OnSelectionInteractionChanged; + _selection = value; + if (_selection != null) + _selection.SelectionChanged += OnSelectionInteractionChanged; + } + } + } + public event EventHandler? CellClearing; public event EventHandler? CellPrepared; - + public event EventHandler? RowDragStarted { add => AddHandler(RowDragStartedEvent, value!); @@ -245,7 +250,7 @@ public event EventHandler? RowDrop public bool TryGetCell(Control? element, [MaybeNullWhen(false)] out TreeDataGridCell result) { - if (element.FindAncestorOfType(true) is { } cell && + if (element.FindAncestorOfType(true) is { } cell && cell.ColumnIndex >= 0 && cell.RowIndex >= 0) { @@ -330,7 +335,7 @@ protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e } protected void OnPreviewKeyDown(object? o, KeyEventArgs e) - { + { _selection?.OnPreviewKeyDown(this, e); } @@ -407,8 +412,8 @@ internal void RaiseRowDragStarted(PointerEventArgs trigger) if (_source is null || RowSelection is null) return; - var allowedEffects = AutoDragDropRows && !_source.IsSorted ? - DragDropEffects.Move : + var allowedEffects = AutoDragDropRows && !_source.IsSorted ? + DragDropEffects.Move : DragDropEffects.None; var route = BuildEventRoute(RowDragStartedEvent); @@ -561,7 +566,7 @@ private void AutoScroll(bool direction) [MemberNotNullWhen(true, nameof(_source))] private bool CalculateAutoDragDrop( TreeDataGridRow targetRow, - DragEventArgs e, + DragEventArgs e, [NotNullWhen(true)] out DragInfo? data, out TreeDataGridRowDropPosition position) { @@ -639,7 +644,7 @@ private void OnDragLeave(RoutedEventArgs e) private void OnDrop(DragEventArgs e) { StopDrag(); - + if (!TryGetRow(e.Source as Control, out var row)) return; @@ -658,7 +663,7 @@ private void OnDrop(DragEventArgs e) position = ev.Position; } - if (autoDrop && + if (autoDrop && _source is not null && position != TreeDataGridRowDropPosition.None) { @@ -695,6 +700,15 @@ private void OnSelectionInteractionChanged(object? sender, EventArgs e) RowsPresenter?.UpdateSelection(SelectionInteraction); } + private void OnSourcePropertyChanged(object? sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == nameof(ITreeDataGridSource.Selection)) + { + SelectionInteraction = Source?.Selection as ITreeDataGridSelectionInteraction; + RowsPresenter?.UpdateSelection(SelectionInteraction); + } + } + private void OnSourceSorted() { RowsPresenter?.RecycleAllElements(); From ffbba99f51082bc0ba2a118bd74a4d2081aae5fb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 15:16:36 +0200 Subject: [PATCH 20/23] Fix keyboard selection with tree data. --- .../Selection/TreeDataGridCellSelectionModel.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs index daeb4508..19d709ef 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/TreeDataGridCellSelectionModel.cs @@ -90,6 +90,7 @@ public void SetSelectedRange(CellIndex start, int columnCount, int rowCount) SetSelectedRange( start.ColumnIndex, _source.Rows.ModelIndexToRowIndex(start.RowIndex), + start.RowIndex, columnCount, rowCount); } @@ -241,11 +242,17 @@ private void SelectFromAnchorTo(int columnIndex, int rowIndex) SetSelectedRange( anchorColumnIndex, anchorRowIndex, + _selectedRows.AnchorIndex, (columnIndex - anchorColumnIndex) + 1, (rowIndex - anchorRowIndex) + 1); } - private void SetSelectedRange(int columnIndex, int rowIndex, int columnCount, int rowCount) + private void SetSelectedRange( + int columnIndex, + int rowIndex, + IndexPath modelIndex, + int columnCount, + int rowCount) { var endColumnIndex = columnIndex + columnCount - 1; var endRowIndex = rowIndex + rowCount - 1; @@ -261,7 +268,7 @@ private void SetSelectedRange(int columnIndex, int rowIndex, int columnCount, in _selectedRows.Select(_source.Rows.RowIndexToModelIndex(i)); } - _selectedRows.AnchorIndex = rowIndex; + _selectedRows.AnchorIndex = modelIndex; _rangeAnchor = (endColumnIndex, endRowIndex); EndBatchUpdate(); From 0f6929fa6686d5f1000e0e5128b59faf0b1f6a58 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 15:25:08 +0200 Subject: [PATCH 21/23] Added CheckBoxColumn docs. Unrelated to this PR but I just noticed it was missing. --- docs/column-types.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/column-types.md b/docs/column-types.md index 34562cc4..d89a313b 100644 --- a/docs/column-types.md +++ b/docs/column-types.md @@ -20,6 +20,24 @@ This is the signature of the `TextColumn` constructor. There are two most import **Note**: The sample above is taken from [this article](https://github.com/AvaloniaUI/Avalonia.Controls.TreeDataGrid/blob/master/docs/get-started-flat.md). If you feel like you need more examples feel free to check it, there is a sample that shows how to use TextColumns and how to run a whole `TreeDataGrid` using them. +## CheckBoxColumn + +As its name suggests, `CheckBoxColumn` displays a `CheckBox` in its cells. For a readonly checkbox: + +```csharp +new CheckColumn("Firstborn", x => x.IsFirstborn) +``` + +The first parameter defines the column header. The second parameter is an expression which gets the value of the property from the model. + +For a read/write checkbox: + +```csharp +new CheckColumn("Firstborn", x => x.IsFirstborn, (o, v) => o.IsFirstborn = v) +``` + +This overload adds a second paramter which is the expression used to set the property in the model. + ## HierarchicalExpanderColumn `HierarchicalExpanderColumn` can be used only with `HierarchicalTreeDataGrid` (a.k.a TreeView) thats what Hierarchical stands for in its name, also it can be used only with `HierarchicalTreeDataGridSource`. This type of columns can be useful when you want cells to show an expander to reveal nested data. From 1f033b496f84dd7f906b2a3adfec67df72fce22b Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 15:53:11 +0200 Subject: [PATCH 22/23] Added basic selection docs. --- docs/selection.md | 92 +++++++++++++++++++++++++++++++++++++++++++++++ readme.md | 1 + 2 files changed, 93 insertions(+) create mode 100644 docs/selection.md diff --git a/docs/selection.md b/docs/selection.md new file mode 100644 index 00000000..e8cf3adf --- /dev/null +++ b/docs/selection.md @@ -0,0 +1,92 @@ +# Selection + +Two selection modes are supported: + +- Row selection allows the user to select whole rows +- Cell selection allows the user to select individial cells + +Both selection types support either single or multiple selection. The default selection type is single row selection. + +## Index Paths + +Because `TreeDataGrid` supports hierarchical data, using a simple index to identify a row in the data source isn't enough. Instead indexes are represented using the `IndexPath` struct. + +An `IndexPath` is essentially an array of indexes, each element of which specifies the index at a succesively deeper level in the hierarchy of the data. + +Consider the following data source: + +``` +|- A +| |- B +| |- C +| |- D +|- E +``` + +- `A` has an index path of `0` as it's the first item at the root of the hierarchy +- `B` has an index path of `0,0` as it's the first child of the first item +- `C` has an index path of `0,1` as it's the second child of the first item +- `D` has an index path of `0,1,0` as it's the first child of `C` +- `E` has an index path of `1` as it's the second item in the root + +`IndexPath` is an immutable struct which is constructed with an array of integers, e.g.: `new ItemPath(0, 1, 0)`. There is also an implicit conversion from `int` for when working with a flat data source. + +## Row Selection + +Row selection is the default and is exposed via the `RowSelection` property on the `FlatTreeDataGridSource` and `HierarchicalTreeDataGridSource` classes when enabled. Row selection is stored in an instance of the `TreeDataGridRowSelectionModel` class. + +By default is single selection. To enable multiple selection set the the `SingleSelect` property to `false`, e.g.: + +```csharp +Source = new FlatTreeDataGridSource(_people) +{ + Columns = + { + new TextColumn("First Name", x => x.FirstName), + new TextColumn("Last Name", x => x.LastName), + new TextColumn("Age", x => x.Age), + }, +}; + +Source.RowSelection!.SingleSelect = false; +``` + +The properties on `ITreeDataGridRowSelectionModel` can be used to manipulate the selection, e.g.: + +```csharp +Source.RowSelection!.SelectedIndex = 1; +``` + +Or + +```csharp +Source.RowSelection!.SelectedIndex = new IndexPath(0, 1); +``` + +## Cell Selection + +To enable cell selection for a `TreeDataGridSource`, assign an instance of `TreeDataGridCellSelectionModel` to the source's `Selection` property: + +```csharp +Source = new FlatTreeDataGridSource(_people) +{ + Columns = + { + new TextColumn("First Name", x => x.FirstName), + new TextColumn("Last Name", x => x.LastName), + new TextColumn("Age", x => x.Age), + }, +}; + +Source.Selection = new TreeDataGridCellSelectionModel(Source); +``` + +Or for multiple cell selection: + +```csharp +Source.Selection = new TreeDataGridCellSelectionModel(Source) { SingleSelect = false }; +``` + +Cell selection is is exposed via the `CellSelection` property on the `FlatTreeDataGridSource` and `HierarchicalTreeDataGridSource` classes when enabled. + +The `CellIndex` struct indentifies an individual cell with by combination of an integer column index and an `IndexPath` row index. \ No newline at end of file diff --git a/readme.md b/readme.md index 049b8f21..ea2f9445 100644 --- a/readme.md +++ b/readme.md @@ -32,3 +32,4 @@ We accept issues and pull requests but we answer and review only pull requests a - [Creating a flat `TreeDataGrid`](docs/get-started-flat.md) - [Creating a hierarchical `TreeDataGrid`](docs/get-started-hierarchical.md) - [Supported column types](docs/column-types.md) +- [Selection](docs/selection.md) From 9f9ed6207edc1cb8f5f156891dc9a6a5fce0ae8c Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 12 May 2023 16:35:20 +0200 Subject: [PATCH 23/23] Fix updating count in IndexRange. With failing-then-passing unit test. --- .../Selection/IndexRanges.cs | 3 +-- .../TreeSelectionModelBaseTests_Single.cs | 14 ++++++++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Controls.TreeDataGrid/Selection/IndexRanges.cs b/src/Avalonia.Controls.TreeDataGrid/Selection/IndexRanges.cs index 62cf76dd..507151cd 100644 --- a/src/Avalonia.Controls.TreeDataGrid/Selection/IndexRanges.cs +++ b/src/Avalonia.Controls.TreeDataGrid/Selection/IndexRanges.cs @@ -44,8 +44,7 @@ public void Add(in IndexPath index) _ranges.Add(parent, ranges); } - IndexRange.Add(ranges, new IndexRange(index[^1])); - ++Count; + Count += IndexRange.Add(ranges, new IndexRange(index[^1])); } public void Add(in IndexPath parent, in IndexRange range) diff --git a/tests/Avalonia.Controls.TreeDataGrid.Tests/Selection/TreeSelectionModelBaseTests_Single.cs b/tests/Avalonia.Controls.TreeDataGrid.Tests/Selection/TreeSelectionModelBaseTests_Single.cs index e432825c..6b4d27f6 100644 --- a/tests/Avalonia.Controls.TreeDataGrid.Tests/Selection/TreeSelectionModelBaseTests_Single.cs +++ b/tests/Avalonia.Controls.TreeDataGrid.Tests/Selection/TreeSelectionModelBaseTests_Single.cs @@ -415,6 +415,20 @@ public void Selecting_Already_Selected_Item_Doesnt_Raise_SelectionChanged() Assert.Equal(0, raised); } + + [Fact] + public void Selecting_Item_Twice_Results_In_Correct_Count() + { + var target = CreateTarget(); + + using (target.BatchUpdate()) + { + target.SelectedIndex = new IndexPath(1); + target.Select(new IndexPath(1)); + } + + Assert.Equal(1, target.Count); + } } public class Deselect