#nullable enable namespace Terminal.Gui; /// Shows a check box that can be cycled between two or three states. public class CheckBox : View { /// /// Gets or sets the default Highlight Style. /// [SerializableConfigurationProperty (Scope = typeof (ThemeScope))] public static HighlightStyle DefaultHighlightStyle { get; set; } = HighlightStyle.PressedOutside | HighlightStyle.Pressed | HighlightStyle.Hover; /// /// Initializes a new instance of . /// public CheckBox () { Width = Dim.Auto (DimAutoStyle.Text); Height = Dim.Auto (DimAutoStyle.Text, 1); CanFocus = true; // Select (Space key and single-click) - Advance state and raise Select event - DO NOT raise Accept AddCommand (Command.Select, AdvanceAndSelect); // Hotkey - Advance state and raise Select event - DO NOT raise Accept AddCommand (Command.HotKey, AdvanceAndSelect); // Accept (Enter key) - Raise Accept event - DO NOT advance state AddCommand (Command.Accept, RaiseAccepting); TitleChanged += Checkbox_TitleChanged; HighlightStyle = DefaultHighlightStyle; } private bool? AdvanceAndSelect (CommandContext ctx) { bool? cancelled = AdvanceCheckState (); if (cancelled is true) { return true; } if (RaiseSelecting (ctx) is true) { return true; } return ctx.Command == Command.HotKey ? cancelled : cancelled is false; } private void Checkbox_TitleChanged (object? sender, EventArgs e) { base.Text = e.CurrentValue; TextFormatter.HotKeySpecifier = HotKeySpecifier; } /// public override string Text { get => Title; set => base.Text = Title = value; } /// public override Rune HotKeySpecifier { get => base.HotKeySpecifier; set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value; } private bool _allowNone; /// /// If allows to be . The default is /// . /// public bool AllowCheckStateNone { get => _allowNone; set { if (_allowNone == value) { return; } _allowNone = value; if (CheckedState == CheckState.None) { CheckedState = CheckState.UnChecked; } } } private CheckState _checkedState = CheckState.UnChecked; /// /// The state of the . /// /// /// /// If is and , the /// /// will display the ConfigurationManager.Glyphs.CheckStateNone character (☒). /// /// /// If , the /// will display the ConfigurationManager.Glyphs.CheckStateUnChecked character (☐). /// /// /// If , the /// will display the ConfigurationManager.Glyphs.CheckStateChecked character (☑). /// /// public CheckState CheckedState { get => _checkedState; set => ChangeCheckedState (value); } /// /// INTERNAL Sets CheckedState. /// /// /// /// if state change was canceled, if the state changed, and /// if the state was not changed for some other reason. /// private bool? ChangeCheckedState (CheckState value) { if (_checkedState == value || (value is CheckState.None && !AllowCheckStateNone)) { return null; } CancelEventArgs e = new (in _checkedState, ref value); if (OnCheckedStateChanging (e)) { return true; } CheckedStateChanging?.Invoke (this, e); if (e.Cancel) { return e.Cancel; } _checkedState = value; UpdateTextFormatterText (); SetNeedsLayout (); EventArgs args = new (in _checkedState); OnCheckedStateChanged (args); CheckedStateChanged?.Invoke (this, args); return false; } /// Called when the state is changing. /// /// /// The state cahnge can be cancelled by setting the args.Cancel to . /// /// protected virtual bool OnCheckedStateChanging (CancelEventArgs args) { return false; } /// Raised when the state is changing. /// /// /// This event can be cancelled. If cancelled, the will not change its state. /// /// public event EventHandler>? CheckedStateChanging; /// Called when the state has changed. protected virtual void OnCheckedStateChanged (EventArgs args) { } /// Raised when the state has changed. public event EventHandler>? CheckedStateChanged; /// /// Advances to the next value. Invokes the cancelable /// event. /// /// /// /// Cycles through the states , , and /// . /// /// /// If the event is not canceled, the will be updated /// and the event will be raised. /// /// /// /// if state change was canceled, if the state changed, and /// if the state was not changed for some other reason. /// public bool? AdvanceCheckState () { CheckState oldValue = CheckedState; CancelEventArgs e = new (in _checkedState, ref oldValue); switch (CheckedState) { case CheckState.None: e.NewValue = CheckState.Checked; break; case CheckState.Checked: e.NewValue = CheckState.UnChecked; break; case CheckState.UnChecked: if (AllowCheckStateNone) { e.NewValue = CheckState.None; } else { e.NewValue = CheckState.Checked; } break; } bool? cancelled = ChangeCheckedState (e.NewValue); return cancelled; } /// protected override void UpdateTextFormatterText () { base.UpdateTextFormatterText (); switch (TextAlignment) { case Alignment.Start: case Alignment.Center: case Alignment.Fill: TextFormatter.Text = $"{GetCheckedGlyph ()} {Text}"; break; case Alignment.End: TextFormatter.Text = $"{Text} {GetCheckedGlyph ()}"; break; } } private Rune GetCheckedGlyph () { return CheckedState switch { CheckState.Checked => Glyphs.CheckStateChecked, CheckState.UnChecked => Glyphs.CheckStateUnChecked, CheckState.None => Glyphs.CheckStateNone, _ => throw new ArgumentOutOfRangeException () }; } }