Merge branch 'v2_develop' into Terminal.Gui-pressed-mousebinding-4674

This commit is contained in:
Tig
2026-02-04 16:28:09 -07:00
committed by GitHub
4 changed files with 610 additions and 110 deletions

View File

@@ -11,7 +11,7 @@ namespace Terminal.Gui.Views;
// HotKey - Do NOT Restore Focus. Advance Active. Do NOT Accept.
// Item HotKey - Do NOT Focus item. If item is not active, make Active. Do NOT Accept.
// Focused:
// Space key - If focused item is Active, move focus to and Acivate next. Else, Activate current. Do NOT Accept.
// Space key - If focused item is Active, move focus to and Activate next. Else, Activate current. Do NOT Accept.
// Enter key - Activate and Accept the focused item.
// HotKey - Restore Focus. Advance Active. Do NOT Accept.
// Item HotKey - If item is not active, make Active. Do NOT Accept.
@@ -34,7 +34,7 @@ public class OptionSelector : SelectorBase, IDesignable
/// <inheritdoc/>
protected override bool OnHandlingHotKey (CommandEventArgs args)
{
if (base.OnHandlingHotKey (args) is true)
if (base.OnHandlingHotKey (args))
{
return true;
}
@@ -65,7 +65,7 @@ public class OptionSelector : SelectorBase, IDesignable
/// <inheritdoc/>
protected override bool OnActivating (CommandEventArgs args)
{
if (base.OnActivating (args) is true)
if (base.OnActivating (args))
{
return true;
}
@@ -134,17 +134,20 @@ public class OptionSelector : SelectorBase, IDesignable
return;
}
if (args.Context?.Binding is KeyBinding && args.Context.Command == Command.HotKey && checkbox.Value == CheckState.Checked)
if (args.Context is { Binding: KeyBinding, Command: Command.HotKey })
{
// If user uses an item hotkey and the item is already checked, do nothing
args.Handled = true;
if (checkbox.Value == CheckState.Checked)
{
// If user uses an item hotkey and the item is already checked, do nothing
args.Handled = true;
return;
return;
}
}
if (checkbox.CanFocus)
{
// For Select, if the view is focusable and SetFocus succeeds, by defition,
// For Select, if the view is focusable and SetFocus succeeds, by definition,
// the event is handled. So return what SetFocus returns.
checkbox.SetFocus ();
}
@@ -176,7 +179,7 @@ public class OptionSelector : SelectorBase, IDesignable
{
int valueIndex = Values.IndexOf (v => v == Value);
Value = valueIndex == Values?.Count () - 1 ? Values! [0] : Values! [valueIndex + 1];
Value = valueIndex == Values?.Count - 1 ? Values! [0] : Values! [valueIndex + 1];
if (HasFocus)
{
@@ -208,7 +211,8 @@ public class OptionSelector : SelectorBase, IDesignable
}
/// <summary>
/// Gets or sets the <see cref="SelectorBase.Labels"/> index for the cursor. The cursor may or may not be the selected
/// Gets or sets the <see cref="SelectorBase.Labels"/> index for the focused item. The active item may or may not be
/// the selected
/// RadioItem.
/// </summary>
/// <remarks>
@@ -216,9 +220,22 @@ public class OptionSelector : SelectorBase, IDesignable
/// Maps to either the X or Y position within <see cref="View.Viewport"/> depending on <see cref="Orientation"/>.
/// </para>
/// </remarks>
public new int Cursor
public int FocusedItem
{
get => !CanFocus ? 0 : SubViews.OfType<CheckBox> ().ToArray ().IndexOf (Focused);
get
{
if (!CanFocus)
{
return 0;
}
if (HasFocus)
{
return SubViews.OfType<CheckBox> ().ToArray ().IndexOf (Focused);
}
return field;
}
set
{
if (!CanFocus)
@@ -226,14 +243,19 @@ public class OptionSelector : SelectorBase, IDesignable
return;
}
field = value;
CheckBox [] checkBoxes = SubViews.OfType<CheckBox> ().ToArray ();
if (value < 0 || value >= checkBoxes.Length)
{
throw new ArgumentOutOfRangeException (nameof (value), @"Cursor index is out of range");
throw new ArgumentOutOfRangeException (nameof (value), @"FocusedItem index is out of range");
}
checkBoxes [value].SetFocus ();
if (HasFocus)
{
checkBoxes [value].SetFocus ();
}
}
}

View File

@@ -30,6 +30,84 @@ public abstract class SelectorBase : View, IOrientation, IValue<int?>
AddCommand (Command.Accept, HandleAcceptCommand);
MouseBindings.Remove (MouseFlags.LeftButtonClicked);
KeyBindings.ReplaceCommands (Key.CursorDown, Command.Down);
KeyBindings.ReplaceCommands (Key.CursorRight, Command.Right);
KeyBindings.ReplaceCommands (Key.CursorUp, Command.Up);
KeyBindings.ReplaceCommands (Key.CursorLeft, Command.Left);
AddCommand (Command.Down, () => MoveNext (Command.Down));
AddCommand (Command.Right, () => MoveNext (Command.Right));
AddCommand (Command.Up, () => MovePrevious (Command.Up));
AddCommand (Command.Left, () => MovePrevious (Command.Left));
}
private bool MoveNext (Command command)
{
if ((command == Command.Down && Orientation == Orientation.Horizontal) || (command == Command.Right && Orientation == Orientation.Vertical))
{
return false;
}
int active = SubViews.OfType<CheckBox> ().ToArray ().IndexOf (Focused);
if (active < SubViews.OfType<CheckBox> ().Count () - 1)
{
active++;
}
else
{
if (Styles.HasFlag (SelectorStyles.ShowValue))
{
_valueField?.SetFocus ();
return true;
}
active = 0;
}
SubViews.OfType<CheckBox> ().ToArray ().ElementAt (active).SetFocus ();
return true;
}
private bool MovePrevious (Command command)
{
if ((command == Command.Up && Orientation == Orientation.Horizontal) || (command == Command.Left && Orientation == Orientation.Vertical))
{
return false;
}
int active = SubViews.OfType<CheckBox> ().ToArray ().IndexOf (Focused);
switch (active)
{
case -1 when Styles.HasFlag (SelectorStyles.ShowValue):
active = SubViews.OfType<CheckBox> ().Count () - 1;
break;
case > 0:
active--;
break;
default:
{
if (Styles.HasFlag (SelectorStyles.ShowValue))
{
_valueField?.SetFocus ();
return true;
}
active = SubViews.OfType<CheckBox> ().Count () - 1;
break;
}
}
SubViews.OfType<CheckBox> ().ToArray ().ElementAt (active).SetFocus ();
return true;
}
/// <summary>
@@ -54,9 +132,7 @@ public abstract class SelectorBase : View, IOrientation, IValue<int?>
private bool? HandleAcceptCommand (ICommandContext? ctx)
{
if (!DoubleClickAccepts
&& ctx?.Binding is MouseBinding mouseBinding
&& mouseBinding.MouseEvent!.Flags.HasFlag (MouseFlags.LeftButtonDoubleClicked))
if (!DoubleClickAccepts && ctx?.Binding is MouseBinding mouseBinding && mouseBinding.MouseEvent!.Flags.HasFlag (MouseFlags.LeftButtonDoubleClicked))
{
return false;
}
@@ -259,7 +335,8 @@ public abstract class SelectorBase : View, IOrientation, IValue<int?>
// TODO: Don't hardcode this; base it on max Value
Width = 5,
ReadOnly = true
ReadOnly = true,
TabStop = TabBehavior.NoStop
};
Add (_valueField);
@@ -293,7 +370,8 @@ public abstract class SelectorBase : View, IOrientation, IValue<int?>
Title = label,
Id = label,
Data = value,
MouseHighlightStates = DefaultMouseHighlightStates
MouseHighlightStates = DefaultMouseHighlightStates,
TabStop = TabBehavior.NoStop
};
return checkbox;

View File

@@ -6,7 +6,7 @@ public class OptionSelectorTests
[Fact]
public void Initialization_ShouldSetDefaults ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
Assert.True (optionSelector.CanFocus);
Assert.Equal (Dim.Auto (DimAutoStyle.Content), optionSelector.Width);
@@ -19,7 +19,7 @@ public class OptionSelectorTests
[Fact]
public void Initialization_With_Options_Value_Is_First ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
@@ -32,38 +32,38 @@ public class OptionSelectorTests
[Fact]
public void SetOptions_ShouldCreateCheckBoxes ()
{
var optionSelector = new OptionSelector ();
List<string> options = new () { "Option1", "Option2", "Option3" };
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2", "Option3"];
optionSelector.Labels = options;
Assert.Equal (options, optionSelector.Labels);
Assert.Equal (options.Count, optionSelector.SubViews.OfType<CheckBox> ().Count ());
Assert.Contains (optionSelector.SubViews, sv => sv is CheckBox cb && cb.Title == "Option1");
Assert.Contains (optionSelector.SubViews, sv => sv is CheckBox cb && cb.Title == "Option2");
Assert.Contains (optionSelector.SubViews, sv => sv is CheckBox cb && cb.Title == "Option3");
Assert.Contains (optionSelector.SubViews, sv => sv is CheckBox { Title: "Option1" });
Assert.Contains (optionSelector.SubViews, sv => sv is CheckBox { Title: "Option2" });
Assert.Contains (optionSelector.SubViews, sv => sv is CheckBox { Title: "Option3" });
}
[Fact]
public void Value_Set_ShouldUpdateCheckedState ()
{
var optionSelector = new OptionSelector ();
List<string> options = new () { "Option1", "Option2" };
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
optionSelector.Value = 1;
CheckBox selectedCheckBox = optionSelector.SubViews.OfType<CheckBox> ().First (cb => (int)cb.Data == 1);
CheckBox selectedCheckBox = optionSelector.SubViews.OfType<CheckBox> ().First (cb => (int)cb.Data! == 1);
Assert.Equal (CheckState.Checked, selectedCheckBox.Value);
CheckBox unselectedCheckBox = optionSelector.SubViews.OfType<CheckBox> ().First (cb => (int)cb.Data == 0);
CheckBox unselectedCheckBox = optionSelector.SubViews.OfType<CheckBox> ().First (cb => (int)cb.Data! == 0);
Assert.Equal (CheckState.UnChecked, unselectedCheckBox.Value);
}
[Fact]
public void Value_Set_OutOfRange_ShouldThrow ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
@@ -75,12 +75,12 @@ public class OptionSelectorTests
[Fact]
public void ValueChanged_Event_ShouldBeRaised ()
{
var optionSelector = new OptionSelector ();
List<string> options = new () { "Option1", "Option2" };
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
var eventRaised = false;
optionSelector.ValueChanged += (sender, args) => eventRaised = true;
optionSelector.ValueChanged += (_, _) => eventRaised = true;
optionSelector.Value = 1;
@@ -91,7 +91,7 @@ public class OptionSelectorTests
public void AssignHotKeys_ShouldAssignUniqueHotKeys ()
{
var optionSelector = new OptionSelector { AssignHotKeys = true };
List<string> options = new () { "Option1", "Option2" };
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
@@ -103,8 +103,8 @@ public class OptionSelectorTests
[Fact]
public void Orientation_Set_ShouldUpdateLayout ()
{
var optionSelector = new OptionSelector ();
List<string> options = new () { "Option1", "Option2" };
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
optionSelector.Orientation = Orientation.Horizontal;
@@ -138,7 +138,7 @@ public class OptionSelectorTests
[Fact]
public void Accept_Command_Fires_Accept ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
optionSelector.Labels = new List<string> { "Option1", "Option2" };
var accepted = false;
@@ -155,7 +155,7 @@ public class OptionSelectorTests
[Fact]
public void LeftButtonClicked_On_Activated_Does_Nothing ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
@@ -209,7 +209,7 @@ public class OptionSelectorTests
[Fact]
public void Key_Space_On_Activated_Cycles ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
@@ -229,7 +229,7 @@ public class OptionSelectorTests
[Fact]
public void Key_Space_On_NotActivated_Activates ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2"];
optionSelector.Labels = options;
@@ -250,7 +250,7 @@ public class OptionSelectorTests
[Fact]
public void Values_ShouldUseOptions_WhenValuesIsNull ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
Assert.Null (optionSelector.Values); // Initially null
List<string> options = ["Option1", "Option2", "Option3"];
@@ -268,8 +268,8 @@ public class OptionSelectorTests
{
// Arrange
OptionSelector optionSelector = new ();
List<string> options = new () { "Option _1", "Option _2", "Option _3" };
List<int> values = new () { 0, 1, 5 };
List<string> options = ["Option _1", "Option _2", "Option _3"];
List<int> values = [0, 1, 5];
optionSelector.Labels = options;
optionSelector.Values = values;
@@ -283,7 +283,7 @@ public class OptionSelectorTests
Assert.Equal (5, optionSelector.Value);
// Verify that the CheckBox states align with the non-sequential Values
CheckBox selectedCheckBox = optionSelector.SubViews.OfType<CheckBox> ().First (cb => (int)cb.Data == 5);
CheckBox selectedCheckBox = optionSelector.SubViews.OfType<CheckBox> ().First (cb => (int)cb.Data! == 5);
Assert.Equal (CheckState.Checked, selectedCheckBox.Value);
CheckBox unselectedCheckBox = optionSelector.SubViews.OfType<CheckBox> ().First (cb => (int)cb.Data! == 0); // Index 0 corresponds to value 0
@@ -312,9 +312,9 @@ public class OptionSelectorTests
}
[Fact]
public void Cursor_Get_ReturnsCorrectIndex ()
public void FocusedItem_Get_ReturnsCorrectIndex ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2", "Option3"];
optionSelector.Labels = options;
@@ -324,17 +324,17 @@ public class OptionSelectorTests
CheckBox secondCheckBox = optionSelector.SubViews.OfType<CheckBox> ().ToArray () [1];
secondCheckBox.SetFocus ();
Assert.Equal (1, optionSelector.Cursor);
Assert.Equal (1, optionSelector.FocusedItem);
// Set focus to third checkbox
CheckBox thirdCheckBox = optionSelector.SubViews.OfType<CheckBox> ().ToArray () [2];
thirdCheckBox.SetFocus ();
Assert.Equal (2, optionSelector.Cursor);
Assert.Equal (2, optionSelector.FocusedItem);
}
[Fact]
public void Cursor_Get_WhenNotFocusable_ReturnsZero ()
public void FocusedItem_Get_WhenNotFocusable_ReturnsZero ()
{
var optionSelector = new OptionSelector { CanFocus = false };
List<string> options = ["Option1", "Option2", "Option3"];
@@ -342,47 +342,48 @@ public class OptionSelectorTests
optionSelector.Labels = options;
optionSelector.Layout ();
Assert.Equal (0, optionSelector.Cursor);
Assert.Equal (0, optionSelector.FocusedItem);
}
[Fact]
public void Cursor_Set_ShouldMoveFocusToCorrectCheckBox ()
public void FocusedItem_Set_ShouldMoveFocusToCorrectCheckBox ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2", "Option3"];
optionSelector.Labels = options;
optionSelector.SetFocus (); // Set focus to optionSelector
optionSelector.Layout ();
// Set cursor to second checkbox
optionSelector.Cursor = 1;
optionSelector.FocusedItem = 1;
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
Assert.True (checkBoxes [1].HasFocus);
Assert.Equal (1, optionSelector.Cursor);
Assert.Equal (1, optionSelector.FocusedItem);
// Set cursor to third checkbox
optionSelector.Cursor = 2;
optionSelector.FocusedItem = 2;
Assert.True (checkBoxes [2].HasFocus);
Assert.Equal (2, optionSelector.Cursor);
Assert.Equal (2, optionSelector.FocusedItem);
}
[Fact]
public void Cursor_Set_OutOfRange_ShouldThrow ()
public void FocusedItem_Set_OutOfRange_ShouldThrow ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2", "Option3"];
optionSelector.Labels = options;
optionSelector.Layout ();
Assert.Throws<ArgumentOutOfRangeException> (() => optionSelector.Cursor = -1);
Assert.Throws<ArgumentOutOfRangeException> (() => optionSelector.Cursor = 3);
Assert.Throws<ArgumentOutOfRangeException> (() => optionSelector.FocusedItem = -1);
Assert.Throws<ArgumentOutOfRangeException> (() => optionSelector.FocusedItem = 3);
}
[Fact]
public void Cursor_Set_WhenNotFocusable_DoesNothing ()
public void FocusedItem_Set_WhenNotFocusable_DoesNothing ()
{
var optionSelector = new OptionSelector { CanFocus = false };
List<string> options = ["Option1", "Option2", "Option3"];
@@ -391,29 +392,30 @@ public class OptionSelectorTests
optionSelector.Layout ();
// Should not throw
optionSelector.Cursor = 1;
optionSelector.FocusedItem = 1;
// Verify nothing changed
Assert.Equal (0, optionSelector.Cursor);
Assert.Equal (0, optionSelector.FocusedItem);
Assert.False (optionSelector is { } && optionSelector.SubViews.OfType<CheckBox> ().Any (cb => cb.HasFocus));
}
[Fact]
public void Cursor_DoesNotChangeValue ()
public void FocusedItem_DoesNotChangeValue ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
List<string> options = ["Option1", "Option2", "Option3"];
optionSelector.Labels = options;
optionSelector.Value = 0; // First option is selected
optionSelector.SetFocus (); // Set focus to optionSelector
optionSelector.Layout ();
// Move cursor to second checkbox
optionSelector.Cursor = 1;
optionSelector.FocusedItem = 1;
// Value should not change, only focus moves
Assert.Equal (0, optionSelector.Value);
Assert.Equal (1, optionSelector.Cursor);
Assert.Equal (1, optionSelector.FocusedItem);
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
Assert.Equal (CheckState.Checked, checkBoxes [0].Value);
@@ -427,7 +429,7 @@ public class OptionSelectorTests
[Fact]
public void OptionSelector_Command_Activate_ForwardsToFocusedCheckBox ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
optionSelector.Labels = ["Option1", "Option2"];
optionSelector.BeginInit ();
optionSelector.EndInit ();
@@ -447,7 +449,7 @@ public class OptionSelectorTests
[Fact]
public void OptionSelector_Command_Accept_RaisesAccepting ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
optionSelector.Labels = ["Option1", "Option2"];
var acceptingFired = false;
@@ -471,16 +473,371 @@ public class OptionSelectorTests
[Fact]
public void OptionSelector_Command_HotKey_ForwardsToFocusedItem ()
{
var optionSelector = new OptionSelector ();
OptionSelector optionSelector = new ();
optionSelector.Labels = ["Option1", "Option2"];
optionSelector.BeginInit ();
optionSelector.EndInit ();
// HotKey forwards to focused item's Activate
// HotKey forwards to focused items Activate
bool? result = optionSelector.InvokeCommand (Command.HotKey);
Assert.True (result);
optionSelector.Dispose ();
}
#region Navigation Command Tests (Down, Up, Right, Left)
// Vertical Orientation - Down Command Tests
[Fact]
public void Command_Down_Vertical_MovesFocusToNextCheckBox ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus ();
Assert.True (checkBoxes [0].HasFocus);
optionSelector.InvokeCommand (Command.Down);
Assert.True (checkBoxes [1].HasFocus);
Assert.False (checkBoxes [0].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Down_Vertical_WrapsAroundToFirst ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [2].SetFocus (); // Focus last checkbox
Assert.True (checkBoxes [2].HasFocus);
optionSelector.InvokeCommand (Command.Down);
Assert.True (checkBoxes [0].HasFocus); // Should wrap to first
Assert.False (checkBoxes [2].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Down_Horizontal_ReturnsFalse ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Horizontal };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus ();
bool? result = optionSelector.InvokeCommand (Command.Down);
Assert.False (result);
Assert.True (checkBoxes [0].HasFocus); // Focus should not change
optionSelector.Dispose ();
}
// Vertical Orientation - Up Command Tests
[Fact]
public void Command_Up_Vertical_MovesFocusToPreviousCheckBox ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [1].SetFocus ();
Assert.True (checkBoxes [1].HasFocus);
optionSelector.InvokeCommand (Command.Up);
Assert.True (checkBoxes [0].HasFocus);
Assert.False (checkBoxes [1].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Up_Vertical_WrapsAroundToLast ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus (); // Focus first checkbox
Assert.True (checkBoxes [0].HasFocus);
optionSelector.InvokeCommand (Command.Up);
Assert.True (checkBoxes [2].HasFocus); // Should wrap to last
Assert.False (checkBoxes [0].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Up_Horizontal_ReturnsFalse ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Horizontal };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [1].SetFocus ();
bool? result = optionSelector.InvokeCommand (Command.Up);
Assert.False (result);
Assert.True (checkBoxes [1].HasFocus); // Focus should not change
optionSelector.Dispose ();
}
// Horizontal Orientation - Right Command Tests
[Fact]
public void Command_Right_Horizontal_MovesFocusToNextCheckBox ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Horizontal };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus ();
Assert.True (checkBoxes [0].HasFocus);
optionSelector.InvokeCommand (Command.Right);
Assert.True (checkBoxes [1].HasFocus);
Assert.False (checkBoxes [0].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Right_Horizontal_WrapsAroundToFirst ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Horizontal };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [2].SetFocus (); // Focus last checkbox
Assert.True (checkBoxes [2].HasFocus);
optionSelector.InvokeCommand (Command.Right);
Assert.True (checkBoxes [0].HasFocus); // Should wrap to first
Assert.False (checkBoxes [2].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Right_Vertical_ReturnsFalse ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus ();
bool? result = optionSelector.InvokeCommand (Command.Right);
Assert.False (result);
Assert.True (checkBoxes [0].HasFocus); // Focus should not change
optionSelector.Dispose ();
}
// Horizontal Orientation - Left Command Tests
[Fact]
public void Command_Left_Horizontal_MovesFocusToPreviousCheckBox ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Horizontal };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [1].SetFocus ();
Assert.True (checkBoxes [1].HasFocus);
optionSelector.InvokeCommand (Command.Left);
Assert.True (checkBoxes [0].HasFocus);
Assert.False (checkBoxes [1].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Left_Horizontal_WrapsAroundToLast ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Horizontal };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus (); // Focus first checkbox
Assert.True (checkBoxes [0].HasFocus);
optionSelector.InvokeCommand (Command.Left);
Assert.True (checkBoxes [2].HasFocus); // Should wrap to last
Assert.False (checkBoxes [0].HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Left_Vertical_ReturnsFalse ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [1].SetFocus ();
bool? result = optionSelector.InvokeCommand (Command.Left);
Assert.False (result);
Assert.True (checkBoxes [1].HasFocus); // Focus should not change
optionSelector.Dispose ();
}
// ShowValue Style Tests
[Fact]
public void Command_Down_Vertical_WithShowValue_FocusesValueFieldAtEnd ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical, Styles = SelectorStyles.ShowValue };
optionSelector.Labels = ["Option1", "Option2"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [1].SetFocus (); // Focus last checkbox
optionSelector.InvokeCommand (Command.Down);
// Should focus the value field instead of wrapping
View valueField = optionSelector.SubViews.FirstOrDefault (v => v.Id == "valueField");
Assert.NotNull (valueField);
Assert.True (valueField.HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Up_Vertical_WithShowValue_FocusesValueFieldAtStart ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical, Styles = SelectorStyles.ShowValue };
optionSelector.Labels = ["Option1", "Option2"];
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus (); // Focus first checkbox
optionSelector.InvokeCommand (Command.Up);
// Should focus the value field instead of wrapping
View valueField = optionSelector.SubViews.FirstOrDefault (v => v.Id == "valueField");
Assert.NotNull (valueField);
Assert.True (valueField.HasFocus);
optionSelector.Dispose ();
}
[Fact]
public void Command_Up_Vertical_WithShowValue_FromValueField_FocusesLastCheckBox ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical, Styles = SelectorStyles.ShowValue };
optionSelector.Labels = ["Option1", "Option2"];
optionSelector.SetFocus ();
optionSelector.Layout ();
View valueField = optionSelector.SubViews.FirstOrDefault (v => v.Id == "valueField");
Assert.NotNull (valueField);
valueField.SetFocus ();
Assert.True (valueField.HasFocus);
optionSelector.InvokeCommand (Command.Up);
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
Assert.True (checkBoxes [1].HasFocus); // Should focus last checkbox
optionSelector.Dispose ();
}
// Navigation Does Not Change Value Tests
[Fact]
public void Command_Down_DoesNotChangeValue ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.Value = 0;
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [0].SetFocus ();
optionSelector.InvokeCommand (Command.Down);
Assert.Equal (0, optionSelector.Value); // Value should remain unchanged
Assert.True (checkBoxes [1].HasFocus); // But focus moved
optionSelector.Dispose ();
}
[Fact]
public void Command_Up_DoesNotChangeValue ()
{
OptionSelector optionSelector = new () { Orientation = Orientation.Vertical };
optionSelector.Labels = ["Option1", "Option2", "Option3"];
optionSelector.Value = 2;
optionSelector.SetFocus ();
optionSelector.Layout ();
CheckBox [] checkBoxes = optionSelector.SubViews.OfType<CheckBox> ().ToArray ();
checkBoxes [2].SetFocus ();
optionSelector.InvokeCommand (Command.Up);
Assert.Equal (2, optionSelector.Value); // Value should remain unchanged
Assert.True (checkBoxes [1].HasFocus); // But focus moved
optionSelector.Dispose ();
}
#endregion
}

View File

@@ -11,7 +11,7 @@ public class SelectorBaseTests
[Fact]
public void Constructor_SetsDefaults ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
Assert.True (selector.CanFocus);
Assert.Equal (Dim.Auto (DimAutoStyle.Content), selector.Width);
@@ -33,7 +33,7 @@ public class SelectorBaseTests
[Fact]
public void Value_Set_ValidValue_UpdatesValue ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Value = 1;
@@ -44,7 +44,7 @@ public class SelectorBaseTests
[Fact]
public void Value_Set_InvalidValue_ThrowsArgumentOutOfRangeException ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
Assert.Throws<ArgumentOutOfRangeException> (() => selector.Value = 5);
@@ -54,7 +54,7 @@ public class SelectorBaseTests
[Fact]
public void Value_Set_Null_Succeeds ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Value = null;
@@ -65,12 +65,12 @@ public class SelectorBaseTests
[Fact]
public void Value_Set_SameValue_DoesNotRaiseEvent ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Value = 1;
var eventRaisedCount = 0;
selector.ValueChanged += (s, e) => eventRaisedCount++;
selector.ValueChanged += (_, _) => eventRaisedCount++;
selector.Value = 1; // Set to same value
@@ -80,11 +80,11 @@ public class SelectorBaseTests
[Fact]
public void Value_Changed_RaisesValueChangedEvent ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
int? capturedValue = null;
selector.ValueChanged += (s, e) => capturedValue = e.NewValue;
selector.ValueChanged += (_, e) => capturedValue = e.NewValue;
selector.Value = 1;
@@ -98,7 +98,7 @@ public class SelectorBaseTests
[Fact]
public void Values_Get_WhenNull_ReturnsSequentialValues ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2", "Option3"];
IReadOnlyList<int>? values = selector.Values;
@@ -111,7 +111,7 @@ public class SelectorBaseTests
[Fact]
public void Values_Set_UpdatesValues ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Values = [10, 20];
@@ -122,7 +122,7 @@ public class SelectorBaseTests
[Fact]
public void Values_Set_SetsDefaultValue ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Value = null;
selector.Labels = ["Option1", "Option2"];
@@ -138,7 +138,7 @@ public class SelectorBaseTests
[Fact]
public void Labels_Set_CreatesSubViews ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
@@ -148,7 +148,7 @@ public class SelectorBaseTests
[Fact]
public void Labels_Set_Null_RemovesSubViews ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Labels = null;
@@ -159,7 +159,7 @@ public class SelectorBaseTests
[Fact]
public void Labels_Values_CountMismatch_DoesNotCreateSubViews ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Values = [0, 1, 2];
selector.Labels = ["Option1", "Option2"]; // Mismatch
@@ -174,7 +174,7 @@ public class SelectorBaseTests
[Fact]
public void SetValuesAndLabels_FromEnum_SetsValuesAndLabels ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.SetValuesAndLabels<SelectorStyles> ();
@@ -187,7 +187,7 @@ public class SelectorBaseTests
[Fact]
public void SetValuesAndLabels_SetsCorrectIntegerValues ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.SetValuesAndLabels<SelectorStyles> ();
@@ -203,7 +203,7 @@ public class SelectorBaseTests
[Fact]
public void Styles_Set_None_NoExtraSubViews ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Styles = SelectorStyles.None;
@@ -215,7 +215,7 @@ public class SelectorBaseTests
[Fact]
public void Styles_Set_ShowValue_AddsValueField ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Styles = SelectorStyles.ShowValue;
@@ -228,7 +228,7 @@ public class SelectorBaseTests
[Fact]
public void Styles_Set_ShowValue_ValueFieldDisplaysCurrentValue ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Value = 1;
@@ -242,7 +242,7 @@ public class SelectorBaseTests
[Fact]
public void Styles_Set_ShowValue_ValueFieldUpdatesOnValueChange ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Styles = SelectorStyles.ShowValue;
@@ -256,7 +256,7 @@ public class SelectorBaseTests
[Fact]
public void Styles_Set_SameValue_DoesNotRecreateSubViews ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Styles = SelectorStyles.ShowValue;
@@ -375,7 +375,7 @@ public class SelectorBaseTests
[Fact]
public void Orientation_Vertical_CheckBoxesStackedVertically ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Orientation = Orientation.Vertical;
selector.Layout ();
@@ -388,7 +388,7 @@ public class SelectorBaseTests
[Fact]
public void Orientation_Horizontal_CheckBoxesArrangedHorizontally ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Orientation = Orientation.Horizontal;
selector.Layout ();
@@ -402,7 +402,7 @@ public class SelectorBaseTests
[Fact]
public void Orientation_Change_TriggersLayout ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
selector.Layout ();
@@ -424,7 +424,7 @@ public class SelectorBaseTests
[Fact]
public void HorizontalSpace_Default_Is2 ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
Assert.Equal (2, selector.HorizontalSpace);
}
@@ -472,7 +472,7 @@ public class SelectorBaseTests
[Fact]
public void DoubleClickAccepts_Default_IsTrue ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
Assert.True (selector.DoubleClickAccepts);
}
@@ -485,10 +485,10 @@ public class SelectorBaseTests
selector.Layout ();
var acceptCount = 0;
selector.Accepting += (s, e) => acceptCount++;
selector.Accepting += (_, _) => acceptCount++;
CheckBox checkBox = selector.SubViews.OfType<CheckBox> ().First ();
checkBox.NewMouseEvent (new () { Position = Point.Empty, Flags = MouseFlags.LeftButtonDoubleClicked });
checkBox.NewMouseEvent (new Mouse { Position = Point.Empty, Flags = MouseFlags.LeftButtonDoubleClicked });
Assert.Equal (1, acceptCount);
}
@@ -501,10 +501,10 @@ public class SelectorBaseTests
selector.Layout ();
var acceptCount = 0;
selector.Accepting += (s, e) => acceptCount++;
selector.Accepting += (_, _) => acceptCount++;
CheckBox checkBox = selector.SubViews.OfType<CheckBox> ().First ();
checkBox.NewMouseEvent (new () { Position = Point.Empty, Flags = MouseFlags.LeftButtonDoubleClicked });
checkBox.NewMouseEvent (new Mouse { Position = Point.Empty, Flags = MouseFlags.LeftButtonDoubleClicked });
Assert.Equal (0, acceptCount);
}
@@ -516,7 +516,7 @@ public class SelectorBaseTests
[Fact]
public void CreateSubViews_RemovesOldSubViewsAndCreatesNew ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
int oldCount = selector.SubViews.Count;
@@ -531,7 +531,7 @@ public class SelectorBaseTests
[Fact]
public void CreateSubViews_SetsCheckBoxProperties ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Test Option"];
selector.Values = [42];
@@ -550,11 +550,11 @@ public class SelectorBaseTests
[Fact]
public void HotKey_Command_DoesNotFireAccept ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
var acceptCount = 0;
selector.Accepting += (s, e) => acceptCount++;
selector.Accepting += (_, _) => acceptCount++;
selector.InvokeCommand (Command.HotKey);
@@ -564,11 +564,11 @@ public class SelectorBaseTests
[Fact]
public void Accept_Command_FiresAccept ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = ["Option1", "Option2"];
var acceptCount = 0;
selector.Accepting += (s, e) => acceptCount++;
selector.Accepting += (_, _) => acceptCount++;
selector.InvokeCommand (Command.Accept);
@@ -582,7 +582,7 @@ public class SelectorBaseTests
[Fact]
public void EmptyLabels_CreatesNoSubViews ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
selector.Labels = [];
@@ -592,7 +592,7 @@ public class SelectorBaseTests
[Fact]
public void Value_WithNoLabels_CanBeSet ()
{
var selector = new OptionSelector ();
OptionSelector selector = new ();
// This should work even without labels
Exception? exception = Record.Exception (() => selector.Value = null);
@@ -602,4 +602,47 @@ public class SelectorBaseTests
}
#endregion
#region Navigation Keys
[Theory]
[InlineData (SelectorStyles.None)]
[InlineData (SelectorStyles.ShowNoneFlag)]
[InlineData (SelectorStyles.ShowAllFlag)]
[InlineData (SelectorStyles.ShowValue)]
[InlineData (SelectorStyles.All)]
public void Navigation_Keys_Move_Out_And_Into_Not_Within (SelectorStyles selectorStyles)
{
using IApplication app = Application.Create ().Init (DriverRegistry.Names.ANSI);
using Runnable runnable = new ();
var view1 = new View { CanFocus = true };
var selector = new OptionSelector { Styles = selectorStyles };
List<string> options = ["Option1", "Option2", "Option3"];
selector.Labels = options;
var view2 = new View { CanFocus = true };
runnable.Add (view1, selector, view2);
app.Begin (runnable);
// Set focus to view1
view1.SetFocus ();
// Invoke Tab command to move focus to selector
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab));
Assert.True (selector.HasFocus);
// Invoke Tab command again to move focus to view2
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab));
Assert.True (view2.HasFocus);
// Now test Shift+Tab to move focus back to selector
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab.WithShift));
Assert.True (selector.HasFocus);
// Finally, Shift+Tab again to move focus back to view1
Assert.True (app.Keyboard.RaiseKeyDownEvent (Key.Tab.WithShift));
Assert.True (view1.HasFocus);
}
#endregion
}