diff --git a/src/TableView.Properties.cs b/src/TableView.Properties.cs index 80ac94b0..f5688f14 100644 --- a/src/TableView.Properties.cs +++ b/src/TableView.Properties.cs @@ -310,6 +310,11 @@ public bool CanPaste /// public static readonly DependencyProperty ShowDragRectangleProperty = DependencyProperty.Register(nameof(ShowDragRectangle), typeof(bool), typeof(TableView), new PropertyMetadata(true, OnShowDragRectangleChanged)); + /// + /// Identifies the 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. /// @@ -869,6 +874,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 698c482d..abfa845a 100644 --- a/src/TableView.cs +++ b/src/TableView.cs @@ -170,6 +170,16 @@ protected override DependencyObject GetContainerForItemOverride() return row; } + /// + /// Gets a value indicating whether keyboard input should be handled in row context, + /// i.e. when is enabled and the effective selection unit is a row. + /// + private bool IsRowKeyboardContext => + UseListViewHotkeys && + (SelectionUnit == TableViewSelectionUnit.Row || + (SelectionUnit == TableViewSelectionUnit.CellOrRow && + LastSelectionUnit == TableViewSelectionUnit.Row)); + /// protected override void OnKeyDown(KeyRoutedEventArgs e) { @@ -182,9 +192,92 @@ protected override 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.) 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. /// @@ -533,6 +626,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. ///