@@ -50,6 +50,7 @@ public partial class TableView : ListView
5050 private TableViewCellSlotRange ? _lastDragSelectionCellRange ;
5151 private ItemIndexRange ? _lastDragSelectionRowRange ;
5252 private bool _cellStateDispatchPending ;
53+ private ( TableViewCellSlot Slot , TaskCompletionSource < bool > Completion ) ? _pendingEdit ;
5354 private readonly HashSet < int > _pendingCellStateRows = [ ] ;
5455 private TableViewColumn ? _resizingColumn ;
5556 private double _resizingOriginalWidth ;
@@ -946,56 +947,13 @@ private void HandleNavigations(KeyRoutedEventArgs e, bool shiftKey, bool ctrlKey
946947 // Handle navigation keys
947948 else if ( e . Key is VirtualKey . Tab or VirtualKey . Enter )
948949 {
949- var isEditing = IsEditing ;
950-
951- var newSlot = CurrentCellSlot ?? new ( ) ;
952-
953- do
954- {
955- newSlot = GetNextSlot ( newSlot , shiftKey , e . Key is VirtualKey . Enter ) ;
956-
957- } while ( isEditing && Columns [ newSlot . Column ] . IsReadOnly ) ;
958-
959- if ( isEditing && currentCell is not null )
960- {
961- if ( ! EndCellEditing ( TableViewEditAction . Commit , currentCell ) ) return ;
962-
963- if ( CurrentCellSlot == newSlot || GetCellFromSlot ( newSlot ) is not { } nextCell || ! nextCell . BeginCellEditing ( e ) )
964- {
965- SetIsEditing ( false ) ;
966- }
967- }
968-
969- MakeSelection ( newSlot , false ) ;
970-
971- e . Handled = true ;
950+ // A cancelled commit leaves the key unhandled, as before the move was factored out.
951+ e . Handled = HandleTabOrEnter ( e . Key , shiftKey , currentCell , e ) ;
972952 }
973953 else if ( ( e . Key is VirtualKey . Left or VirtualKey . Right or VirtualKey . Up or VirtualKey . Down )
974954 && ! IsEditing )
975955 {
976- var row = ( LastSelectionUnit is TableViewSelectionUnit . Row ? CurrentRowIndex : CurrentCellSlot ? . Row ) ?? - 1 ;
977- var column = CurrentCellSlot ? . Column ?? - 1 ;
978-
979- if ( row == - 1 && column == - 1 )
980- {
981- row = column = 0 ;
982- }
983- else if ( e . Key is VirtualKey . Left or VirtualKey . Right )
984- {
985- column = e . Key is VirtualKey . Left ? ctrlKey ? 0 : column - 1 : ctrlKey ? Columns . VisibleColumns . Count - 1 : column + 1 ;
986- if ( column >= Columns . VisibleColumns . Count )
987- {
988- column = 0 ;
989- row ++ ;
990- }
991- }
992- else
993- {
994- row = e . Key == VirtualKey . Up ? ctrlKey ? 0 : row - 1 : ctrlKey ? Items . Count - 1 : row + 1 ;
995- }
996-
997- var newSlot = new TableViewCellSlot ( row , column ) ;
998- MakeSelection ( newSlot , shiftKey ) ;
956+ MakeSelection ( GetArrowSlot ( e . Key , ctrlKey ) , shiftKey ) ;
999957 e . Handled = true ;
1000958 }
1001959 else if ( e . Key is VirtualKey . Home or VirtualKey . End )
@@ -1034,6 +992,184 @@ private int CalculateAvailablePageSize()
1034992 return ( int ) Math . Floor ( availableHeight / rowHeight ) ;
1035993 }
1036994
995+ /// <summary>
996+ /// Moves the current cell as Tab or Enter does. While editing, the edit is committed first; the cell
997+ /// moved to is then edited too unless <see cref="ContinueEditingOnNavigation"/> is <see langword="false"/>.
998+ /// </summary>
999+ /// <returns><see langword="false"/> if committing the edit was cancelled, so nothing moved.</returns>
1000+ internal bool HandleTabOrEnter ( VirtualKey key , bool shiftKey , TableViewCell ? currentCell , RoutedEventArgs editingArgs )
1001+ {
1002+ var isEditing = IsEditing ;
1003+
1004+ var newSlot = CurrentCellSlot ?? new ( ) ;
1005+
1006+ do
1007+ {
1008+ newSlot = GetNextSlot ( newSlot , shiftKey , key is VirtualKey . Enter && EnterKeyNavigation is TableViewEnterKeyNavigation . Down ) ;
1009+
1010+ } while ( isEditing && Columns [ newSlot . Column ] . IsReadOnly ) ;
1011+
1012+ if ( isEditing && currentCell is not null )
1013+ {
1014+ if ( ! EndCellEditing ( TableViewEditAction . Commit , currentCell ) ) return false ;
1015+
1016+ if ( ! ContinueEditingOnNavigation || CurrentCellSlot == newSlot || GetCellFromSlot ( newSlot ) is not { } nextCell || ! nextCell . BeginCellEditing ( editingArgs ) )
1017+ {
1018+ SetIsEditing ( false ) ;
1019+ }
1020+ }
1021+
1022+ MakeSelection ( newSlot , false ) ;
1023+ return true ;
1024+ }
1025+
1026+ /// <summary>
1027+ /// Commits the edit in progress, if any, and ends the edit session, leaving the edited cell current.
1028+ /// </summary>
1029+ /// <remarks>
1030+ /// This is what Tab or Enter do to the cell being left, without moving. <see cref="CellEditEnding"/> is
1031+ /// raised, and a handler that cancels it keeps the edit open.
1032+ /// </remarks>
1033+ /// <returns><see langword="true"/> if no edit is in progress any more; <see langword="false"/> if the
1034+ /// commit was cancelled.</returns>
1035+ public bool CommitEdit ( )
1036+ {
1037+ if ( ! IsEditing )
1038+ {
1039+ return true ;
1040+ }
1041+
1042+ if ( CurrentCellSlot is { } slot && GetCellFromSlot ( slot ) is { } cell )
1043+ {
1044+ // Move focus to the cell BEFORE the editing element is torn down, as the Escape path does.
1045+ cell . Focus ( FocusState . Programmatic ) ;
1046+
1047+ if ( ! EndCellEditing ( TableViewEditAction . Commit , cell ) )
1048+ {
1049+ return false ;
1050+ }
1051+ }
1052+
1053+ SetIsEditing ( false ) ;
1054+ return true ;
1055+ }
1056+
1057+ /// <summary>
1058+ /// Moves the current cell as the given navigation key would, when no edit is in progress.
1059+ /// </summary>
1060+ /// <remarks>
1061+ /// Supports the arrow keys, Tab and Enter (honouring <see cref="EnterKeyNavigation"/>). A caller that
1062+ /// handles a key inside a cell editor - for example an arrow key that should commit the edit and move,
1063+ /// as in a spreadsheet entry mode - calls <see cref="CommitEdit"/> and then this.
1064+ /// </remarks>
1065+ /// <returns><see langword="true"/> if the key was a supported navigation key and no edit is in progress.</returns>
1066+ public bool NavigateFromCurrentCell ( VirtualKey key , bool shiftKey = false , bool ctrlKey = false )
1067+ {
1068+ if ( IsEditing )
1069+ {
1070+ return false ;
1071+ }
1072+
1073+ if ( key is VirtualKey . Left or VirtualKey . Right or VirtualKey . Up or VirtualKey . Down )
1074+ {
1075+ MakeSelection ( GetArrowSlot ( key , ctrlKey ) , shiftKey ) ;
1076+ return true ;
1077+ }
1078+
1079+ if ( key is VirtualKey . Tab or VirtualKey . Enter )
1080+ {
1081+ MakeSelection ( GetNextSlot ( CurrentCellSlot ?? new ( ) , shiftKey , key is VirtualKey . Enter && EnterKeyNavigation is TableViewEnterKeyNavigation . Down ) , false ) ;
1082+ return true ;
1083+ }
1084+
1085+ return false ;
1086+ }
1087+
1088+ /// <summary>
1089+ /// Makes the cell at <paramref name="slot"/> the current cell and begins editing it, as pressing F2 on
1090+ /// that cell does.
1091+ /// </summary>
1092+ /// <remarks>
1093+ /// <para>This is for a cell that exposes an affordance of its own while it is not being edited - a
1094+ /// drop-down arrow, a picker button - and needs a single click on that affordance to open the editor.
1095+ /// The control starts an edit only from a double tap and from F2, and a single click is not a gesture it
1096+ /// can infer.</para>
1097+ /// <para>The sequence matches the existing gestures:</para>
1098+ /// <list type="number">
1099+ /// <item>If another cell is being edited, that edit is committed first, as tapping or tabbing to a
1100+ /// different cell does. If committing is cancelled, no edit begins.</item>
1101+ /// <item>The cell becomes the <see cref="CurrentCellSlot"/> and is selected, as keyboard navigation to
1102+ /// it does, and is scrolled into view and focused.</item>
1103+ /// <item>The edit then begins. <see cref="BeginningEdit"/> is raised, and a handler that cancels it is
1104+ /// honoured.</item>
1105+ /// </list>
1106+ /// <para>No edit begins for a read-only table, column or cell, for a column that draws itself through
1107+ /// <see cref="TableViewColumn.UseSingleElement"/> (a double tap does not begin one either), or when the
1108+ /// cell is already being edited.</para>
1109+ /// </remarks>
1110+ /// <param name="slot">The cell to edit.</param>
1111+ /// <returns>A task whose result is <see langword="true"/> if the cell entered edit mode; otherwise
1112+ /// <see langword="false"/>.</returns>
1113+ public async Task < bool > BeginEditAsync ( TableViewCellSlot slot )
1114+ {
1115+ if ( IsReadOnly || ! slot . IsValid ( this ) )
1116+ {
1117+ return false ;
1118+ }
1119+
1120+ if ( IsEditing )
1121+ {
1122+ if ( CurrentCellSlot == slot )
1123+ {
1124+ return false ;
1125+ }
1126+
1127+ if ( CurrentCellSlot is { } currentSlot
1128+ && GetCellFromSlot ( currentSlot ) is { } currentCell
1129+ && ! EndCellEditing ( TableViewEditAction . Commit , currentCell ) )
1130+ {
1131+ return false ;
1132+ }
1133+
1134+ SetIsEditing ( false ) ;
1135+ }
1136+
1137+ // A later request supersedes an earlier one that has not started yet.
1138+ _pendingEdit ? . Completion . TrySetResult ( false ) ;
1139+ _pendingEdit = null ;
1140+
1141+ if ( CurrentCellSlot == slot )
1142+ {
1143+ // Already current, so OnCurrentCellChanged will not run. A realized cell is edited directly,
1144+ // as F2 does, so the call completes synchronously; only a cell scrolled out of view waits.
1145+ return TryBeginEdit ( GetCellFromSlot ( slot ) ?? await ScrollCellIntoView ( slot ) ) ;
1146+ }
1147+
1148+ var pending = new TaskCompletionSource < bool > ( ) ;
1149+ _pendingEdit = ( slot , pending ) ;
1150+
1151+ MakeSelection ( slot , false ) ;
1152+
1153+ if ( CurrentCellSlot != slot )
1154+ {
1155+ // Selection declined to make the cell current, so OnCurrentCellChanged will not start the edit.
1156+ _pendingEdit = null ;
1157+ return false ;
1158+ }
1159+
1160+ return await pending . Task ;
1161+ }
1162+
1163+ /// <summary>
1164+ /// Begins editing <paramref name="cell"/> if the same conditions a double tap applies allow it.
1165+ /// </summary>
1166+ private bool TryBeginEdit ( TableViewCell ? cell )
1167+ {
1168+ return cell is { IsReadOnly : false , Column . UseSingleElement : false }
1169+ && ! IsEditing
1170+ && cell . BeginCellEditing ( new RoutedEventArgs ( ) ) ;
1171+ }
1172+
10371173 /// <summary>
10381174 /// Ends the editing of a cell, committing or canceling the edit based on the specified action.
10391175 /// </summary>
@@ -1256,6 +1392,35 @@ private void OnScrollContentPresenterPointerWheelChanged(object sender, PointerR
12561392 }
12571393 }
12581394
1395+ /// <summary>
1396+ /// Gets the slot an arrow key moves the current cell to.
1397+ /// </summary>
1398+ private TableViewCellSlot GetArrowSlot ( VirtualKey key , bool ctrlKey )
1399+ {
1400+ var row = ( LastSelectionUnit is TableViewSelectionUnit . Row ? CurrentRowIndex : CurrentCellSlot ? . Row ) ?? - 1 ;
1401+ var column = CurrentCellSlot ? . Column ?? - 1 ;
1402+
1403+ if ( row == - 1 && column == - 1 )
1404+ {
1405+ row = column = 0 ;
1406+ }
1407+ else if ( key is VirtualKey . Left or VirtualKey . Right )
1408+ {
1409+ column = key is VirtualKey . Left ? ctrlKey ? 0 : column - 1 : ctrlKey ? Columns . VisibleColumns . Count - 1 : column + 1 ;
1410+ if ( column >= Columns . VisibleColumns . Count )
1411+ {
1412+ column = 0 ;
1413+ row ++ ;
1414+ }
1415+ }
1416+ else
1417+ {
1418+ row = key == VirtualKey . Up ? ctrlKey ? 0 : row - 1 : ctrlKey ? Items . Count - 1 : row + 1 ;
1419+ }
1420+
1421+ return new TableViewCellSlot ( row , column ) ;
1422+ }
1423+
12591424 /// <summary>
12601425 /// Gets the next cell slot based on the current slot and input keys.
12611426 /// </summary>
@@ -2181,6 +2346,15 @@ private async Task OnCurrentCellChanged(TableViewCellSlot? oldSlot, TableViewCel
21812346 var cell = await ScrollCellIntoView ( newSlot . Value ) ;
21822347 cell ? . ApplyCurrentCellState ( ) ;
21832348 cell ? . Focus ( FocusState . Programmatic ) ;
2349+
2350+ // A BeginEditAsync call that moved the current cell starts its edit here, once the cell has
2351+ // been scrolled into view and focused, so that the editing element is the last thing to take
2352+ // focus rather than racing the Focus call above.
2353+ if ( _pendingEdit is { } pending && pending . Slot == newSlot . Value )
2354+ {
2355+ _pendingEdit = null ;
2356+ pending . Completion . TrySetResult ( CurrentCellSlot == newSlot && TryBeginEdit ( cell ) ) ;
2357+ }
21842358 }
21852359 }
21862360
0 commit comments