diff --git a/src/WinUI.TableView/KeyBoardHelper.cs b/src/WinUI.TableView/KeyBoardHelper.cs new file mode 100644 index 0000000..e2fa839 --- /dev/null +++ b/src/WinUI.TableView/KeyBoardHelper.cs @@ -0,0 +1,19 @@ +using Microsoft.UI.Input; +using Windows.System; +using Windows.UI.Core; + +namespace WinUI.TableView; +internal static class KeyBoardHelper +{ + public static bool IsShiftKeyDown() + { + var shiftKey = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift); + return shiftKey is CoreVirtualKeyStates.Down or (CoreVirtualKeyStates.Down | CoreVirtualKeyStates.Locked); + } + + public static bool IsCtrlKeyDown() + { + var ctrlKey = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Control); + return ctrlKey is CoreVirtualKeyStates.Down or (CoreVirtualKeyStates.Down | CoreVirtualKeyStates.Locked); + } +} diff --git a/src/WinUI.TableView/TableView.Properties.cs b/src/WinUI.TableView/TableView.Properties.cs new file mode 100644 index 0000000..f7c1147 --- /dev/null +++ b/src/WinUI.TableView/TableView.Properties.cs @@ -0,0 +1,246 @@ +using CommunityToolkit.WinUI.Collections; +using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Data; +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace WinUI.TableView; +public partial class TableView +{ + public static readonly new DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(IList), typeof(TableView), new PropertyMetadata(null, OnItemsSourceChanged)); + public static readonly new DependencyProperty SelectionModeProperty = DependencyProperty.Register(nameof(SelectionMode), typeof(ListViewSelectionMode), typeof(TableView), new PropertyMetadata(ListViewSelectionMode.Extended, OnSelectionModeChanged)); + public static readonly DependencyProperty HeaderRowHeightProperty = DependencyProperty.Register(nameof(HeaderRowHeight), typeof(double), typeof(TableView), new PropertyMetadata(32d, OnHeaderRowHeightChanged)); + public static readonly DependencyProperty RowHeightProperty = DependencyProperty.Register(nameof(RowHeight), typeof(double), typeof(TableView), new PropertyMetadata(40d)); + public static readonly DependencyProperty RowMaxHeightProperty = DependencyProperty.Register(nameof(RowMaxHeight), typeof(double), typeof(TableView), new PropertyMetadata(double.PositiveInfinity)); + public static readonly DependencyProperty ShowExportOptionsProperty = DependencyProperty.Register(nameof(ShowExportOptions), typeof(bool), typeof(TableView), new PropertyMetadata(false)); + public static readonly DependencyProperty AutoGenerateColumnsProperty = DependencyProperty.Register(nameof(AutoGenerateColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnAutoGenerateColumnsChanged)); + public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(TableView), new PropertyMetadata(false)); + public static readonly DependencyProperty ShowOptionsButtonProperty = DependencyProperty.Register(nameof(ShowOptionsButton), typeof(bool), typeof(TableView), new PropertyMetadata(true)); + public static readonly DependencyProperty CanResizeColumnsProperty = DependencyProperty.Register(nameof(CanResizeColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true)); + public static readonly DependencyProperty CanSortColumnsProperty = DependencyProperty.Register(nameof(CanSortColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnCanSortColumnsChanged)); + public static readonly DependencyProperty CanFilterColumnsProperty = DependencyProperty.Register(nameof(CanFilterColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnCanFilterColumnsChanged)); + public static readonly DependencyProperty MinColumnWidthProperty = DependencyProperty.Register(nameof(MinColumnWidth), typeof(double), typeof(TableView), new PropertyMetadata(50d, OnMinColumnWidthChanged)); + public static readonly DependencyProperty MaxColumnWidthProperty = DependencyProperty.Register(nameof(MaxColumnWidth), typeof(double), typeof(TableView), new PropertyMetadata(double.PositiveInfinity, OnMaxColumnWidthChanged)); + public static readonly DependencyProperty SelectionUnitProperty = DependencyProperty.Register(nameof(SelectionUnit), typeof(TableViewSelectionUnit), typeof(TableView), new PropertyMetadata(TableViewSelectionUnit.CellOrRow, OnSelectionUnitChanged)); + + public IAdvancedCollectionView CollectionView { get; private set; } = new AdvancedCollectionView(); + internal IDictionary> ActiveFilters { get; } = new Dictionary>(); + internal TableViewCellSlot? CurrentCellSlot { get; set; } + internal TableViewCellSlot? SelectionStartCellSlot { get; set; } + internal HashSet SelectedCells { get; set; } = new HashSet(); + internal HashSet> SelectedCellRanges { get; } = new HashSet>(); + internal bool IsEditing { get; set; } + public TableViewColumnsCollection Columns { get; } = new(); + + public double HeaderRowHeight + { + get => (double)GetValue(HeaderRowHeightProperty); + set => SetValue(HeaderRowHeightProperty, value); + } + + public double RowHeight + { + get => (double)GetValue(RowHeightProperty); + set => SetValue(RowHeightProperty, value); + } + + public double RowMaxHeight + { + get => (double)GetValue(RowMaxHeightProperty); + set => SetValue(RowMaxHeightProperty, value); + } + + public new IList ItemsSource + { + get => (IList)GetValue(ItemsSourceProperty); + set => SetValue(ItemsSourceProperty, value); + } + + public new ListViewSelectionMode SelectionMode + { + get => (ListViewSelectionMode)GetValue(SelectionModeProperty); + set => SetValue(SelectionModeProperty, value); + } + + public bool ShowExportOptions + { + get => (bool)GetValue(ShowExportOptionsProperty); + set => SetValue(ShowExportOptionsProperty, value); + } + + public bool AutoGenerateColumns + { + get => (bool)GetValue(AutoGenerateColumnsProperty); + set => SetValue(AutoGenerateColumnsProperty, value); + } + + public bool IsReadOnly + { + get => (bool)GetValue(IsReadOnlyProperty); + set => SetValue(IsReadOnlyProperty, value); + } + + public bool ShowOptionsButton + { + get => (bool)GetValue(ShowOptionsButtonProperty); + set => SetValue(ShowOptionsButtonProperty, value); + } + + public bool CanResizeColumns + { + get => (bool)GetValue(CanResizeColumnsProperty); + set => SetValue(CanResizeColumnsProperty, value); + } + + public bool CanSortColumns + { + get => (bool)GetValue(CanSortColumnsProperty); + set => SetValue(CanSortColumnsProperty, value); + } + + public bool CanFilterColumns + { + get => (bool)GetValue(CanFilterColumnsProperty); + set => SetValue(CanFilterColumnsProperty, value); + } + + public double MinColumnWidth + { + get => (double)GetValue(MinColumnWidthProperty); + set => SetValue(MinColumnWidthProperty, value); + } + + public double MaxColumnWidth + { + get => (double)GetValue(MaxColumnWidthProperty); + set => SetValue(MaxColumnWidthProperty, value); + } + + public TableViewSelectionUnit SelectionUnit + { + get => (TableViewSelectionUnit)GetValue(SelectionUnitProperty); + set => SetValue(SelectionUnitProperty, value); + } + + private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView) + { + tableView.OnItemsSourceChanged(e); + tableView.SelectedCellRanges.Clear(); + tableView.OnCellSelectionChanged(); + } + } + + private static void OnSelectionModeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView) + { + if (tableView.SelectionMode is ListViewSelectionMode.Single or ListViewSelectionMode.None) + { + var currentCell = tableView.CurrentCellSlot.HasValue ? tableView.GetCellFromSlot(tableView.CurrentCellSlot.Value) : default; + currentCell?.ApplyCurrentCellState(); + tableView.SelectedCellRanges.Clear(); + + if (tableView.SelectionMode is ListViewSelectionMode.Single && tableView.CurrentCellSlot.HasValue) + { + tableView.SelectedCellRanges.Add(new() { tableView.CurrentCellSlot.Value }); + } + + tableView.OnCellSelectionChanged(); + } + + tableView.UpdateBaseSelectionMode(); + } + } + + private static void OnHeaderRowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + (d as TableView)?.UpdateVerticalScrollBarMargin(); + } + + private static void OnAutoGenerateColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView) + { + if (tableView.AutoGenerateColumns) + { + tableView.GenerateColumns(); + } + else + { + tableView.RemoveAutoGeneratedColumns(); + } + } + } + + private static void OnCanSortColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView && e.NewValue is false) + { + tableView.ClearSorting(); + } + } + + private static void OnCanFilterColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView && e.NewValue is false) + { + tableView.ClearFilters(); + } + } + + private static void OnMinColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView table && table._headerRow is not null) + { + table._headerRow.CalculateHeaderWidths(); + } + } + + private static void OnMaxColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView table && table._headerRow is not null) + { + table._headerRow.CalculateHeaderWidths(); + } + } + + private static void OnSelectionUnitChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) + { + if (d is TableView tableView) + { + if (tableView.SelectionUnit is TableViewSelectionUnit.Row) + { + tableView.SelectedCellRanges.Clear(); + tableView.OnCellSelectionChanged(); + } + + tableView.UpdateBaseSelectionMode(); + } + } + + private void OnBaseItemsSourceChanged(DependencyObject sender, DependencyProperty dp) + { + throw new InvalidOperationException("Setting this property directly is not allowed. Use TableView.ItemsSource instead."); + } + + private void OnBaseSelectionModeChanged(DependencyObject sender, DependencyProperty dp) + { + if (!_shouldThrowSelectionModeChangedException) + { + throw new InvalidOperationException("Setting this property directly is not allowed. Use TableView.SelectionMode instead."); + } + } +} + +public static class ItemIndexRangeExtensions +{ + public static bool IsInRange(this ItemIndexRange range, int index) + { + return index >= range.FirstIndex && index <= range.LastIndex; + } +} diff --git a/src/WinUI.TableView/TableView.cs b/src/WinUI.TableView/TableView.cs index 545a904..fad69a8 100644 --- a/src/WinUI.TableView/TableView.cs +++ b/src/WinUI.TableView/TableView.cs @@ -18,15 +18,19 @@ using System.Text; using System.Threading.Tasks; using Windows.ApplicationModel.DataTransfer; +using Windows.Foundation; using Windows.Storage; using Windows.Storage.Pickers; +using Windows.System; using WinRT.Interop; using WinUI.TableView.Extensions; namespace WinUI.TableView; -public class TableView : ListView +public partial class TableView : ListView { private TableViewHeaderRow? _headerRow; + private ScrollViewer _scrollViewer = null!; + private bool _shouldThrowSelectionModeChangedException; public TableView() { @@ -34,8 +38,122 @@ public TableView() CollectionView.Filter = Filter; base.ItemsSource = CollectionView; - + base.SelectionMode = ListViewSelectionMode.Extended; + RegisterPropertyChangedCallback(ItemsControl.ItemsSourceProperty, OnBaseItemsSourceChanged); + RegisterPropertyChangedCallback(ListViewBase.SelectionModeProperty, OnBaseSelectionModeChanged); Loaded += OnLoaded; + SelectionChanged += TableView_SelectionChanged; + } + + private void TableView_SelectionChanged(object sender, SelectionChangedEventArgs e) + { + if (!KeyBoardHelper.IsCtrlKeyDown()) + { + SelectedCellRanges.Clear(); + } + else + { + SelectedCellRanges.RemoveWhere(slots => + { + slots.RemoveWhere(slot => SelectedRanges.Any(range => range.IsInRange(slot.Row))); + return slots.Count == 0; + }); + } + + SetCurrentCell(null); + OnCellSelectionChanged(); + } + + protected override void PrepareContainerForItemOverride(DependencyObject element, object item) + { + base.PrepareContainerForItemOverride(element, item); + + if (element is TableViewRow row) + { + var index = IndexFromContainer(element); + row.SetValue(TableViewRow.IndexProperty, index); + row.ApplyCellsSelectionState(); + + if (CurrentCellSlot.HasValue) + { + row.ApplyCurrentCellState(CurrentCellSlot.Value); + } + } + } + + protected override DependencyObject GetContainerForItemOverride() + { + return new TableViewRow { TableView = this }; + } + + protected override void OnPreviewKeyDown(KeyRoutedEventArgs e) + { + base.OnPreviewKeyDown(e); + + var shiftKey = KeyBoardHelper.IsShiftKeyDown(); + var ctrlKey = KeyBoardHelper.IsCtrlKeyDown(); + var currentCell = CurrentCellSlot.HasValue ? GetCellFromSlot(CurrentCellSlot.Value) : default; + + if (e.Key is VirtualKey.F2 && currentCell is not null && !IsEditing) + { + currentCell.PrepareForEdit(); + IsEditing = true; + e.Handled = true; + } + else if (e.Key is VirtualKey.Escape && currentCell is not null && IsEditing) + { + currentCell?.SetElement(); + IsEditing = false; + e.Handled = true; + } + else if (e.Key is VirtualKey.Space && currentCell is not null && CurrentCellSlot.HasValue && !IsEditing) + { + if (!currentCell.IsSelected) + { + SelectCells(CurrentCellSlot.Value, shiftKey, ctrlKey); + } + else + { + DeselectCell(CurrentCellSlot.Value); + } + } + // Handle navigation keys + else if (e.Key is VirtualKey.Tab or VirtualKey.Enter) + { + var isEditing = IsEditing; + var newSlot = GetNextSlot(CurrentCellSlot, shiftKey, e.Key is VirtualKey.Enter); + + SelectCells(newSlot, false); + + if (isEditing && currentCell is not null) + { + currentCell.SetElement(); + currentCell = GetCellFromSlot(newSlot); + currentCell.PrepareForEdit(); + } + + e.Handled = true; + } + else if ((e.Key is VirtualKey.Left or VirtualKey.Right or VirtualKey.Up or VirtualKey.Down) + && SelectionUnit is not TableViewSelectionUnit.Row + && !IsEditing) + { + var row = CurrentCellSlot?.Row ?? 0; + var column = CurrentCellSlot?.Column ?? 0; + + if (e.Key is VirtualKey.Left or VirtualKey.Right) + { + column = e.Key is VirtualKey.Left ? ctrlKey ? 0 : column - 1 : ctrlKey ? Columns.VisibleColumns.Count - 1 : column + 1; + } + else + { + row = e.Key == VirtualKey.Up ? ctrlKey ? 0 : row - 1 : ctrlKey ? Items.Count - 1 : row + 1; + } + + var newSlot = new TableViewCellSlot(row, column); + SelectCells(newSlot, shiftKey); + e.Handled = true; + } } protected override void OnApplyTemplate() @@ -52,6 +170,7 @@ private void OnLoaded(object sender, RoutedEventArgs e) return; } + _scrollViewer = scrollViewer; Canvas.SetZIndex(ItemsPanelRoot, -1); var scrollProperties = ElementCompositionPreview.GetScrollViewerManipulationPropertySet(scrollViewer); @@ -68,6 +187,47 @@ private void OnLoaded(object sender, RoutedEventArgs e) UpdateVerticalScrollBarMargin(); } + private TableViewCellSlot GetNextSlot(TableViewCellSlot? currentSlot, bool isShiftKeyDown, bool isEnterKey) + { + var rows = Items.Count; + var columns = Columns.VisibleColumns.Count; + var currentRow = currentSlot?.Row ?? 0; + var currentColumn = currentSlot?.Column ?? 0; + var nextRow = currentRow; + var nextColumn = currentColumn; + + if (isEnterKey) + { + nextRow += isShiftKeyDown ? -1 : 1; + if (nextRow < 0) + { + nextRow = rows - 1; + nextColumn = (nextColumn - 1 + columns) % columns; + } + else if (nextRow >= rows) + { + nextRow = 0; + nextColumn = (nextColumn + 1) % columns; + } + } + else + { + nextColumn += isShiftKeyDown ? -1 : 1; + if (nextColumn < 0) + { + nextColumn = columns - 1; + nextRow = (nextRow - 1 + rows) % rows; + } + else if (nextColumn >= columns) + { + nextColumn = 0; + nextRow = (nextRow + 1) % rows; + } + } + + return new TableViewCellSlot(nextRow, nextColumn); + } + private bool Filter(object obj) { return ActiveFilters.All(item => item.Value(obj)); @@ -84,7 +244,7 @@ internal void CopyToClipboardInternal(bool includeHeaders) } var package = new DataPackage(); - package.SetText(GetSelectedRowsContent(includeHeaders)); + package.SetText(GetSelectedContent(includeHeaders)); Clipboard.SetContent(package); } @@ -93,32 +253,67 @@ protected virtual void OnCopyToClipboard(TableViewCopyToClipboardEventArgs args) CopyToClipboard?.Invoke(this, args); } - public string GetSelectedRowsContent(bool includeHeaders, char separator = '\t') + public string GetSelectedContent(bool includeHeaders, char separator = '\t') { - var items = SelectedItems.OrderBy(item2 => Items.IndexOf(item2)); - return GetRowsContent(items, includeHeaders, separator); + var slots = Enumerable.Empty(); + + if (SelectedItems.Any() || SelectedCells.Any()) + { + slots = SelectedRanges.SelectMany(x => Enumerable.Range(x.FirstIndex, (int)x.Length)) + .SelectMany(r => Enumerable.Range(0, Columns.VisibleColumns.Count) + .Select(c => new TableViewCellSlot(r, c))) + .OrderBy(x => x.Row) + .ThenByDescending(x => x.Column); + } + else if (CurrentCellSlot.HasValue) + { + slots = new[] { CurrentCellSlot.Value }; + } + + return GetCellsContent(slots, includeHeaders, separator); } - public string GetAllRowsContent(bool includeHeaders, char separator = '\t') + public string GetAllContent(bool includeHeaders, char separator = '\t') { - return GetRowsContent(Items, includeHeaders, separator); + var slots = Enumerable.Range(0, Items.Count) + .SelectMany(r => Enumerable.Range(0, Columns.VisibleColumns.Count) + .Select(c => new TableViewCellSlot(r, c))) + .Concat(SelectedCells) + .OrderBy(x => x.Row) + .ThenByDescending(x => x.Column); + + return GetCellsContent(slots, includeHeaders, separator); } - private string GetRowsContent(IEnumerable items, bool includeHeaders, char separator) + private string GetCellsContent(IEnumerable slots, bool includeHeaders, char separator) { + var minRow = slots.Select(x => x.Row).Min(); + var maxRow = slots.Select(x => x.Row).Max(); + var minColumn = slots.Select(x => x.Column).Min(); + var maxColumn = slots.Select(x => x.Column).Max(); + var stringBuilder = new StringBuilder(); var properties = new Dictionary(); if (includeHeaders) { - stringBuilder.AppendLine(GetHeadersContent(separator)); + stringBuilder.AppendLine(GetHeadersContent(separator, minColumn, maxColumn)); } - foreach (var item in items) + for (var row = minRow; row <= maxRow; row++) { + var item = Items[row]; var type = ItemsSource?.GetType() is { } listType && listType.IsGenericType ? listType.GetGenericArguments()[0] : item?.GetType(); - foreach (var column in Columns.VisibleColumns.OfType()) + + for (var col = minColumn; col <= maxColumn; col++) { + if (Columns.VisibleColumns[col] is not TableViewBoundColumn column || + !slots.Contains(new TableViewCellSlot(row, col))) + { + stringBuilder.Append('\t'); + continue; + } + var property = column.Binding.Path.Path; if (!properties.TryGetValue(property, out var pis)) { @@ -138,53 +333,16 @@ private string GetRowsContent(IEnumerable items, bool includeHeaders, ch return stringBuilder.ToString(); } - private string GetHeadersContent(char separator) - { - return string.Join(separator, Columns.VisibleColumns.OfType().Select(x => x.Header)); - } - - internal async void SelectNextRow() - { - var nextIndex = SelectedIndex + 1; - if (nextIndex < Items.Count) - { - SelectedIndex = nextIndex; - } - else if (TabNavigation == KeyboardNavigationMode.Cycle) - { - SelectedIndex = 0; - } - else - { - return; - } - - await Task.Delay(5); - var listViewItem = ContainerFromItem(SelectedItem) as ListViewItem; - var row = listViewItem?.FindDescendant(); - row?.SelectNextCell(null); - } - - internal async void SelectPreviousRow() + private string GetHeadersContent(char separator, int minColumn, int maxColumn) { - var previousIndex = SelectedIndex - 1; - if (previousIndex >= 0) - { - SelectedIndex = previousIndex; - } - else if (TabNavigation == KeyboardNavigationMode.Cycle) - { - SelectedIndex = Items.Count - 1; - } - else + var stringBuilder = new StringBuilder(); + for (var col = minColumn; col <= maxColumn; col++) { - return; + var column = Columns.VisibleColumns[col]; + stringBuilder.Append($"{column.Header}{separator}"); } - await Task.Delay(5); - var listViewItem = ContainerFromItem(SelectedItem) as ListViewItem; - var row = listViewItem?.FindDescendant(); - row?.SelectPreviousCell(null); + return stringBuilder.ToString(); } private void GenerateColumns() @@ -275,7 +433,7 @@ private void RemoveAutoGeneratedColumns() internal async void ExportSelectedToCSV() { var args = new TableViewExportRowsContentEventArgs(); - OnExportSelectedRowsContent(args); + OnExportSelectedContent(args); if (args.Handled) { @@ -290,7 +448,7 @@ internal async void ExportSelectedToCSV() return; } - var content = GetSelectedRowsContent(true, ','); + var content = GetSelectedContent(true, ','); using var stream = await file.OpenStreamForWriteAsync(); stream.SetLength(0); @@ -300,7 +458,7 @@ internal async void ExportSelectedToCSV() catch { } } - protected virtual void OnExportSelectedRowsContent(TableViewExportRowsContentEventArgs args) + protected virtual void OnExportSelectedContent(TableViewExportRowsContentEventArgs args) { ExportSelectedRowsContent?.Invoke(this, args); } @@ -308,7 +466,7 @@ protected virtual void OnExportSelectedRowsContent(TableViewExportRowsContentEve internal async void ExportAllToCSV() { var args = new TableViewExportRowsContentEventArgs(); - OnExportAllRowsContent(args); + OnExportAllContent(args); if (args.Handled) { @@ -323,7 +481,7 @@ internal async void ExportAllToCSV() return; } - var content = GetAllRowsContent(true, ','); + var content = GetAllContent(true, ','); using var stream = await file.OpenStreamForWriteAsync(); stream.SetLength(0); @@ -333,7 +491,7 @@ internal async void ExportAllToCSV() catch { } } - protected virtual void OnExportAllRowsContent(TableViewExportRowsContentEventArgs args) + protected virtual void OnExportAllContent(TableViewExportRowsContentEventArgs args) { ExportAllRowsContent?.Invoke(this, args); } @@ -347,66 +505,6 @@ private static async Task GetStorageFile(IntPtr hWnd) return await savePicker.PickSaveFileAsync(); } - private static void OnItemsSourceChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableView tableView) - { - tableView.OnItemsSourceChanged(e); - } - } - - private static void OnHeaderRowHeightChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - (d as TableView)?.UpdateVerticalScrollBarMargin(); - } - - private static void OnAutoGenerateColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableView tableView) - { - if (tableView.AutoGenerateColumns) - { - tableView.GenerateColumns(); - } - else - { - tableView.RemoveAutoGeneratedColumns(); - } - } - } - - private static void OnCanSortColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableView tableView && e.NewValue is false) - { - tableView.ClearSorting(); - } - } - - private static void OnCanFilterColumnsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableView tableView && e.NewValue is false) - { - tableView.ClearFilters(); - } - } - - private static void OnMinColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableView table && table._headerRow is not null) - { - table._headerRow.CalculateHeaderWidths(); - } - } - - private static void OnMaxColumnWidthChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) - { - if (d is TableView table && table._headerRow is not null) - { - table._headerRow.CalculateHeaderWidths(); - } - } - private void UpdateVerticalScrollBarMargin() { if (GetTemplateChild("ScrollViewer") is ScrollViewer scrollViewer) @@ -421,6 +519,7 @@ private void UpdateVerticalScrollBarMargin() internal void ClearSorting() { + DeselectAll(); CollectionView.SortDescriptions.Clear(); foreach (var header in Columns.Select(x => x.HeaderControl)) @@ -434,6 +533,7 @@ internal void ClearSorting() internal void ClearFilters() { + DeselectAll(); ActiveFilters.Clear(); CollectionView.RefreshFilter(); @@ -446,106 +546,328 @@ internal void ClearFilters() } } - public IAdvancedCollectionView CollectionView { get; } = new AdvancedCollectionView(); - - internal IDictionary> ActiveFilters { get; } = new Dictionary>(); - - public TableViewColumnsCollection Columns { get; } = new(); - - public double HeaderRowHeight + private bool IsValidSlot(TableViewCellSlot slot) { - get => (double)GetValue(HeaderRowHeightProperty); - set => SetValue(HeaderRowHeightProperty, value); + return slot.Row >= 0 && slot.Column >= 0 && slot.Row < Items.Count && slot.Column < Columns.VisibleColumns.Count; } - public double RowHeight + internal new void SelectAll() { - get => (double)GetValue(RowHeightProperty); - set => SetValue(RowHeightProperty, value); + if (IsEditing) + { + return; + } + + if (SelectionUnit is TableViewSelectionUnit.Cell) + { + SelectAllCells(); + SetCurrentCell(null); + } + else + { + switch (SelectionMode) + { + case ListViewSelectionMode.Single: + SelectedItem = Items.FirstOrDefault(); + break; + case ListViewSelectionMode.Multiple: + case ListViewSelectionMode.Extended: + SelectRange(new ItemIndexRange(0, (uint)Items.Count)); + break; + } + } } - public double RowMaxHeight + private void SelectAllCells() { - get => (double)GetValue(RowMaxHeightProperty); - set => SetValue(RowMaxHeightProperty, value); + switch (SelectionMode) + { + case ListViewSelectionMode.Single: + if (Items.Count > 0 && Columns.VisibleColumns.Count > 0) + { + SelectedCellRanges.Clear(); + SelectedCellRanges.Add(new() { new TableViewCellSlot(0, 0) }); + } + break; + case ListViewSelectionMode.Multiple: + case ListViewSelectionMode.Extended: + SelectedCellRanges.Clear(); + var selectionRange = new HashSet(); + + for (var row = 0; row < Items.Count; row++) + { + for (var column = 0; column < Columns.VisibleColumns.Count; column++) + { + selectionRange.Add(new TableViewCellSlot(row, column)); + } + } + SelectedCellRanges.Add(selectionRange); + break; + } + + OnCellSelectionChanged(); } - public new IList ItemsSource + internal void DeselectAll() { - get => (IList)GetValue(ItemsSourceProperty); - set => SetValue(ItemsSourceProperty, value); + switch (SelectionMode) + { + case ListViewSelectionMode.Single: + SelectedItem = null; + break; + case ListViewSelectionMode.Multiple: + case ListViewSelectionMode.Extended: + DeselectRange(new ItemIndexRange(0, (uint)Items.Count)); + break; + } + + DeselectAllCells(); } - public bool ShowExportOptions + private void DeselectAllCells() { - get => (bool)GetValue(ShowExportOptionsProperty); - set => SetValue(ShowExportOptionsProperty, value); + SelectedCellRanges.Clear(); + OnCellSelectionChanged(); + SetCurrentCell(null); } - public bool AutoGenerateColumns + internal async void SelectCells(TableViewCellSlot slot, bool shiftKey, bool ctrlKey = false) { - get => (bool)GetValue(AutoGenerateColumnsProperty); - set => SetValue(AutoGenerateColumnsProperty, value); + if (!IsValidSlot(slot)) + { + return; + } + + if (SelectionMode != ListViewSelectionMode.None && SelectionUnit != TableViewSelectionUnit.Row) + { + ctrlKey = ctrlKey || SelectionMode is ListViewSelectionMode.Multiple; + if (!ctrlKey || !(SelectionMode is ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended)) + { + DeselectAll(); + } + + var selectionRange = (SelectionStartCellSlot is null ? null : SelectedCellRanges.LastOrDefault(x => SelectionStartCellSlot.HasValue && x.Contains(SelectionStartCellSlot.Value))) ?? new HashSet(); + SelectedCellRanges.Remove(selectionRange); + selectionRange.Clear(); + SelectionStartCellSlot ??= CurrentCellSlot; + if (shiftKey && SelectionMode is ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended) + { + var currentSlot = SelectionStartCellSlot ?? slot; + var startRow = Math.Min(slot.Row, currentSlot.Row); + var endRow = Math.Max(slot.Row, currentSlot.Row); + var startCol = Math.Min(slot.Column, currentSlot.Column); + var endCol = Math.Max(slot.Column, currentSlot.Column); + for (var row = startRow; row <= endRow; row++) + { + for (var column = startCol; column <= endCol; column++) + { + var nextSlot = new TableViewCellSlot(row, column); + selectionRange.Add(nextSlot); + if (SelectedCellRanges.LastOrDefault(x => x.Contains(nextSlot)) is { } range) + { + range.Remove(nextSlot); + } + } + } + } + else + { + SelectionStartCellSlot = null; + selectionRange.Add(slot); + + if (SelectedCellRanges.LastOrDefault(x => x.Contains(slot)) is { } range) + { + range.Remove(slot); + } + } + + SetCurrentCell((await ScrollCellIntoView(slot)).Slot); + SelectedCellRanges.Add(selectionRange); + OnCellSelectionChanged(); + } + else + { + SetCurrentCell((await ScrollCellIntoView(slot)).Slot); + } } - public bool IsReadOnly + internal void DeselectCell(TableViewCellSlot slot) { - get => (bool)GetValue(IsReadOnlyProperty); - set => SetValue(IsReadOnlyProperty, value); + var selectionRange = SelectedCellRanges.LastOrDefault(x => x.Contains(slot)); + selectionRange?.Remove(slot); + + if (selectionRange?.Count == 0) + { + SelectedCellRanges.Remove(selectionRange); + } + + SetCurrentCell(slot); + OnCellSelectionChanged(); } - public bool ShowOptionsButton + internal void SetCurrentCell(TableViewCellSlot? slot) { - get => (bool)GetValue(ShowOptionsButtonProperty); - set => SetValue(ShowOptionsButtonProperty, value); + var oldSlot = CurrentCellSlot; + CurrentCellSlot = slot; + CurrentCellChanged?.Invoke(this, new TableViewCurrentCellChangedEventArgs(oldSlot, slot)); } - public bool CanResizeColumns + private void OnCellSelectionChanged() { - get => (bool)GetValue(CanResizeColumnsProperty); - set => SetValue(CanResizeColumnsProperty, value); + var oldSelection = SelectedCells; + SelectedCells = new HashSet(SelectedCellRanges.SelectMany(x => x)); + SelectedCellsChanged?.Invoke(this, new TableViewCellSelectionChangedEvenArgs(oldSelection, SelectedCells)); } - public bool CanSortColumns + internal async Task ScrollCellIntoView(TableViewCellSlot slot) { - get => (bool)GetValue(CanSortColumnsProperty); - set => SetValue(CanSortColumnsProperty, value); + var row = await ScrollRowIntoView(slot.Row); + var (start, end) = GetColumnsInDisplay(); + var xOffset = 0d; + var yOffset = _scrollViewer.VerticalOffset; + + if (slot.Column < start) + { + for (var i = 0; i < slot.Column; i++) + { + xOffset += Columns.VisibleColumns[i].ActualWidth; + } + } + else if (slot.Column > end) + { + for (var i = 0; i <= slot.Column; i++) + { + xOffset += Columns.VisibleColumns[i].ActualWidth; + } + + var change = xOffset - _scrollViewer.HorizontalOffset - (_scrollViewer.ViewportWidth - 16); + xOffset = _scrollViewer.HorizontalOffset + change; + } + else if (row is not null) + { + return row.Cells.ElementAt(slot.Column); + } + + var tcs = new TaskCompletionSource(); + + void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) + { + if (e.IsIntermediate) + { + return; + } + + tcs.TrySetResult(result: default); + } + + try + { + _scrollViewer.ViewChanged += ViewChanged; + _scrollViewer.ChangeView(xOffset, yOffset, null, true); + _scrollViewer.ScrollToHorizontalOffset(xOffset); + await tcs.Task; + } + finally + { + _scrollViewer.ViewChanged -= ViewChanged; + } + + return row?.Cells.ElementAt(slot.Column)!; } - public bool CanFilterColumns + private async Task ScrollRowIntoView(int index) { - get => (bool)GetValue(CanFilterColumnsProperty); - set => SetValue(CanFilterColumnsProperty, value); + var item = Items[index]; + index = Items.IndexOf(item); // if the ItemsSource has duplicate items in it. ScrollIntoView will only bring first index of item. + ScrollIntoView(item); + + var tries = 0; + while (tries < 10) + { + if (ContainerFromIndex(index) is TableViewRow row) + { + var transform = row.TransformToVisual(_scrollViewer); + var positionInScrollViewer = transform.TransformPoint(new Point(0, 0)); + if ((index == 0 && _scrollViewer.VerticalOffset > 0) || (index > 0 && positionInScrollViewer.Y <= row.ActualHeight)) + { + var xOffset = _scrollViewer.HorizontalOffset; + var yOffset = index == 0 ? 0d : _scrollViewer.VerticalOffset - row.ActualHeight + positionInScrollViewer.Y + 8; + var tcs = new TaskCompletionSource(); + + try + { + _scrollViewer.ViewChanged += ViewChanged; + _scrollViewer.ChangeView(xOffset, yOffset, null, true); + await tcs.Task; + } + finally + { + _scrollViewer.ViewChanged -= ViewChanged; + } + + void ViewChanged(object? _, ScrollViewerViewChangedEventArgs e) + { + if (e.IsIntermediate) + { + return; + } + + tcs.TrySetResult(result: default); + } + } + + row.Focus(FocusState.Programmatic); + return row; + } + + tries++; + await Task.Delay(1); // let the animation complete + } + + return default; } - public double MinColumnWidth + private TableViewCell GetCellFromSlot(TableViewCellSlot slot) { - get => (double)GetValue(MinColumnWidthProperty); - set => SetValue(MinColumnWidthProperty, value); + return ContainerFromIndex(slot.Row) is TableViewRow row ? row.Cells[slot.Column] : default!; } - public double MaxColumnWidth + private (int start, int end) GetColumnsInDisplay() { - get => (double)GetValue(MaxColumnWidthProperty); - set => SetValue(MaxColumnWidthProperty, value); + var start = -1; + var end = -1; + var width = 0d; + foreach (var column in Columns.VisibleColumns) + { + if (width >= _scrollViewer.HorizontalOffset && width + column.ActualWidth <= _scrollViewer.HorizontalOffset + _scrollViewer.ViewportWidth) + { + if (start == -1) + { + start = Columns.VisibleColumns.IndexOf(column); + } + else + { + end = Columns.VisibleColumns.IndexOf(column); + } + } + + width += column.ActualWidth; + } + + return (start, end); } - public static readonly new DependencyProperty ItemsSourceProperty = DependencyProperty.Register(nameof(ItemsSource), typeof(IList), typeof(TableView), new PropertyMetadata(null, OnItemsSourceChanged)); - public static readonly DependencyProperty HeaderRowHeightProperty = DependencyProperty.Register(nameof(HeaderRowHeight), typeof(double), typeof(TableView), new PropertyMetadata(32d, OnHeaderRowHeightChanged)); - public static readonly DependencyProperty RowHeightProperty = DependencyProperty.Register(nameof(RowHeight), typeof(double), typeof(TableView), new PropertyMetadata(40d)); - public static readonly DependencyProperty RowMaxHeightProperty = DependencyProperty.Register(nameof(RowMaxHeight), typeof(double), typeof(TableView), new PropertyMetadata(double.PositiveInfinity)); - public static readonly DependencyProperty ShowExportOptionsProperty = DependencyProperty.Register(nameof(ShowExportOptions), typeof(bool), typeof(TableView), new PropertyMetadata(false)); - public static readonly DependencyProperty AutoGenerateColumnsProperty = DependencyProperty.Register(nameof(AutoGenerateColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnAutoGenerateColumnsChanged)); - public static readonly DependencyProperty IsReadOnlyProperty = DependencyProperty.Register(nameof(IsReadOnly), typeof(bool), typeof(TableView), new PropertyMetadata(false)); - public static readonly DependencyProperty ShowOptionsButtonProperty = DependencyProperty.Register(nameof(ShowOptionsButton), typeof(bool), typeof(TableView), new PropertyMetadata(true)); - public static readonly DependencyProperty CanResizeColumnsProperty = DependencyProperty.Register(nameof(CanResizeColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true)); - public static readonly DependencyProperty CanSortColumnsProperty = DependencyProperty.Register(nameof(CanSortColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnCanSortColumnsChanged)); - public static readonly DependencyProperty CanFilterColumnsProperty = DependencyProperty.Register(nameof(CanFilterColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnCanFilterColumnsChanged)); - public static readonly DependencyProperty MinColumnWidthProperty = DependencyProperty.Register(nameof(MinColumnWidth), typeof(double), typeof(TableView), new PropertyMetadata(50d, OnMinColumnWidthChanged)); - public static readonly DependencyProperty MaxColumnWidthProperty = DependencyProperty.Register(nameof(MaxColumnWidth), typeof(double), typeof(TableView), new PropertyMetadata(double.PositiveInfinity, OnMaxColumnWidthChanged)); + private void UpdateBaseSelectionMode() + { + _shouldThrowSelectionModeChangedException = true; + base.SelectionMode = SelectionUnit is TableViewSelectionUnit.Cell ? ListViewSelectionMode.None : SelectionMode; + _shouldThrowSelectionModeChangedException = false; + } public event EventHandler? AutoGeneratingColumn; public event EventHandler? ExportAllRowsContent; public event EventHandler? ExportSelectedRowsContent; public event EventHandler? CopyToClipboard; + internal event EventHandler? SelectedCellsChanged; + internal event EventHandler? CurrentCellChanged; } diff --git a/src/WinUI.TableView/TableViewCell.cs b/src/WinUI.TableView/TableViewCell.cs index 147a81b..c045aa0 100644 --- a/src/WinUI.TableView/TableViewCell.cs +++ b/src/WinUI.TableView/TableViewCell.cs @@ -1,4 +1,4 @@ -using Microsoft.UI.Input; +using CommunityToolkit.WinUI; using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; @@ -7,17 +7,26 @@ using System.Linq; using System.Threading.Tasks; using Windows.Foundation; -using Windows.System; -using Windows.UI.Core; -using CommunityToolkit.WinUI; namespace WinUI.TableView; +[TemplateVisualState(Name = VisualStates.StateNormal, GroupName = VisualStates.GroupCommon)] +[TemplateVisualState(Name = VisualStates.StatePointerOver, GroupName = VisualStates.GroupCommon)] +[TemplateVisualState(Name = VisualStates.StateRegular, GroupName = VisualStates.GroupCurrent)] +[TemplateVisualState(Name = VisualStates.StateCurrent, GroupName = VisualStates.GroupCurrent)] +[TemplateVisualState(Name = VisualStates.StateSelected, GroupName = VisualStates.GroupSelection)] +[TemplateVisualState(Name = VisualStates.StateUnselected, GroupName = VisualStates.GroupSelection)] public class TableViewCell : ContentControl { public TableViewCell() { DefaultStyleKey = typeof(TableViewCell); + Loaded += OnLoaded; + } + + private void OnLoaded(object sender, RoutedEventArgs e) + { + ApplySelectionState(); } protected override Size MeasureOverride(Size availableSize) @@ -32,76 +41,98 @@ protected override Size MeasureOverride(Size availableSize) return size; } - protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) + protected override void OnPointerEntered(PointerRoutedEventArgs e) { - if (!IsReadOnly) - { - PrepareForEdit(); - } + base.OnPointerEntered(e); + + VisualStates.GoToState(this, false, VisualStates.StatePointerOver); } - protected override void OnKeyDown(KeyRoutedEventArgs e) + protected override void OnPointerExited(PointerRoutedEventArgs e) { - base.OnKeyDown(e); + base.OnPointerEntered(e); - if (e.Key is VirtualKey.Tab or VirtualKey.Enter && - Row is not null && - !VisualTreeHelper.GetOpenPopupsForXamlRoot(XamlRoot).Any()) - { - var shiftKey = InputKeyboardSource.GetKeyStateForCurrentThread(VirtualKey.Shift); - var isShiftKeyDown = shiftKey is CoreVirtualKeyStates.Down or (CoreVirtualKeyStates.Down | CoreVirtualKeyStates.Locked); + VisualStates.GoToState(this, false, VisualStates.StateNormal); + } - if (isShiftKeyDown) - { - Row.SelectPreviousCell(this); - } - else - { - Row.SelectNextCell(this); - } + protected override void OnTapped(TappedRoutedEventArgs e) + { + base.OnTapped(e); + + var shiftKey = KeyBoardHelper.IsShiftKeyDown(); + var ctrlKey = KeyBoardHelper.IsCtrlKeyDown(); + + if (IsSelected && (ctrlKey || TableView.SelectionMode is ListViewSelectionMode.Multiple) && !shiftKey) + { + TableView.DeselectCell(Slot); } - else if (e.Key == VirtualKey.Escape) + else { - if (!VisualTreeHelper.GetOpenPopupsForXamlRoot(XamlRoot).Any()) - { - SetElement(); - } + TableView.SelectCells(Slot, shiftKey, ctrlKey); } + + Focus(FocusState.Programmatic); } - internal async void PrepareForEdit() + protected override void OnPointerPressed(PointerRoutedEventArgs e) { - SetEditingElement(); + base.OnPointerPressed(e); - await Task.Delay(20); + if (!KeyBoardHelper.IsShiftKeyDown()) + { + TableView.SelectionStartCellSlot = Slot; + } - if ((Content ?? ContentTemplateRoot) is UIElement editingElement) + e.Handled = TableView.SelectionUnit != TableViewSelectionUnit.Row; + } + + protected override void OnPointerReleased(PointerRoutedEventArgs e) + { + base.OnPointerReleased(e); + + if (!KeyBoardHelper.IsShiftKeyDown()) { - editingElement.Focus(FocusState.Programmatic); + TableView.SelectionStartCellSlot = null; } } - protected override async void OnLostFocus(RoutedEventArgs e) + protected override void OnPointerMoved(PointerRoutedEventArgs e) { - base.OnLostFocus(e); + base.OnPointerMoved(e); - await Task.Delay(20); + var point = e.GetCurrentPoint(this); - var focusedElement = FocusManager.GetFocusedElement(XamlRoot) as FrameworkElement; - if (focusedElement?.FindAscendantOrSelf() == this) + if (point.Properties.IsLeftButtonPressed && !TableView.IsEditing) { - return; + var ctrlKey = KeyBoardHelper.IsCtrlKeyDown(); + + TableView.SelectCells(Slot, true, ctrlKey); } + } - if (VisualTreeHelper.GetOpenPopupsForXamlRoot(XamlRoot).Any()) + protected override void OnDoubleTapped(DoubleTappedRoutedEventArgs e) + { + if (!IsReadOnly) { - return; + PrepareForEdit(); + + TableView.IsEditing = true; } + } + + internal async void PrepareForEdit() + { + SetEditingElement(); + + await Task.Delay(20); - SetElement(); + if ((Content ?? ContentTemplateRoot) is UIElement editingElement) + { + editingElement.Focus(FocusState.Programmatic); + } } - private void SetElement() + internal void SetElement() { if (Column is TableViewTemplateColumn templateColumn) { @@ -123,18 +154,49 @@ private void SetEditingElement() { Content = Column.GenerateEditingElement(); } + + if (TableView is not null) + { + TableView.IsEditing = true; + } + } + + internal void ApplySelectionState() + { + var stateName = IsSelected ? VisualStates.StateSelected : VisualStates.StateUnselected; + VisualStates.GoToState(this, false, stateName); + } + + internal void ApplyCurrentCellState() + { + var stateName = IsCurrent ? VisualStates.StateCurrent : VisualStates.StateRegular; + VisualStates.GoToState(this, false, stateName); } private static void OnColumnChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { if (d is TableViewCell cell && e.NewValue is TableViewColumn column) { - cell.SetElement(); + if (cell.TableView?.IsEditing == true) + { + cell.SetEditingElement(); + } + else + { + cell.SetElement(); + } } } public bool IsReadOnly => TableView.IsReadOnly || Column is TableViewTemplateColumn { EditingTemplate: null } or { IsReadOnly: true }; + internal TableViewCellSlot Slot => new(Row.Index, Index); + + internal int Index { get; set; } + + public bool IsSelected => TableView.SelectedCells.Contains(Slot); + public bool IsCurrent => TableView.CurrentCellSlot == Slot; + public TableViewColumn Column { get => (TableViewColumn)GetValue(ColumnProperty); @@ -156,4 +218,4 @@ public TableView TableView public static readonly DependencyProperty ColumnProperty = DependencyProperty.Register(nameof(Column), typeof(TableViewColumn), typeof(TableViewCell), new PropertyMetadata(default, OnColumnChanged)); public static readonly DependencyProperty TableViewRowProperty = DependencyProperty.Register(nameof(Row), typeof(TableViewRow), typeof(TableViewCell), new PropertyMetadata(default)); public static readonly DependencyProperty TableViewProperty = DependencyProperty.Register(nameof(TableView), typeof(TableView), typeof(TableViewCell), new PropertyMetadata(default)); -} \ No newline at end of file +} diff --git a/src/WinUI.TableView/TableViewCellSelectionChangedEvenArgs.cs b/src/WinUI.TableView/TableViewCellSelectionChangedEvenArgs.cs new file mode 100644 index 0000000..029cda6 --- /dev/null +++ b/src/WinUI.TableView/TableViewCellSelectionChangedEvenArgs.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; + +namespace WinUI.TableView; + +internal class TableViewCellSelectionChangedEvenArgs : EventArgs +{ + public TableViewCellSelectionChangedEvenArgs(HashSet oldSelection, + HashSet newSelection) + { + OldSelection = oldSelection; + NewSelection = newSelection; + } + + public HashSet OldSelection { get; } + public HashSet NewSelection { get; } +} \ No newline at end of file diff --git a/src/WinUI.TableView/TableViewCellSlot.cs b/src/WinUI.TableView/TableViewCellSlot.cs new file mode 100644 index 0000000..fc09909 --- /dev/null +++ b/src/WinUI.TableView/TableViewCellSlot.cs @@ -0,0 +1,2 @@ +namespace WinUI.TableView; +internal readonly record struct TableViewCellSlot(int Row, int Column); diff --git a/src/WinUI.TableView/TableViewColumnHeader.cs b/src/WinUI.TableView/TableViewColumnHeader.cs index 49abd36..0451f43 100644 --- a/src/WinUI.TableView/TableViewColumnHeader.cs +++ b/src/WinUI.TableView/TableViewColumnHeader.cs @@ -74,6 +74,8 @@ private void DoSort(SD direction, bool singleSorting = true) } } + _tableView.DeselectAll(); + if (_tableView.CollectionView.SortDescriptions.FirstOrDefault(x => x.PropertyName == _propertyPath) is { } description) { _tableView.CollectionView.SortDescriptions.Remove(description); @@ -88,6 +90,7 @@ private void ClearSorting() { if (CanSort && _tableView is not null && SortDirection is not null) { + _tableView.DeselectAll(); SortDirection = null; if (_tableView.CollectionView.SortDescriptions.FirstOrDefault(x => x.PropertyName == _propertyPath) is { } description) @@ -102,6 +105,7 @@ private void ClearFilter() if (_tableView?.ActiveFilters.ContainsKey(_propertyPath) == true) { _tableView.ActiveFilters.Remove(_propertyPath); + _tableView.DeselectAll(); } IsFiltered = false; @@ -116,6 +120,7 @@ private void ApplyFilter() return; } + _tableView.DeselectAll(); _tableView.ActiveFilters[_propertyPath] = Filter; _tableView.CollectionView.RefreshFilter(); IsFiltered = true; diff --git a/src/WinUI.TableView/TableViewCurrentCellChangedEventArgs.cs b/src/WinUI.TableView/TableViewCurrentCellChangedEventArgs.cs new file mode 100644 index 0000000..1d8ef7a --- /dev/null +++ b/src/WinUI.TableView/TableViewCurrentCellChangedEventArgs.cs @@ -0,0 +1,15 @@ +using System; + +namespace WinUI.TableView; + +internal class TableViewCurrentCellChangedEventArgs : EventArgs +{ + public TableViewCurrentCellChangedEventArgs(TableViewCellSlot? oldSlot, TableViewCellSlot? newSlot) + { + OldSlot = oldSlot; + NewSlot = newSlot; + } + + public TableViewCellSlot? OldSlot { get; } + public TableViewCellSlot? NewSlot { get; } +} \ No newline at end of file diff --git a/src/WinUI.TableView/TableViewHeaderRow.OptionsFlyoutViewModel.cs b/src/WinUI.TableView/TableViewHeaderRow.OptionsFlyoutViewModel.cs index fc3b804..b8549f2 100644 --- a/src/WinUI.TableView/TableViewHeaderRow.OptionsFlyoutViewModel.cs +++ b/src/WinUI.TableView/TableViewHeaderRow.OptionsFlyoutViewModel.cs @@ -1,8 +1,6 @@ -using CommunityToolkit.WinUI; -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; using Microsoft.UI.Xaml.Input; -using System.Linq; namespace WinUI.TableView; @@ -19,12 +17,12 @@ public OptionsFlyoutViewModel(TableView _tableView) private void InitializeCommands() { SelectAllCommand.Description = "Select all rows."; - SelectAllCommand.ExecuteRequested += delegate { TableView.SelectAllSafe(); }; + SelectAllCommand.ExecuteRequested += delegate { TableView.SelectAll(); }; SelectAllCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.SelectionMode is ListViewSelectionMode.Multiple or ListViewSelectionMode.Extended; DeselectAllCommand.Description = "Deselect all rows."; DeselectAllCommand.ExecuteRequested += delegate { TableView.DeselectAll(); }; - DeselectAllCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.SelectedItems.Count > 0; + DeselectAllCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.SelectedItems.Count > 0 || TableView.SelectedCells.Count > 0; CopyCommand.Description = "Copy the selected row's content to clipboard."; CopyCommand.ExecuteRequested += delegate @@ -37,9 +35,11 @@ private void InitializeCommands() TableView.CopyToClipboardInternal(false); }; + CopyCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.SelectedItems.Count > 0 || TableView.SelectedCells.Count > 0 || TableView.CurrentCellSlot.HasValue; CopyWithHeadersCommand.Description = "Copy the selected row's content including column headers to clipboard."; CopyWithHeadersCommand.ExecuteRequested += delegate { TableView.CopyToClipboardInternal(true); }; + CopyWithHeadersCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.SelectedItems.Count > 0 || TableView.SelectedCells.Count > 0 || TableView.CurrentCellSlot.HasValue; ClearSortingCommand.ExecuteRequested += delegate { TableView.ClearSorting(); }; ClearSortingCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.CollectionView.SortDescriptions.Count > 0; @@ -50,11 +50,9 @@ private void InitializeCommands() ExportAllToCSVCommand.ExecuteRequested += delegate { TableView.ExportAllToCSV(); }; ExportSelectedToCSVCommand.ExecuteRequested += delegate { TableView.ExportSelectedToCSV(); }; - ExportSelectedToCSVCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.SelectedItems.Count > 0; + ExportSelectedToCSVCommand.CanExecuteRequested += (_, e) => e.CanExecute = TableView.SelectedItems.Count > 0 || TableView.SelectedCells.Count > 0 || TableView.CurrentCellSlot.HasValue; } - - public StandardUICommand SelectAllCommand { get; } = new(StandardUICommandKind.SelectAll); public StandardUICommand DeselectAllCommand { get; } = new() { Label = "Deselect All" }; public StandardUICommand CopyCommand { get; } = new(StandardUICommandKind.Copy); diff --git a/src/WinUI.TableView/TableViewRow.cs b/src/WinUI.TableView/TableViewRow.cs index 71b5076..627cfb8 100644 --- a/src/WinUI.TableView/TableViewRow.cs +++ b/src/WinUI.TableView/TableViewRow.cs @@ -1,36 +1,53 @@ -using CommunityToolkit.WinUI; -using Microsoft.UI.Xaml; +using Microsoft.UI.Xaml; using Microsoft.UI.Xaml.Controls; +using Microsoft.UI.Xaml.Controls.Primitives; using System; using System.Collections.Generic; using System.Collections.Specialized; using System.Linq; namespace WinUI.TableView; - -public class TableViewRow : Control +public class TableViewRow : ListViewItem { - private TableView? _tableView; - private StackPanel? _cellsStackPanel; + private TableViewCellPresenter? _cellPresenter; + private ListViewItemPresenter _itemPresenter = null!; public TableViewRow() { DefaultStyleKey = typeof(TableViewRow); + SizeChanged += OnSizeChanged; } protected override void OnApplyTemplate() { base.OnApplyTemplate(); - _tableView = this.FindAscendant(); - _cellsStackPanel = GetTemplateChild("PART_StackPanel") as StackPanel; + _itemPresenter = (ListViewItemPresenter)GetTemplateChild("Root"); + } + + protected override void OnContentChanged(object oldContent, object newContent) + { + if (TableView is null) + { + return; + } - if (_tableView is not null) + if (_cellPresenter is null) { - AddCells(_tableView.Columns.VisibleColumns); + _cellPresenter = ContentTemplateRoot as TableViewCellPresenter; + if (_cellPresenter is not null) + { + _cellPresenter.Children.Clear(); + AddCells(TableView.Columns.VisibleColumns); + } + } + } - _tableView.Columns.CollectionChanged += OnColumnsCollectionChanged; - _tableView.Columns.ColumnPropertyChanged += OnColumnPropertyChanged; + private async void OnSizeChanged(object sender, SizeChangedEventArgs e) + { + if (TableView.CurrentCellSlot?.Row == Index) + { + _ = await TableView.ScrollCellIntoView(TableView.CurrentCellSlot.Value); } } @@ -44,9 +61,9 @@ private void OnColumnsCollectionChanged(object? sender, NotifyCollectionChangedE { RemoveCells(oldItems); } - else if (e.Action == NotifyCollectionChangedAction.Reset && _cellsStackPanel is not null) + else if (e.Action == NotifyCollectionChangedAction.Reset && _cellPresenter is not null) { - _cellsStackPanel.Children.Clear(); + _cellPresenter.Children.Clear(); } } @@ -74,14 +91,14 @@ private void OnColumnPropertyChanged(object? sender, TableViewColumnPropertyChan private void RemoveCells(IEnumerable columns) { - if (_cellsStackPanel is not null) + if (_cellPresenter is not null) { foreach (var column in columns) { - var cell = _cellsStackPanel.Children.OfType().FirstOrDefault(x => x.Column == column); + var cell = _cellPresenter.Children.OfType().FirstOrDefault(x => x.Column == column); if (cell is not null) { - _cellsStackPanel.Children.Remove(cell); + _cellPresenter.Children.Remove(cell); } } } @@ -89,73 +106,121 @@ private void RemoveCells(IEnumerable columns) private void AddCells(IEnumerable columns, int index = -1) { - if (_cellsStackPanel is not null) + if (_cellPresenter is not null) { foreach (var column in columns) { - var cell = new TableViewCell { Column = column, Row = this, TableView = _tableView!, IsTabStop = false, Width = column.ActualWidth }; + var cell = new TableViewCell + { + Row = this, + Column = column, + TableView = TableView!, + Index = TableView.Columns.VisibleColumns.IndexOf(column), + Width = column.ActualWidth + }; + if (index < 0) { - _cellsStackPanel.Children.Add(cell); + _cellPresenter.Children.Add(cell); } else { - index = Math.Min(index, _cellsStackPanel.Children.Count); - _cellsStackPanel.Children.Insert(index, cell); + index = Math.Min(index, _cellPresenter.Children.Count); + _cellPresenter.Children.Insert(index, cell); index++; } } } } - internal void SelectNextCell(TableViewCell? currentCell) + private static void OnTableViewChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - _tableView ??= this.FindAscendant(); + if (d is not TableViewRow row) + { + return; + } - if (_tableView is not null) + if (e.NewValue is TableView newTableView && newTableView.Columns is not null) { - var nextCellIndex = currentCell is null ? 0 : Cells.IndexOf(currentCell) + 1; - if (nextCellIndex < Cells.Count) - { - var nextCell = Cells[nextCellIndex]; - if (nextCell.IsReadOnly) - { - SelectNextCell(nextCell); - } - else - { - nextCell.PrepareForEdit(); - } - } - else - { - _tableView.SelectNextRow(); - } + newTableView.Columns.CollectionChanged += row.OnColumnsCollectionChanged; + newTableView.Columns.ColumnPropertyChanged += row.OnColumnPropertyChanged; + newTableView.SelectedCellsChanged += row.OnCellSelectionChanged; + newTableView.CurrentCellChanged += row.OnCurrentCellChanged; + } + + if (e.OldValue is TableView oldTableView && oldTableView.Columns is not null) + { + oldTableView.Columns.CollectionChanged -= row.OnColumnsCollectionChanged; + oldTableView.Columns.ColumnPropertyChanged -= row.OnColumnPropertyChanged; + oldTableView.SelectedCellsChanged -= row.OnCellSelectionChanged; + oldTableView.CurrentCellChanged -= row.OnCurrentCellChanged; } } - internal void SelectPreviousCell(TableViewCell? currentCell) + private static void OnIndexChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { - _tableView ??= this.FindAscendant(); + (d as TableViewRow)?.ApplyCellsSelectionState(); + } - if (_tableView is not null) + private void OnCellSelectionChanged(object? sender, TableViewCellSelectionChangedEvenArgs e) + { + if (e.OldSelection.Any(x => x.Row == Index) || + e.NewSelection.Any(x => x.Row == Index)) { - var previousCellIndex = currentCell is null ? Cells.Count - 1 : Cells.IndexOf(currentCell) - 1; - if (previousCellIndex >= 0) - { - var previousCell = Cells[previousCellIndex]; - if (previousCell.IsReadOnly) - { - SelectPreviousCell(previousCell); - } - previousCell.PrepareForEdit(); - } - else - { - _tableView.SelectPreviousRow(); + ApplyCellsSelectionState(); + } + } + + private void OnCurrentCellChanged(object? sender, TableViewCurrentCellChangedEventArgs e) + { + if (e.OldSlot?.Row == Index) + { + ApplyCurrentCellState(e.OldSlot.Value); + } + + if (e.NewSlot?.Row == Index) + { + ApplyCurrentCellState(e.NewSlot.Value); + } + } + + internal void ApplyCurrentCellState(TableViewCellSlot slot) + { + if (slot.Column >= 0 && slot.Column < Cells.Count) + { + var cell = Cells[slot.Column]; + cell.ApplyCurrentCellState(); } + } + + internal void ApplyCellsSelectionState() + { + foreach (var cell in Cells) + { + cell.ApplySelectionState(); } } - public IList Cells => (_cellsStackPanel?.Children.OfType() ?? Enumerable.Empty()).ToList(); + internal IList Cells => _cellPresenter?.Cells ?? new List(); + + public int Index => (int)GetValue(IndexProperty); + + public TableView TableView + { + get => (TableView)GetValue(TableViewProperty); + set => SetValue(TableViewProperty, value); + } + + public static readonly DependencyProperty IndexProperty = DependencyProperty.Register(nameof(Index), typeof(int), typeof(TableViewRow), new PropertyMetadata(-1, OnIndexChanged)); + public static readonly DependencyProperty TableViewProperty = DependencyProperty.Register(nameof(TableView), typeof(TableView), typeof(TableViewRow), new PropertyMetadata(default, OnTableViewChanged)); } + +public class TableViewCellPresenter : StackPanel +{ + public TableViewCellPresenter() + { + Orientation = Orientation.Horizontal; + } + + public IList Cells => Children.OfType().ToList().AsReadOnly(); +} \ No newline at end of file diff --git a/src/WinUI.TableView/TableViewSelectionUnit.cs b/src/WinUI.TableView/TableViewSelectionUnit.cs new file mode 100644 index 0000000..d738ecf --- /dev/null +++ b/src/WinUI.TableView/TableViewSelectionUnit.cs @@ -0,0 +1,8 @@ +namespace WinUI.TableView; + +public enum TableViewSelectionUnit +{ + CellOrRow, + Cell, + Row, +} \ No newline at end of file diff --git a/src/WinUI.TableView/Themes/TableView.xaml b/src/WinUI.TableView/Themes/TableView.xaml index f36c98f..41551ce 100644 --- a/src/WinUI.TableView/Themes/TableView.xaml +++ b/src/WinUI.TableView/Themes/TableView.xaml @@ -3,10 +3,10 @@ xmlns:local="using:WinUI.TableView" xmlns:interactivity="using:Microsoft.Xaml.Interactivity" xmlns:behaviors="using:CommunityToolkit.WinUI.Behaviors"> - + - + - +