diff --git a/Terminal.Gui/Views/ListView/ListView.Movement.cs b/Terminal.Gui/Views/ListView/ListView.Movement.cs index ada247c17d..516cf40980 100644 --- a/Terminal.Gui/Views/ListView/ListView.Movement.cs +++ b/Terminal.Gui/Views/ListView/ListView.Movement.cs @@ -9,6 +9,13 @@ public partial class ListView /// clears any existing multi-selection. /// /// if the selection was moved. + /// + /// + /// When the selection is already at the last item and is + /// , the selection wraps around to the first item. + /// Otherwise the method returns without changing the selection. + /// + /// public bool MoveDown (bool extend = false) { if (Source is null || Source.Count == 0) @@ -30,13 +37,18 @@ public bool MoveDown (bool extend = false) // Can move down by one. newItem = SelectedItem.Value + 1; } - else if (SelectedItem >= Viewport.Y + Viewport.Height) + else if (Viewport.Height > 0 && SelectedItem >= Viewport.Y + Viewport.Height) { // Just scroll viewport Viewport = Viewport with { Y = Source.Count - Viewport.Height }; return true; } + else if (TabStop == TabBehavior.NoStop) + { + // Wrap to top + newItem = 0; + } else { // Already at bottom @@ -189,6 +201,13 @@ public bool MovePageUp (bool extend = false) /// clears any existing multi-selection. /// /// if the selection was moved. + /// + /// + /// When the selection is already at the first item and is + /// , the selection wraps around to the last item. + /// Otherwise the method returns without changing the selection. + /// + /// public bool MoveUp (bool extend = false) { if (Source is null || Source.Count == 0) @@ -221,6 +240,11 @@ public bool MoveUp (bool extend = false) return true; } + else if (TabStop == TabBehavior.NoStop) + { + // Wrap to bottom + newItem = Source.Count - 1; + } else { // Already at top diff --git a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs index f3c09fa7b1..b35445e1e5 100644 --- a/Tests/UnitTestsParallelizable/Views/ListViewTests.cs +++ b/Tests/UnitTestsParallelizable/Views/ListViewTests.cs @@ -1076,14 +1076,14 @@ public void ShowMarks_True_SpaceWithShift_RadioButton_MarksAndMovesDown () // Press Space+Shift again - cannot move down further // In radio button mode, item should toggle: marked → unmarked - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift)); Assert.Equal (2, lv.SelectedItem); // Still at item 2 Assert.False (lv.Source.IsMarked (0)); Assert.False (lv.Source.IsMarked (1)); Assert.False (lv.Source.IsMarked (2)); // Toggled off // Press key combo again - should toggle back to marked - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift)); Assert.Equal (2, lv.SelectedItem); // Still at item 2 Assert.False (lv.Source.IsMarked (0)); Assert.False (lv.Source.IsMarked (1)); @@ -1137,14 +1137,14 @@ public void ShowMarks_True_SpaceWithShift_SelectsThenDown_MultipleSelection () Assert.False (lv.Source.IsMarked (2)); // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift)); Assert.Equal (2, lv.SelectedItem); // cannot move down any further Assert.True (lv.Source.IsMarked (0)); Assert.True (lv.Source.IsMarked (1)); Assert.True (lv.Source.IsMarked (2)); // but can toggle marked // Press key combo again - Assert.True (lv.NewKeyDownEvent (Key.Space.WithShift)); + Assert.False (lv.NewKeyDownEvent (Key.Space.WithShift)); Assert.Equal (2, lv.SelectedItem); // cannot move down any further Assert.True (lv.Source.IsMarked (0)); Assert.True (lv.Source.IsMarked (1)); @@ -3129,4 +3129,80 @@ public void RowRender_Event_Receives_Correct_Row_Indices_After_Scroll () } #endregion RowRender RowAttribute Override + + #region Movement Cycling + + // Copilot + [Fact] + public void MoveDown_AtBottom_WhenTabStopIsNoStop_WrapsToTop () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Source = new ListWrapper (source), TabStop = TabBehavior.NoStop }; + lv.SetFocus (); + + // Move to last item + Assert.True (lv.MoveDown ()); // selects 0 + Assert.True (lv.MoveDown ()); // selects 1 + Assert.True (lv.MoveDown ()); // selects 2 + Assert.Equal (2, lv.SelectedItem); + + // Should wrap to top + Assert.True (lv.MoveDown ()); + Assert.Equal (0, lv.SelectedItem); + } + + // Copilot + [Fact] + public void MoveDown_AtBottom_WhenTabStopIsNotNoStop_ReturnsFalse () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Source = new ListWrapper (source), TabStop = TabBehavior.TabStop }; + lv.SetFocus (); + + // Move to last item + Assert.True (lv.MoveDown ()); // selects 0 + Assert.True (lv.MoveDown ()); // selects 1 + Assert.True (lv.MoveDown ()); // selects 2 + Assert.Equal (2, lv.SelectedItem); + + // Should NOT wrap — returns false + Assert.False (lv.MoveDown ()); + Assert.Equal (2, lv.SelectedItem); + } + + // Copilot + [Fact] + public void MoveUp_AtTop_WhenTabStopIsNoStop_WrapsToBottom () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Frame = new Rectangle (0, 0, 10, 20), Source = new ListWrapper (source), TabStop = TabBehavior.NoStop }; + lv.SetFocus (); + + // Select first item + Assert.True (lv.MoveDown ()); + Assert.Equal (0, lv.SelectedItem); + + // Should wrap to bottom + Assert.True (lv.MoveUp ()); + Assert.Equal (2, lv.SelectedItem); + } + + // Copilot + [Fact] + public void MoveUp_AtTop_WhenTabStopIsNotNoStop_ReturnsFalse () + { + ObservableCollection source = ["one", "two", "three"]; + ListView lv = new () { Frame = new Rectangle (0, 0, 10, 20), Source = new ListWrapper (source), TabStop = TabBehavior.TabStop }; + lv.SetFocus (); + + // Select first item + Assert.True (lv.MoveDown ()); + Assert.Equal (0, lv.SelectedItem); + + // Should NOT wrap — returns false + Assert.False (lv.MoveUp ()); + Assert.Equal (0, lv.SelectedItem); + } + + #endregion Movement Cycling }