From ac38fedf4acd285ba2e94c498940a96c8946036d Mon Sep 17 00:00:00 2001 From: "hephaestusdailylog@gmail.com" Date: Fri, 21 Nov 2025 11:06:00 +1100 Subject: [PATCH] Add ListView-like hotkeys support to TableView Introduced a new `UseListViewHotkeys` dependency property to the `TableView` class, allowing users to enable ListView-like hotkeys for navigation and selection. This feature is opt-in and defaults to `false` to ensure backward compatibility. Enhanced the `OnKeyDown` method to support additional keyboard navigation and selection scenarios when `UseListViewHotkeys` is enabled. Added support for toggling row selection with `Enter` and improved navigation using keys like `Up`, `Down`, `Home`, `End`, `PageUp`, and `PageDown`. Added helper methods `ToggleCurrentRowSelection`, `GetFocusedRowIndex`, and `GetRowFromElement` to manage row selection and focus. Updated `using` directives to include `Microsoft.UI.Xaml.Media` for visual tree traversal. --- src/TableView.Properties.cs | 14 +++++ src/TableView.cs | 120 ++++++++++++++++++++++++++++++++++++ 2 files changed, 134 insertions(+) diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs index 8ec2803..e04d25e 100644 --- a/src/TableView.Properties.cs +++ b/src/TableView.Properties.cs @@ -254,6 +254,11 @@ public partial class TableView /// Identifies the CanReorderColumns dependency property. /// public static readonly DependencyProperty CanReorderColumnsProperty = DependencyProperty.Register(nameof(CanReorderColumns), typeof(bool), typeof(TableView), new PropertyMetadata(true)); + + /// + /// Identifies the UseListViewHotkeys dependency property. + /// + public static readonly DependencyProperty UseListViewHotkeysProperty = DependencyProperty.Register(nameof(UseListViewHotkeys), typeof(bool), typeof(TableView), new PropertyMetadata(false)); /// /// Gets or sets a value indicating whether opening the column filter over header right-click is enabled. /// @@ -753,6 +758,15 @@ public bool CanReorderColumns set => SetValue(CanReorderColumnsProperty, value); } + /// + /// Gets or sets whether the TableView should use ListView like hotkeys for navigation and selection. + /// + public bool UseListViewHotkeys + { + get => (bool)GetValue(UseListViewHotkeysProperty); + set => SetValue(UseListViewHotkeysProperty, value); + } + /// /// Handles changes to the ItemsSource property. /// diff --git a/src/TableView.cs b/src/TableView.cs index a61aa58..72d85dd 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -5,6 +5,7 @@ using Microsoft.UI.Xaml.Controls.Primitives; using Microsoft.UI.Xaml.Data; using Microsoft.UI.Xaml.Input; +using Microsoft.UI.Xaml.Media; using System; using System.Collections; using System.Collections.Generic; @@ -112,6 +113,12 @@ protected override DependencyObject GetContainerForItemOverride() return row; } + private bool IsRowKeyboardContext => + (UseListViewHotkeys == true) && //Not sure if I'm solving a bug or adding a feature, so I've made it apply only when the UseListViewHotkeys is set to true just in case. + (SelectionUnit == TableViewSelectionUnit.Row || + (SelectionUnit == TableViewSelectionUnit.CellOrRow && + LastSelectionUnit == TableViewSelectionUnit.Row)); + /// protected override async void OnKeyDown(KeyRoutedEventArgs e) { @@ -124,9 +131,94 @@ protected override async void OnKeyDown(KeyRoutedEventArgs e) return; } + if (!IsEditing && IsRowKeyboardContext) + { + if (!shiftKey && + !ctrlKey && + SelectionMode == ListViewSelectionMode.Multiple && + e.Key == VirtualKey.Enter) + { + ToggleCurrentRowSelection(); + e.Handled = true; + return; + } + + var rowSelectionOnly = SelectionUnit == TableViewSelectionUnit.Row; + + if (e.Key is VirtualKey.Up or VirtualKey.Down or + VirtualKey.Home or VirtualKey.End or + VirtualKey.PageUp or VirtualKey.PageDown || + (rowSelectionOnly && (e.Key is VirtualKey.Left or VirtualKey.Right))) + { + int? prevCellRow = null; + if (SelectionUnit == TableViewSelectionUnit.Row && CurrentCellSlot.HasValue) + { + prevCellRow = CurrentCellSlot.Value.Row; + } + + base.OnKeyDown(e); // Let ListView move the row focus / selection + + var focusedIndex = GetFocusedRowIndex(); + if (focusedIndex >= 0) + { + CurrentRowIndex = focusedIndex; + SelectionStartRowIndex ??= focusedIndex; + } + + if (SelectionUnit == TableViewSelectionUnit.Row && + prevCellRow.HasValue && + focusedIndex >= 0 && + focusedIndex != prevCellRow.Value) + { + CurrentCellSlot = null; // will un-apply old cell's current-state border + } + return; + } + } + // Everything else (cell nav, F2 in cell mode, Space, etc.) await HandleNavigations(e, shiftKey, ctrlKey); } + + + private void ToggleCurrentRowSelection() + { + + var index = GetFocusedRowIndex(); + + if (index < 0) + { + var rowIndex = CurrentRowIndex ?? SelectedIndex; + + if (rowIndex is < 0 || rowIndex >= Items.Count) + { + return; + } + + index = rowIndex; + } + + + if (index < 0 || index >= Items.Count) { return; } + + var isSelected = SelectedRanges.Any(r => r.IsInRange(index)); + + var singleIndexRange = new ItemIndexRange(index, 1u); + + if (isSelected) + { + DeselectRange(singleIndexRange); + } + else + { + SelectRange(singleIndexRange); + } + + SelectionStartRowIndex = index; + CurrentRowIndex = index; + + } + /// /// Handles navigation keys. /// @@ -367,6 +459,34 @@ private TableViewCellSlot GetNextSlot(TableViewCellSlot? currentSlot, bool isShi return new TableViewCellSlot(nextRow, nextColumn); } + private int GetFocusedRowIndex() + { + if (XamlRoot is null) + return -1; + + var focused = FocusManager.GetFocusedElement(XamlRoot) as DependencyObject; + if (focused is null) + return -1; + + var row = GetRowFromElement(focused); + return row?.Index ?? -1; + } + + private static TableViewRow? GetRowFromElement(DependencyObject element) + { + var current = element; + + while (current is not null) + { + if (current is TableViewRow row) + return row; + + current = VisualTreeHelper.GetParent(current); + } + + return null; + } + /// /// Copies the selected rows or cells content to the clipboard. ///