| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- #nullable enable
- using System.Collections.Immutable;
- using System.Diagnostics;
- namespace Terminal.Gui.Views;
- // DoubleClick - Focus, Select, and Accept the item under the mouse.
- // Click - Focus, Select, and do NOT Accept the item under the mouse.
- // CanFocus - Not Focused:
- // HotKey - Restore Focus. Advance Active. Do NOT Accept.
- // Item HotKey - Focus item. If item is not active, make Active. Do NOT Accept.
- // !CanFocus - Not Focused:
- // 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, Select current. Do NOT Accept.
- // Enter key - Select 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.
- /// <summary>
- /// Provides a user interface for displaying and selecting a single item from a list of options.
- /// Each option is represented by a checkbox, but only one can be selected at a time.
- /// <see cref="OptionSelector{TEnum}"/> provides a type-safe version where a <see langword="enum"/> can be
- /// provided.
- /// </summary>
- public class OptionSelector : SelectorBase, IDesignable
- {
- /// <inheritdoc />
- public OptionSelector ()
- {
- // By default, for OptionSelector, Value is set to 0. It can be set to null if a developer
- // really wants that.
- base.Value = 0;
- }
- /// <inheritdoc />
- protected override bool OnHandlingHotKey (CommandEventArgs args)
- {
- if (base.OnHandlingHotKey (args) is true)
- {
- return true;
- }
- if (!CanFocus)
- {
- if (RaiseSelecting (args.Context) is true)
- {
- return true;
- }
- }
- else if (!HasFocus && Value is null)
- {
- if (RaiseSelecting (args.Context) is true)
- {
- return true;
- }
- SetFocus ();
- Value = Values? [0];
- return true;
- }
- return false;
- }
- /// <inheritdoc />
- protected override bool OnSelecting (CommandEventArgs args)
- {
- if (base.OnSelecting (args) is true)
- {
- return true;
- }
- if (!CanFocus || args.Context?.Source is not CheckBox checkBox)
- {
- Cycle ();
- return false;
- }
- if (args.Context is CommandContext<KeyBinding> { } && (int)checkBox.Data! == Value)
- {
- // Caused by keypress. If the checkbox is already checked, we cycle to the next one.
- Cycle ();
- }
- else
- {
- if (Value == (int)checkBox.Data!)
- {
- return true;
- }
- Value = (int)checkBox.Data!;
- // if (HasFocus)
- {
- UpdateChecked ();
- }
- }
- return false;
- }
- /// <inheritdoc />
- protected override void OnSubViewAdded (View view)
- {
- base.OnSubViewAdded (view);
- if (view is not CheckBox checkbox)
- {
- return;
- }
- checkbox.RadioStyle = true;
- checkbox.Selecting += OnCheckboxOnSelecting;
- checkbox.Accepting += OnCheckboxOnAccepting;
- }
- private void OnCheckboxOnSelecting (object? sender, CommandEventArgs args)
- {
- if (sender is not CheckBox checkbox)
- {
- return;
- }
- // Verify at most one is checked
- Debug.Assert (SubViews.OfType<CheckBox> ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1);
- if (args.Context is CommandContext<MouseBinding> { } && checkbox.CheckedState == CheckState.Checked)
- {
- // If user clicks with mouse and item is already checked, do nothing
- args.Handled = true;
- return;
- }
- if (args.Context is CommandContext<KeyBinding> binding && binding.Command == Command.HotKey && checkbox.CheckedState == CheckState.Checked)
- {
- // If user uses an item hotkey and the item is already checked, do nothing
- args.Handled = true;
- return;
- }
- if (checkbox.CanFocus)
- {
- // For Select, if the view is focusable and SetFocus succeeds, by defition,
- // the event is handled. So return what SetFocus returns.
- checkbox.SetFocus ();
- }
- // Selecting doesn't normally propogate, so we do it here
- if (InvokeCommand (Command.Select, args.Context) is true)
- {
- // Do not return here; we want to toggle the checkbox state
- args.Handled = true;
- return;
- }
- args.Handled = true;
- }
- private void OnCheckboxOnAccepting (object? sender, CommandEventArgs args)
- {
- if (sender is not CheckBox checkbox)
- {
- return;
- }
- Value = (int)checkbox.Data!;
- args.Handled = false; // Do not set to false; let Accepting propagate
- }
- private void Cycle ()
- {
- int valueIndex = Values.IndexOf (v => v == Value);
- Value = valueIndex == Values?.Count () - 1
- ? Values! [0]
- : Values! [valueIndex + 1];
- if (HasFocus)
- {
- valueIndex = Values.IndexOf (v => v == Value);
- SubViews.OfType<CheckBox> ().ToArray () [valueIndex].SetFocus ();
- }
- // Verify at most one is checked
- Debug.Assert (SubViews.OfType<CheckBox> ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1);
- }
- /// <summary>
- /// Updates the checked state of all checkbox subviews so that only the checkbox corresponding
- /// to the current <see cref="SelectorBase.Value"/> is checked. Throws <see cref="InvalidOperationException"/>
- /// if a checkbox's Data property is not set.
- /// </summary>
- /// <exception cref="InvalidOperationException"></exception>
- public override void UpdateChecked ()
- {
- foreach (CheckBox cb in SubViews.OfType<CheckBox> ())
- {
- int value = (int)(cb.Data ?? throw new InvalidOperationException ("CheckBox.Data must be set"));
- cb.CheckedState = value == Value ? CheckState.Checked : CheckState.UnChecked;
- }
- // Verify at most one is checked
- Debug.Assert (SubViews.OfType<CheckBox> ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1);
- }
- /// <summary>
- /// Gets or sets the <see cref="SelectorBase.Labels"/> index for the cursor. The cursor may or may not be the selected
- /// RadioItem.
- /// </summary>
- /// <remarks>
- /// <para>
- /// Maps to either the X or Y position within <see cref="View.Viewport"/> depending on <see cref="Orientation"/>.
- /// </para>
- /// </remarks>
- public int Cursor
- {
- get => !CanFocus ? 0 : SubViews.OfType<CheckBox> ().ToArray ().IndexOf (Focused);
- set
- {
- if (!CanFocus)
- {
- return;
- }
- CheckBox [] checkBoxes = SubViews.OfType<CheckBox> ().ToArray ();
- if (value < 0 || value >= checkBoxes.Length)
- {
- throw new ArgumentOutOfRangeException (nameof (value), @"Cursor index is out of range");
- }
- checkBoxes [value].SetFocus ();
- }
- }
- /// <inheritdoc/>
- public bool EnableForDesign ()
- {
- AssignHotKeys = true;
- Labels = ["Option 1", "Option 2", "Third Option", "Option Quattro"];
- return true;
- }
- }
|