diff --git a/Examples/InlineColorPicker/Program.cs b/Examples/InlineColorPicker/Program.cs index 73290a50f0..9f57db9a9f 100644 --- a/Examples/InlineColorPicker/Program.cs +++ b/Examples/InlineColorPicker/Program.cs @@ -1,17 +1,44 @@ // Inline ColorPicker — demonstrates using RunnableWrapper in inline mode. // +// NOTE: See https://github.com/gui-cs/clet that turns every Terminal.Gui View into a CLI command +// NOTE: — typed inputs, a real file picker, a Markdown viewer — with consistent JSON output, +// NOTE: predictable exit codes, and full keyboard/mouse support. Works for humans and AI agents alike. +// // Renders a ColorPicker inline in the terminal (primary buffer) without dialog buttons. // If the user accepts (double-click), the selected color name is written to stdout. // If the user cancels (Esc), nothing is output and exit code is 1. // // Usage: // dotnet run --project Examples/InlineColorPicker +// dotnet run --project Examples/InlineColorPicker -- --initial "#FF0000" +// dotnet run --project Examples/InlineColorPicker -- --initial Red // $color = dotnet run --project Examples/InlineColorPicker # capture in shell using Terminal.Gui.App; using Terminal.Gui.Drawing; +using Terminal.Gui.ViewBase; using Terminal.Gui.Views; +// Parse command-line arguments +string? initialValue = null; + +for (var i = 0; i < args.Length; i++) +{ + if (args [i] is "--initial" or "-i") + { + if (i + 1 < args.Length) + { + initialValue = args [++i]; + } + else + { + Console.Error.WriteLine ("Error: --initial requires a color value (e.g., \"#FF0000\" or \"Red\")."); + + return 1; + } + } +} + // Enable inline mode before Init Application.AppModel = AppModel.Inline; @@ -19,14 +46,23 @@ // Wrap ColorPicker in a RunnableWrapper — no dialog buttons, just the picker. // ColorPicker raises Command.Accept on double-click. -RunnableWrapper wrapper = new () -{ - Title = "Select a Color (Double-click to accept, Esc to cancel)", - ResultExtractor = cp => cp.Value -}; +RunnableWrapper wrapper = new () { Title = "Select a Color (Double-click to accept, Esc to cancel)", ResultExtractor = cp => cp.Value }; // Enable color name display wrapper.GetWrappedView ().Style.ShowColorName = true; +wrapper.GetWrappedView ().ApplyStyleChanges (); + +// Apply initial value via IValue.TrySetValueFromString if provided +if (initialValue is { }) +{ + if (!((IValue)wrapper.GetWrappedView ()).TrySetValueFromString (initialValue)) + { + Console.Error.WriteLine ($"Error: '{initialValue}' is not a valid color (use e.g., \"#FF0000\" or \"Red\")."); + app.Dispose (); + + return 1; + } +} // Run inline — blocks until user accepts or cancels app.Run (wrapper); @@ -39,9 +75,7 @@ { StandardColorsNameResolver resolver = new (); - string output = resolver.TryNameColor (selectedColor, out string? name) - ? name - : selectedColor.ToString (); + string output = resolver.TryNameColor (selectedColor, out string? name) ? name : selectedColor.ToString (); Console.WriteLine (output); diff --git a/Examples/InlineSelect/Program.cs b/Examples/InlineSelect/Program.cs index a80daf7ae1..4c34f8e4a4 100644 --- a/Examples/InlineSelect/Program.cs +++ b/Examples/InlineSelect/Program.cs @@ -1,55 +1,63 @@ // InlineSelect — demonstrates using RunnableWrapper in inline mode. // +// NOTE: See https://github.com/gui-cs/clet that turns every Terminal.Gui View into a CLI command +// NOTE: — typed inputs, a real file picker, a Markdown viewer — with consistent JSON output, +// NOTE: predictable exit codes, and full keyboard/mouse support. Works for humans and AI agents alike. +// // Renders an OptionSelector inline in the terminal with options from the command line. // Supports horizontal or vertical orientation via --horizontal / --vertical flags. // Hot keys are auto-assigned from option text. // Supports --timeout to auto-cancel via CancellationToken (demonstrates RunAsync). +// Supports --initial to pre-select an option via IValue.TrySetValueFromString. // // Usage: // dotnet run --project Examples/InlineSelect -- Apple Banana Cherry // dotnet run --project Examples/InlineSelect -- --horizontal Red Green Blue Yellow // dotnet run --project Examples/InlineSelect -- --vertical One Two Three // dotnet run --project Examples/InlineSelect -- --timeout 10 Apple Banana Cherry +// dotnet run --project Examples/InlineSelect -- --initial 1 Apple Banana Cherry using Terminal.Gui.App; using Terminal.Gui.Drawing; using Terminal.Gui.ViewBase; using Terminal.Gui.Views; +using Timeout = System.Threading.Timeout; // Parse command-line arguments Orientation orientation = Orientation.Vertical; List options = []; int? timeoutSeconds = null; +string? initialValue = null; -for (int i = 0; i < args.Length; i++) +for (var i = 0; i < args.Length; i++) { string arg = args [i]; - if (arg is "--horizontal" or "-h") - { - orientation = Orientation.Horizontal; - } - else if (arg is "--vertical" or "-v") + switch (arg) { - orientation = Orientation.Vertical; - } - else if (arg is "--timeout" or "-t") - { - if (i + 1 < args.Length && int.TryParse (args [i + 1], out int seconds)) - { + case "--horizontal" or "-h": orientation = Orientation.Horizontal; break; + + case "--vertical" or "-v": orientation = Orientation.Vertical; break; + + case "--timeout" or "-t" when i + 1 < args.Length && int.TryParse (args [i + 1], out int seconds): timeoutSeconds = seconds; i++; // skip the next arg (the number) - } - else - { + + break; + + case "--timeout" or "-t": Console.Error.WriteLine ("Error: --timeout requires a number of seconds."); return 1; - } - } - else - { - options.Add (arg); + + case "--initial" or "-i" when i + 1 < args.Length: initialValue = args [++i]; break; + + case "--initial" or "-i": + Console.Error.WriteLine ("Error: --initial requires an index value."); + + return 1; + + default: options.Add (arg); break; } } @@ -66,12 +74,7 @@ IApplication app = Application.Create ().Init (); // Build the OptionSelector with command-line options -OptionSelector selector = new () -{ - Labels = options, - Orientation = orientation, - AssignHotKeys = true -}; +OptionSelector selector = new () { Labels = options, Orientation = orientation, AssignHotKeys = true }; // Wrap in RunnableWrapper — auto-extracts Value via IValue RunnableWrapper wrapper = new (selector) @@ -83,6 +86,25 @@ BorderStyle = LineStyle.Rounded }; +// Apply initial value if provided — match by label (case-insensitive) or by numeric index +if (initialValue is { }) +{ + // First try matching a label + int matchIndex = options.FindIndex (o => string.Equals (o, initialValue, StringComparison.OrdinalIgnoreCase)); + + if (matchIndex >= 0) + { + selector.Value = matchIndex; + } + else if (!((IValue)selector).TrySetValueFromString (initialValue)) + { + Console.Error.WriteLine ($"Error: '{initialValue}' does not match any option and is not a valid index."); + app.Dispose (); + + return 1; + } +} + // Run with optional timeout via RunAsync + CancellationToken if (timeoutSeconds.HasValue) { @@ -93,22 +115,20 @@ DateTime startTime = DateTime.UtcNow; int totalMs = timeoutSeconds.Value * 1000; - using Timer progressTimer = new ( - _ => app.Invoke ( - _ => - { - int elapsedMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; - int percent = Math.Min (elapsedMs * 100 / totalMs, 100); - app.Driver?.ProgressIndicator?.SetValue (percent); - }), - null, - 0, - 250); + await using Timer progressTimer = new (_ => app.Invoke (_ => + { + var elapsedMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds; + int percent = Math.Min (elapsedMs * 100 / totalMs, 100); + app.Driver?.ProgressIndicator?.SetValue (percent); + }), + null, + 0, + 250); await app.RunAsync (wrapper, cts.Token); // Clear the progress indicator when done - progressTimer.Change (System.Threading.Timeout.Infinite, System.Threading.Timeout.Infinite); + progressTimer.Change (Timeout.Infinite, Timeout.Infinite); app.Driver?.ProgressIndicator?.Clear (); } else @@ -121,7 +141,7 @@ app.Dispose (); -if (result is { } selectedIndex && selectedIndex >= 0 && selectedIndex < options.Count) +if (result is { } selectedIndex and >= 0 && selectedIndex < options.Count) { Console.WriteLine (options [selectedIndex]); diff --git a/Examples/PromptExample/Program.cs b/Examples/PromptExample/Program.cs index 321531ff93..6011553bad 100644 --- a/Examples/PromptExample/Program.cs +++ b/Examples/PromptExample/Program.cs @@ -1,4 +1,7 @@ // Example demonstrating the Prompt API for getting typed input from users +// NOTE: See https://github.com/gui-cs/clet that turns every Terminal.Gui View into a CLI command +// NOTE: — typed inputs, a real file picker, a Markdown viewer — with consistent JSON output, +// NOTE: predictable exit codes, and full keyboard/mouse support. Works for humans and AI agents alike. using Terminal.Gui.App; using Terminal.Gui.Configuration; @@ -8,6 +11,7 @@ using Terminal.Gui.ViewBase; using Terminal.Gui.Views; using Color = Terminal.Gui.Drawing.Color; + // ReSharper disable AccessToDisposedClosure ConfigurationManager.Enable (ConfigLocations.All); @@ -19,7 +23,18 @@ mainWindow.Text = "This example demonstrates various uses of the Prompt API.\nPress the buttons to try different prompt types.\nPress Esc to quit."; mainWindow.TextAlignment = Alignment.Center; -int buttonY = 0; +// Initial Value TextField — entered text is applied to prompt views via IValue.TrySetValueFromString +TextField initialValueField = new () +{ + Title = "Initial _Value (TrySetValueFromString)", + X = 0, + Y = 0, + Width = Dim.Fill (), + BorderStyle = LineStyle.Dotted +}; +mainWindow.Add (initialValueField); + +var buttonY = 2; // Example 1: TextField with string result using auto-Text extraction Button textFieldButton = new () { Title = "TextField (Auto-Text)", X = Pos.Center (), Y = buttonY++ }; @@ -31,6 +46,12 @@ prompt.Title = textFieldButton.Title; prompt.GetWrappedView ().Width = 40; prompt.GetWrappedView ().Text = "Default name"; + + if (!string.IsNullOrEmpty (initialValueField.Text)) + { + ((IValue)prompt.GetWrappedView ()) + .TrySetValueFromString (initialValueField.Text); + } }); MessageBox.Query (app, textFieldButton.Title, result is { } ? $"You entered: {result}" : "Canceled", Strings.btnOk); @@ -51,6 +72,13 @@ "Some text\nis nice."; prompt.GetWrappedView ().Width = Dim.Fill (0, 40); prompt.GetWrappedView ().Height = Dim.Fill (0, 8); + + if (!string.IsNullOrEmpty (initialValueField.Text)) + { + ((IValue)prompt.GetWrappedView ()) + .TrySetValueFromString (initialValueField + .Text); + } }); MessageBox.Query (app, textViewButton.Title, result is { } ? $"You entered: {result}" : "Canceled", Strings.btnOk); @@ -68,6 +96,12 @@ { prompt.Title = "Select a Date"; prompt.GetWrappedView ().Value = DateTime.Now; + + if (!string.IsNullOrEmpty (initialValueField.Text)) + { + ((IValue)prompt.GetWrappedView ()) + .TrySetValueFromString (initialValueField.Text); + } }); if (result is { } selectedDate) @@ -88,7 +122,16 @@ colorPickerButton.Accepting += (_, _) => { Color? result = mainWindow.Prompt (input: null, - beginInitHandler: prompt => { prompt.Title = "Pick a Color"; }); + beginInitHandler: prompt => + { + prompt.Title = "Pick a Color"; + + if (!string.IsNullOrEmpty (initialValueField.Text)) + { + ((IValue)prompt.GetWrappedView ()) + .TrySetValueFromString (initialValueField.Text); + } + }); if (result is { } selectedColor) { @@ -111,6 +154,12 @@ { prompt.Title = "Pick a Color (as text)"; prompt.GetWrappedView ().SelectedColor = Color.Red; + + if (!string.IsNullOrEmpty (initialValueField.Text)) + { + ((IValue)prompt.GetWrappedView ()) + .TrySetValueFromString (initialValueField.Text); + } }); MessageBox.Query (app, colorTextButton.Title, result is { } ? $"Color as text: {result}" : "Canceled", Strings.btnOk); @@ -178,6 +227,14 @@ beginInitHandler: prompt => { prompt.Title = "Choose Selector Styles"; + + if (!string.IsNullOrEmpty (initialValueField + .Text)) + { + ((IValue)prompt.GetWrappedView ()) + .TrySetValueFromString (initialValueField + .Text); + } }); if (result is { } styles) diff --git a/Examples/UICatalog/Scenarios/Popovers.cs b/Examples/UICatalog/Scenarios/Popovers.cs index 95cf1ffba3..5ba8db1170 100644 --- a/Examples/UICatalog/Scenarios/Popovers.cs +++ b/Examples/UICatalog/Scenarios/Popovers.cs @@ -16,6 +16,7 @@ public class Popovers : Scenario private EventLog? _eventLog; private Button? _activateButton; private TextField? _resultTextField; + private TextField? _initialValueTextField; private readonly Dictionary _popoverInstances = []; @@ -83,15 +84,23 @@ public override void Main () _activateButton.Accepting += ShowPopover; - _resultTextField = new TextField + _initialValueTextField = new TextField { Y = Pos.Bottom (_activateButton), Width = Dim.Fill (), + Title = "_Initial Value (TrySetValueFromString)", + BorderStyle = LineStyle.Dotted + }; + + _resultTextField = new TextField + { + Y = Pos.Bottom (_initialValueTextField), + Width = Dim.Fill (), ReadOnly = true, Title = "Result", BorderStyle = LineStyle.Dotted }; - _popoverTargetFrame.Add (_activateButton, _resultTextField); + _popoverTargetFrame.Add (_activateButton, _initialValueTextField, _resultTextField); _eventLog.SetViewToLog (window); window.Add (_viewListView, _popoverTargetFrame, _eventLog); @@ -135,6 +144,22 @@ private void ShowPopover (object? sender, CommandEventArgs args) args.Handled = true; + // Apply initial value via IValue.TrySetValueFromString if the TextField has content + if (!string.IsNullOrEmpty (_initialValueTextField?.Text)) + { + View? contentView = (popover as View)?.SubViews.FirstOrDefault (); + + if (contentView is IValue iValue) + { + bool success = iValue.TrySetValueFromString (_initialValueTextField.Text); + _eventLog?.Log ($"TrySetValueFromString(\"{_initialValueTextField.Text}\") on {contentView.GetType ().Name}: {(success ? "succeeded" : "failed")}"); + } + else + { + _eventLog?.Log ($"Content view does not implement IValue; cannot apply initial value."); + } + } + try { Point idealPosition = _resultTextField?.FrameToScreen ().Location ?? Point.Empty; diff --git a/Terminal.Gui/ViewBase/IValue.cs b/Terminal.Gui/ViewBase/IValue.cs index e951ba091e..8859b55712 100644 --- a/Terminal.Gui/ViewBase/IValue.cs +++ b/Terminal.Gui/ViewBase/IValue.cs @@ -27,6 +27,30 @@ public interface IValue /// Raised when has changed, providing the value as an un-typed object. /// event EventHandler>? ValueChangedUntyped; + + /// + /// Attempts to set by parsing the supplied string. + /// + /// The string representation of the value to set. + /// + /// if was successfully parsed and assigned; + /// if the value type cannot be parsed from a string or parsing failed. + /// + /// + /// + /// The default implementation supports: + /// + /// + /// values (assigned directly). + /// Any type implementing (e.g. , , , , , , ). + /// wrappers around any of the above. + /// types (case-insensitive). + /// + /// + /// Views may override this method to provide custom parsing logic. + /// + /// + bool TrySetValueFromString (string input); } /// @@ -71,4 +95,22 @@ public interface IValue : IValue /// object? IValue.GetValue () => Value; + + /// + /// + /// The default implementation handles , types implementing + /// , types, and + /// wrappers around any of those. Views with bespoke parsing should override this method. + /// + bool IValue.TrySetValueFromString (string input) + { + if (!IValueParser.TryParseValue (input, out TValue? parsed)) + { + return false; + } + + Value = parsed; + + return true; + } } diff --git a/Terminal.Gui/ViewBase/IValueParser.cs b/Terminal.Gui/ViewBase/IValueParser.cs new file mode 100644 index 0000000000..e6c4a53283 --- /dev/null +++ b/Terminal.Gui/ViewBase/IValueParser.cs @@ -0,0 +1,98 @@ +using System.Diagnostics.CodeAnalysis; +using System.Reflection; + +namespace Terminal.Gui.ViewBase; + +/// +/// Helpers for parsing string input into the value type exposed by . +/// +/// +/// +/// Used by the default implementation of and by +/// derived classes that implement multiple IValue<T> interfaces and need to +/// disambiguate the diamond-inherited default implementation. +/// +/// +public static class IValueParser +{ + /// + /// Attempts to parse into a value of type . + /// + /// The target value type. May be a reference type, value type, or . + /// The string representation to parse. + /// When this method returns , contains the parsed value; otherwise . + /// + /// if was parsed successfully; otherwise . + /// + /// + /// Supported types: + /// + /// (assigned directly). + /// Any type implementing via reflection on the static TryParse(string, IFormatProvider?, out T) method. + /// wrappers around any of the above. + /// types (case-insensitive). + /// + /// + [UnconditionalSuppressMessage ( + "Trimming", + "IL2090:'this' argument does not satisfy 'DynamicallyAccessedMemberTypes.PublicMethods' in call to target method.", + Justification = "Reflective lookup of static TryParse(string, IFormatProvider?, out T) is intentional. Callers using AOT/trimming with custom IParsable types should preserve the TryParse method via DynamicDependency or equivalent.")] + public static bool TryParseValue (string input, out TValue? parsed) + { + parsed = default; + + if (input is null) + { + return false; + } + + Type valueType = typeof (TValue); + Type underlyingType = Nullable.GetUnderlyingType (valueType) ?? valueType; + + // string passthrough + if (underlyingType == typeof (string)) + { + parsed = (TValue?)(object?)input; + + return true; + } + + // Enum support (case-insensitive) + if (underlyingType.IsEnum) + { + if (Enum.TryParse (underlyingType, input, ignoreCase: true, out object? parsedEnum)) + { + parsed = (TValue?)parsedEnum; + + return true; + } + + return false; + } + + // IParsable.TryParse(string, IFormatProvider?, out T) via reflection + MethodInfo? tryParse = underlyingType.GetMethod ( + "TryParse", + BindingFlags.Public | BindingFlags.Static, + binder: null, + types: [typeof (string), typeof (IFormatProvider), underlyingType.MakeByRefType ()], + modifiers: null); + + if (tryParse is null) + { + return false; + } + + object?[] args = [input, null, null]; + var success = (bool)tryParse.Invoke (null, args)!; + + if (!success) + { + return false; + } + + parsed = (TValue?)args [2]; + + return true; + } +} diff --git a/Terminal.Gui/Views/ListView/ListViewT.cs b/Terminal.Gui/Views/ListView/ListViewT.cs index c9ddb252b4..d77298fc9b 100644 --- a/Terminal.Gui/Views/ListView/ListViewT.cs +++ b/Terminal.Gui/Views/ListView/ListViewT.cs @@ -122,6 +122,23 @@ public void SetSource (ObservableCollection? source) /// object? IValue.GetValue () => Value; + /// + /// + /// Resolves the diamond between 's IValue<int?> and + /// this view's IValue<T> by parsing into . + /// + bool IValue.TrySetValueFromString (string input) + { + if (!IValueParser.TryParseValue (input, out T? parsed)) + { + return false; + } + + Value = parsed; + + return true; + } + /// /// Gets or sets the currently selected object. /// This is a convenience property that is an alias for . diff --git a/Terminal.Gui/Views/Menu/MenuItem.cs b/Terminal.Gui/Views/Menu/MenuItem.cs index eab6a75e64..ab67500d43 100644 --- a/Terminal.Gui/Views/Menu/MenuItem.cs +++ b/Terminal.Gui/Views/Menu/MenuItem.cs @@ -147,6 +147,19 @@ public Menu? SubMenu /// public object GetValue () => Title; + /// + public bool TrySetValueFromString (string input) + { + if (input is null) + { + return false; + } + + Title = input; + + return true; + } + /// event EventHandler>? IValue.ValueChangedUntyped { diff --git a/Terminal.Gui/Views/Selectors/OptionSelector.cs b/Terminal.Gui/Views/Selectors/OptionSelector.cs index 45edda196f..aabcc05883 100644 --- a/Terminal.Gui/Views/Selectors/OptionSelector.cs +++ b/Terminal.Gui/Views/Selectors/OptionSelector.cs @@ -175,6 +175,22 @@ public override void UpdateChecked () Debug.Assert (SubViews.OfType ().Count (cb => cb.Value == CheckState.Checked) <= 1); } + /// + protected override void OnValueChanged (int? value, int? previousValue) + { + if (value is null || Values is null) + { + return; + } + + int index = Values.IndexOf (v => v == value); + + if (index >= 0 && index < SubViews.OfType ().Count ()) + { + FocusedItem = index; + } + } + /// /// Gets or sets the index for the focused item. The active item may or may not be /// the selected diff --git a/Terminal.Gui/Views/TextInput/DateEditor.cs b/Terminal.Gui/Views/TextInput/DateEditor.cs index 5b428e66b1..7cac78f36e 100644 --- a/Terminal.Gui/Views/TextInput/DateEditor.cs +++ b/Terminal.Gui/Views/TextInput/DateEditor.cs @@ -141,6 +141,23 @@ public DateTimeFormatInfo Format object? IValue.GetValue () => Value; + /// + /// + /// Resolves the diamond between 's IValue<string> + /// and this view's IValue<DateTime> by parsing into . + /// + bool IValue.TrySetValueFromString (string input) + { + if (!IValueParser.TryParseValue (input, out DateTime parsed)) + { + return false; + } + + Value = parsed; + + return true; + } + /// /// Synchronizes the backing field when the base class /// property changes programmatically. diff --git a/Terminal.Gui/Views/TextInput/TimeEditor.cs b/Terminal.Gui/Views/TextInput/TimeEditor.cs index 366ab7f7b8..4f995754df 100644 --- a/Terminal.Gui/Views/TextInput/TimeEditor.cs +++ b/Terminal.Gui/Views/TextInput/TimeEditor.cs @@ -157,6 +157,23 @@ public DateTimeFormatInfo Format object? IValue.GetValue () => Value; + /// + /// + /// Resolves the diamond between 's IValue<string> + /// and this view's IValue<TimeSpan> by parsing into . + /// + bool IValue.TrySetValueFromString (string input) + { + if (!IValueParser.TryParseValue (input, out TimeSpan parsed)) + { + return false; + } + + Value = parsed; + + return true; + } + /// /// Synchronizes the backing field when the base class /// property changes programmatically. diff --git a/Tests/UnitTestsParallelizable/ViewBase/IValueTests.cs b/Tests/UnitTestsParallelizable/ViewBase/IValueTests.cs index 536a29cae8..bbe1a73c44 100644 --- a/Tests/UnitTestsParallelizable/ViewBase/IValueTests.cs +++ b/Tests/UnitTestsParallelizable/ViewBase/IValueTests.cs @@ -415,4 +415,244 @@ public void SenderOfEvents_IsCorrect () Assert.Equal (view, changingSender); Assert.Equal (view, changedSender); } + + #region TrySetValueFromString + + // Copilot + [Fact] + public void TrySetValueFromString_Int_ParsesValid () + { + TestIntValueView view = new (); + + bool result = ((IValue)view).TrySetValueFromString ("42"); + + Assert.True (result); + Assert.Equal (42, view.Value); + } + + // Copilot + [Fact] + public void TrySetValueFromString_Int_ReturnsFalse_ForInvalid () + { + TestIntValueView view = new () { Value = 7 }; + + bool result = ((IValue)view).TrySetValueFromString ("not a number"); + + Assert.False (result); + Assert.Equal (7, view.Value); + } + + // Copilot + [Fact] + public void TrySetValueFromString_String_AssignsDirectly () + { + TestStringValueView view = new (); + + bool result = ((IValue)view).TrySetValueFromString ("hello"); + + Assert.True (result); + Assert.Equal ("hello", view.Value); + } + + // Copilot - Test view implementing IValue to verify IParsable parsing. + private sealed class TestDateValueView : View, IValue + { + private DateTime _value; + + public DateTime Value + { + get => _value; + set => + CWPPropertyHelper.ChangeProperty (this, + ref _value, + value, + args => false, + ValueChanging, + _ => { }, + args => ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs (args.OldValue, args.NewValue)), + ValueChanged, + out _); + } + + public event EventHandler>? ValueChanging; + public event EventHandler>? ValueChanged; + public event EventHandler>? ValueChangedUntyped; + } + + // Copilot + [Fact] + public void TrySetValueFromString_DateTime_ParsesViaIParsable () + { + TestDateValueView view = new (); + + bool result = ((IValue)view).TrySetValueFromString ("2024-06-15"); + + Assert.True (result); + Assert.Equal (new DateTime (2024, 6, 15), view.Value); + } + + // Copilot - Test view implementing IValue to verify nullable enum parsing. + private sealed class TestEnumValueView : View, IValue + { + private DayOfWeek? _value; + + public DayOfWeek? Value + { + get => _value; + set => + CWPPropertyHelper.ChangeProperty (this, + ref _value, + value, + args => false, + ValueChanging, + _ => { }, + args => ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs (args.OldValue, args.NewValue)), + ValueChanged, + out _); + } + + public event EventHandler>? ValueChanging; + public event EventHandler>? ValueChanged; + public event EventHandler>? ValueChangedUntyped; + } + + // Copilot + [Theory] + [InlineData ("Monday", DayOfWeek.Monday)] + [InlineData ("friday", DayOfWeek.Friday)] // case-insensitive + public void TrySetValueFromString_NullableEnum_Parses (string input, DayOfWeek expected) + { + TestEnumValueView view = new (); + + bool result = ((IValue)view).TrySetValueFromString (input); + + Assert.True (result); + Assert.Equal (expected, view.Value); + } + + // Copilot + [Fact] + public void TrySetValueFromString_Enum_ReturnsFalse_ForInvalid () + { + TestEnumValueView view = new () { Value = DayOfWeek.Sunday }; + + bool result = ((IValue)view).TrySetValueFromString ("NotADay"); + + Assert.False (result); + Assert.Equal (DayOfWeek.Sunday, view.Value); + } + + // Copilot - Test view implementing IValue to verify unsupported type handling. + private sealed class TestObjectValueView : View, IValue + { + private object? _value; + + public object? Value + { + get => _value; + set => + CWPPropertyHelper.ChangeProperty (this, + ref _value, + value, + args => false, + ValueChanging, + _ => { }, + args => ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs (args.OldValue, args.NewValue)), + ValueChanged, + out _); + } + + public event EventHandler>? ValueChanging; + public event EventHandler>? ValueChanged; + public event EventHandler>? ValueChangedUntyped; + } + + // Copilot + [Fact] + public void TrySetValueFromString_UnsupportedType_ReturnsFalse () + { + TestObjectValueView view = new (); + + bool result = ((IValue)view).TrySetValueFromString ("anything"); + + Assert.False (result); + Assert.Null (view.Value); + } + + // Copilot + [Fact] + public void TrySetValueFromString_RaisesValueChanged () + { + TestIntValueView view = new (); + var raised = 0; + view.ValueChanged += (_, _) => raised++; + + bool result = ((IValue)view).TrySetValueFromString ("99"); + + Assert.True (result); + Assert.Equal (1, raised); + Assert.Equal (99, view.Value); + } + + // Copilot - Verifies TrySetValueFromString integration with real Views. + [Fact] + public void TrySetValueFromString_NumericUpDown_Int_Parses () + { + NumericUpDown upDown = new (); + + bool result = ((IValue)upDown).TrySetValueFromString ("123"); + + Assert.True (result); + Assert.Equal (123, upDown.Value); + } + + // Copilot + [Fact] + public void TrySetValueFromString_ColorPicker_ParsesHexColor () + { + ColorPicker picker = new (); + + bool result = ((IValue)picker).TrySetValueFromString ("#FF0000"); + + Assert.True (result); + Assert.Equal (Color.Red, picker.SelectedColor); + } + + // Copilot + [Fact] + public void TrySetValueFromString_ColorPicker_ReturnsFalse_ForInvalid () + { + ColorPicker picker = new (); + + bool result = ((IValue)picker).TrySetValueFromString ("not-a-color"); + + Assert.False (result); + } + + // Copilot + [Fact] + public void TrySetValueFromString_DatePicker_ParsesIsoDate () + { + DatePicker picker = new (); + + bool result = ((IValue)picker).TrySetValueFromString ("2024-12-25"); + + Assert.True (result); + Assert.Equal (new DateTime (2024, 12, 25), picker.Value); + } + + // Copilot + [Fact] + public void TrySetValueFromString_MenuItem_SetsTitle () + { + MenuItem item = new () { Title = "Old" }; + + bool result = ((IValue)item).TrySetValueFromString ("New"); + + Assert.True (result); + Assert.Equal ("New", item.Title); + Assert.Equal ("New", item.GetValue ()); + } + + #endregion TrySetValueFromString }