| `int` | Selected item index |
diff --git a/docfx/docs/views.md b/docfx/docs/views.md
index 25fd61fc36..35a81b1c46 100644
--- a/docfx/docs/views.md
+++ b/docfx/docs/views.md
@@ -930,9 +930,9 @@ Fully featured multi-line text editor
-## [TimeField](xref:Terminal.Gui.Views.TimeField)
+## [TimeEditor](xref:Terminal.Gui.Views.TimeEditor)
-Provides time editing functionality with specialized cursor behavior for time entry.
+Provides time editing functionality using `TextValidateField` with culture-aware formatting.
From 5e7e2088b2eb7253db1427a699db31e797324d6a Mon Sep 17 00:00:00 2001
From: Tig
Date: Sat, 7 Mar 2026 16:14:10 -0700
Subject: [PATCH 17/26] Refactor TimeEditor and providers, remove VerifyChar
method
Refactored TimeEditor, TimeTextProvider, NetMaskedTextProvider, and TextRegexProvider for clarity and consistency. Removed the VerifyChar method from ITextValidateProvider and all implementations, simplifying the provider interface. Standardized event raising and improved handling of AM/PM, manual parsing, and pattern normalization in TimeTextProvider. Updated TextValidateField drawing and key handling logic for simplicity. Modernized and clarified all related tests to match the new API and style. Made various code style improvements and updated documentation throughout.
---
.../UICatalog/Scenarios/TextInputControls.cs | 9 +-
.../Views/TextInput/ITextValidateProvider.cs | 14 -
.../Views/TextInput/NetMaskedTextProvider.cs | 46 +--
.../Views/TextInput/TextEditingLineStatus.cs | 3 +-
.../Views/TextInput/TextRegexProvider.cs | 56 +--
.../Views/TextInput/TextValidateField.cs | 21 +-
Terminal.Gui/Views/TextInput/TimeEditor.cs | 11 +-
.../Views/TextInput/TimeTextProvider.cs | 183 ++++-----
.../Views/TextValidateFieldTests.cs | 1 +
.../Views/TimeEditorTests.cs | 380 ++++++++----------
10 files changed, 305 insertions(+), 419 deletions(-)
diff --git a/Examples/UICatalog/Scenarios/TextInputControls.cs b/Examples/UICatalog/Scenarios/TextInputControls.cs
index f2b03e06d4..b47edd7d44 100644
--- a/Examples/UICatalog/Scenarios/TextInputControls.cs
+++ b/Examples/UICatalog/Scenarios/TextInputControls.cs
@@ -247,15 +247,10 @@ void TextViewDrawContent (object? sender, DrawEventArgs e) =>
label = new Label { Text = "T_imeEditor:", Y = Pos.Top (dateField), X = Pos.Right (labelMirroringDateField) + 5 };
win.Add (label);
- _timeEditor = new TimeEditor ()
- {
- X = Pos.Right (label) + 1,
- Y = Pos.Top (dateField),
- Value = DateTime.Now.TimeOfDay
- };
+ _timeEditor = new TimeEditor { X = Pos.Right (label) + 1, Y = Pos.Top (dateField), Value = DateTime.Now.TimeOfDay };
win.Add (_timeEditor);
- _labelMirroringTimeEditor = new Label ()
+ _labelMirroringTimeEditor = new Label
{
X = Pos.Right (_timeEditor) + 1,
Y = Pos.Top (_timeEditor),
diff --git a/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs b/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs
index 69c23cd30d..e5f67aa267 100644
--- a/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs
+++ b/Terminal.Gui/Views/TextInput/ITextValidateProvider.cs
@@ -1,7 +1,3 @@
-
-
-using System.ComponentModel;
-
namespace Terminal.Gui.Views;
/// TextValidateField Providers Interface. All TextValidateField are created with a ITextValidateProvider.
@@ -53,16 +49,6 @@ public interface ITextValidateProvider
/// true if the character was successfully inserted, otherwise false.
bool InsertAt (char ch, int pos);
- ///
- /// Tests whether the specified character would be set successfully at the specified position.
- ///
- public bool VerifyChar (char input, int position, out MaskedTextResultHint hint);
-
- /// Method that invoke the event if it's defined.
- /// The previous text before replaced.
- /// Returns the
- void OnTextChanged (EventArgs oldValue);
-
///
/// Changed event, raised when the text has changed.
///
diff --git a/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs b/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs
index 49b393cb62..080d9cb03f 100644
--- a/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs
+++ b/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs
@@ -31,10 +31,8 @@ public string Mask
get => _provider!.Mask;
set
{
- string current = _provider != null
- ? _provider.ToString (false, false)
- : string.Empty;
- _provider = new (value == string.Empty ? "&&&&&&" : value);
+ string current = _provider != null ? _provider.ToString (false, false) : string.Empty;
+ _provider = new MaskedTextProvider (value == string.Empty ? "&&&&&&" : value);
if (!string.IsNullOrEmpty (current))
{
@@ -47,11 +45,7 @@ public string Mask
public event EventHandler>? TextChanged;
///
- public string Text
- {
- get => _provider!.ToString ();
- set => _provider!.Set (value);
- }
+ public string Text { get => _provider!.ToString (); set => _provider!.Set (value); }
///
public bool IsValid => _provider!.MaskCompleted;
@@ -86,20 +80,11 @@ public int Cursor (int pos)
}
///
- public int CursorStart ()
- {
- return _provider!.IsEditPosition (0)
- ? 0
- : _provider.FindEditPositionFrom (0, true);
- }
+ public int CursorStart () => _provider!.IsEditPosition (0) ? 0 : _provider.FindEditPositionFrom (0, true);
///
- public int CursorEnd ()
- {
- return _provider!.IsEditPosition (_provider.Length - 1)
- ? _provider.Length - 1
- : _provider.FindEditPositionFrom (_provider.Length, false);
- }
+ public int CursorEnd () =>
+ _provider!.IsEditPosition (_provider.Length - 1) ? _provider.Length - 1 : _provider.FindEditPositionFrom (_provider.Length, false);
///
public int CursorLeft (int pos)
@@ -125,7 +110,7 @@ public bool Delete (int pos)
if (result)
{
- OnTextChanged (new (in oldValue));
+ OnTextChanged (new EventArgs (in oldValue));
}
return result;
@@ -139,18 +124,17 @@ public bool InsertAt (char ch, int pos)
if (result)
{
- OnTextChanged (new (in oldValue));
+ OnTextChanged (new EventArgs (in oldValue));
}
return result;
}
- ///
- public bool VerifyChar (char input, int position, out MaskedTextResultHint hint)
- {
- return _provider!.VerifyChar (input, position, out hint);
- }
-
- ///
- public void OnTextChanged (EventArgs args) { TextChanged?.Invoke (this, args); }
+ ///
+ /// Raises the TextChanged event to notify subscribers that the text has changed.
+ ///
+ /// Call this method to trigger the TextChanged event when the text value is updated. Subscribers
+ /// can use this event to respond to changes in the text.
+ /// An EventArgs object that contains the event data for the text change.
+ public void OnTextChanged (EventArgs args) => TextChanged?.Invoke (this, args);
}
diff --git a/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs b/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs
index 0259b15ee3..cd757e60ff 100644
--- a/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs
+++ b/Terminal.Gui/Views/TextInput/TextEditingLineStatus.cs
@@ -1,4 +1,3 @@
-
///
/// Represents the status of a line during text editing operations in a .
///
@@ -8,8 +7,8 @@
/// and maintaining the state of text modifications. Each value describes a specific type of change or state for a
/// line.
///
+
// ReSharper disable once CheckNamespace
-#nullable disable
public enum TextEditingLineStatus
{
///
diff --git a/Terminal.Gui/Views/TextInput/TextRegexProvider.cs b/Terminal.Gui/Views/TextInput/TextRegexProvider.cs
index a64a494bea..e70b1b3a4a 100644
--- a/Terminal.Gui/Views/TextInput/TextRegexProvider.cs
+++ b/Terminal.Gui/Views/TextInput/TextRegexProvider.cs
@@ -1,4 +1,3 @@
-using System.ComponentModel;
using System.Text.RegularExpressions;
namespace Terminal.Gui.Views;
@@ -98,12 +97,13 @@ public int CursorRight (int pos)
///
public bool Delete (int pos)
{
- if (_text.Count > 0 && pos < _text.Count)
+ if (_text.Count <= 0 || pos >= _text.Count)
{
- string oldValue = Text;
- _text.RemoveAt (pos);
- OnTextChanged (new (in oldValue));
+ return true;
}
+ string oldValue = Text;
+ _text.RemoveAt (pos);
+ OnTextChanged (new EventArgs (in oldValue));
return true;
}
@@ -114,56 +114,34 @@ public bool InsertAt (char ch, int pos)
List aux = _text.ToList ();
aux.Insert (pos, (Rune)ch);
- if (Validate (aux) || !ValidateOnInput)
- {
- string oldValue = Text;
- _text.Insert (pos, (Rune)ch);
- OnTextChanged (new (in oldValue));
-
- return true;
- }
-
- return false;
- }
-
- ///
- public bool VerifyChar (char input, int position, out MaskedTextResultHint hint)
- {
- if (position < 0 || position > _text.Count)
+ if (!Validate (aux) && ValidateOnInput)
{
- hint = MaskedTextResultHint.PositionOutOfRange;
-
return false;
}
+ string oldValue = Text;
+ _text.Insert (pos, (Rune)ch);
+ OnTextChanged (new EventArgs (in oldValue));
- List aux = _text.ToList ();
- aux.Insert (position, (Rune)input);
-
- if (Validate (aux))
- {
- hint = MaskedTextResultHint.Success;
-
- return true;
- }
- hint = MaskedTextResultHint.InvalidInput;
-
- return false;
+ return true;
}
- ///
+ ///
+ /// Raises the TextChanged event to notify subscribers that the text has changed.
+ ///
+ /// An EventArgs object that contains the event data representing the new text value.
public void OnTextChanged (EventArgs args) => TextChanged?.Invoke (this, args);
/// Compiles the regex pattern for validation./>
- private void CompileMask () => _regex = new (StringExtensions.ToString (_pattern), RegexOptions.Compiled);
+ private void CompileMask () => _regex = new Regex (StringExtensions.ToString (_pattern), RegexOptions.Compiled);
private void SetupText ()
{
- if (_text is { } && IsValid)
+ if (_text is not null && IsValid)
{
return;
}
- _text = new ();
+ _text = [];
}
private bool Validate (List text)
diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidateField.cs
index 4ebcfdb072..d7153d5671 100644
--- a/Terminal.Gui/Views/TextInput/TextValidateField.cs
+++ b/Terminal.Gui/Views/TextInput/TextValidateField.cs
@@ -178,7 +178,7 @@ protected override bool OnDrawingContent (DrawContext? context)
return true;
}
- VisualRole role = VisualRole.Editable;
+ var role = VisualRole.Editable;
Attribute textColor = IsValid ? GetAttributeForRole (role) : SchemeManager.GetScheme (Schemes.Error).GetAttributeForRole (role);
(int marginLeft, int marginRight) = GetMargins (Viewport.Width);
@@ -210,12 +210,13 @@ protected override bool OnDrawingContent (DrawContext? context)
AddRune ((Rune)' ');
}
- if (HasFocus && _provider.DisplayText.Length > 0 && InsertionPoint < _provider.DisplayText.Length)
+ if (!HasFocus || _provider.DisplayText.Length <= 0 || InsertionPoint >= _provider.DisplayText.Length)
{
- SetAttributeForRole (VisualRole.Focus);
- Move (InsertionPoint + marginLeft, 0);
- AddRune ((Rune)_provider.DisplayText [InsertionPoint]);
+ return true;
}
+ SetAttributeForRole (VisualRole.Focus);
+ Move (InsertionPoint + marginLeft, 0);
+ AddRune ((Rune)_provider.DisplayText [InsertionPoint]);
return true;
}
@@ -237,14 +238,14 @@ protected override bool OnKeyDownNotHandled (Key key)
bool inserted = _provider.InsertAt ((char)rune.Value, InsertionPoint);
- if (inserted)
+ if (!inserted)
{
- CursorRight ();
-
- return true;
+ return false;
}
+ CursorRight ();
+
+ return true;
- return false;
}
/// Delete char at cursor position - 1, moving the cursor.
diff --git a/Terminal.Gui/Views/TextInput/TimeEditor.cs b/Terminal.Gui/Views/TextInput/TimeEditor.cs
index a190df19b2..b26f13e9b9 100644
--- a/Terminal.Gui/Views/TextInput/TimeEditor.cs
+++ b/Terminal.Gui/Views/TextInput/TimeEditor.cs
@@ -57,7 +57,7 @@ namespace Terminal.Gui.Views;
public class TimeEditor : TextValidateField, IValue, IDesignable
{
private TimeTextProvider TimeProvider => (TimeTextProvider)Provider!;
- private TimeSpan _lastKnownValue = TimeSpan.Zero;
+ private TimeSpan _lastKnownValue;
///
/// Initializes a new instance of the class.
@@ -65,10 +65,10 @@ public class TimeEditor : TextValidateField, IValue, IDesignable
public TimeEditor ()
{
Provider = new TimeTextProvider ();
+
// Add one so there is always a blank cell after the last editable character for the cursor.
Width = Dim.Auto (minimumContentDim: Provider!.DisplayText.Length + 1);
-
// Subscribe to provider's text changed to raise our value events
TimeProvider.TextChanged += (_, _) => RaiseValueChangedEvents ();
@@ -94,6 +94,7 @@ public DateTimeFormatInfo Format
set
{
TimeProvider.Format = value;
+
// Add one so there is always a blank cell after the last editable character for the cursor.
Width = TimeProvider.DisplayText.Length + 1;
SetNeedsDraw ();
@@ -138,7 +139,7 @@ public TimeSpan Value
// Update _lastKnownValue before setting to prevent double-firing from TextChanged handler
_lastKnownValue = value;
-
+
TimeProvider.TimeValue = value;
Text = TimeProvider.Text;
@@ -161,7 +162,7 @@ public TimeSpan Value
public event EventHandler>? ValueChangedUntyped;
///
- object? IValue.GetValue () => Value;
+ object IValue.GetValue () => Value;
///
/// Called when the is changing.
@@ -198,7 +199,6 @@ private void RaiseValueChangedEvents ()
return;
}
-
// Raise ValueChanging to allow cancellation
ValueChangingEventArgs changingArgs = new (_lastKnownValue, currentValue);
@@ -224,7 +224,6 @@ private void RaiseValueChangedEvents ()
return;
}
-
ValueChangedEventArgs changedArgs = new (_lastKnownValue, currentValue);
_lastKnownValue = currentValue;
OnValueChanged (changedArgs);
diff --git a/Terminal.Gui/Views/TextInput/TimeTextProvider.cs b/Terminal.Gui/Views/TextInput/TimeTextProvider.cs
index e78fa59ebf..68aecc04a5 100644
--- a/Terminal.Gui/Views/TextInput/TimeTextProvider.cs
+++ b/Terminal.Gui/Views/TextInput/TimeTextProvider.cs
@@ -47,7 +47,7 @@ public class TimeTextProvider : ITextValidateProvider
private bool _is12Hour;
private bool _hasAmPm;
private int _fieldLength;
- private HashSet _separatorPositions = [];
+ private readonly HashSet _separatorPositions = [];
private int _amPmPosition = -1;
private bool _isPm;
@@ -71,7 +71,7 @@ public DateTimeFormatInfo Format
_format = value;
_separator = value.TimeSeparator;
AnalyzePattern ();
- OnTextChanged (new (in string.Empty));
+ OnTextChanged (new EventArgs (in string.Empty));
}
}
@@ -97,17 +97,17 @@ public string Text
get => FormatTimeValue ();
set
{
- if (TryParseTimeValue (value, out TimeSpan parsedValue))
+ if (!TryParseTimeValue (value, out TimeSpan parsedValue))
{
- string oldValue = Text;
- _timeValue = parsedValue;
- _isPm = _timeValue.Hours >= 12;
-
+ return;
+ }
+ string oldValue = Text;
+ _timeValue = parsedValue;
+ _isPm = _timeValue.Hours >= 12;
- if (oldValue != Text)
- {
- OnTextChanged (new (in oldValue));
- }
+ if (oldValue != Text)
+ {
+ OnTextChanged (new EventArgs (in oldValue));
}
}
}
@@ -234,21 +234,20 @@ public bool Delete (int pos)
// Replace digit with '0'
string currentText = FormatTimeValue ();
- if (pos >= 0 && pos < currentText.Length && char.IsDigit (currentText [pos]))
+ if (pos < 0 || pos >= currentText.Length || !char.IsDigit (currentText [pos]))
{
- StringBuilder sb = new (currentText);
- sb [pos] = '0';
-
- if (TryParseTimeValue (sb.ToString (), out TimeSpan newValue))
- {
- _timeValue = newValue;
- OnTextChanged (new (in oldValue));
+ return false;
+ }
+ StringBuilder sb = new (currentText) { [pos] = '0' };
- return true;
- }
+ if (!TryParseTimeValue (sb.ToString (), out TimeSpan newValue))
+ {
+ return false;
}
+ _timeValue = newValue;
+ OnTextChanged (new EventArgs (in oldValue));
- return false;
+ return true;
}
///
@@ -259,26 +258,24 @@ public bool InsertAt (char ch, int pos)
// Handle AM/PM toggle
if (_hasAmPm && pos == _amPmPosition)
{
- if (char.ToUpperInvariant (ch) == 'A' || char.ToUpperInvariant (ch) == 'P')
+ if (char.ToUpperInvariant (ch) != 'A' && char.ToUpperInvariant (ch) != 'P')
{
- _isPm = char.ToUpperInvariant (ch) == 'P';
-
- // Update the time value hours to reflect AM/PM change
- int hours = _timeValue.Hours % 12;
-
- if (_isPm && hours < 12)
- {
- hours += 12;
- }
-
+ return false;
+ }
+ _isPm = char.ToUpperInvariant (ch) == 'P';
- _timeValue = new TimeSpan (hours, _timeValue.Minutes, _timeValue.Seconds);
- OnTextChanged (new (in oldValue));
+ // Update the time value hours to reflect AM/PM change
+ int hours = _timeValue.Hours % 12;
- return true;
+ if (_isPm && hours < 12)
+ {
+ hours += 12;
}
- return false;
+ _timeValue = new TimeSpan (hours, _timeValue.Minutes, _timeValue.Seconds);
+ OnTextChanged (new EventArgs (in oldValue));
+
+ return true;
}
// Only accept digits for time positions
@@ -290,52 +287,28 @@ public bool InsertAt (char ch, int pos)
// Replace digit at position
string currentText = FormatTimeValue ();
- if (pos >= 0 && pos < currentText.Length)
- {
- StringBuilder sb = new (currentText);
- sb [pos] = ch;
-
- if (TryParseTimeValue (sb.ToString (), out TimeSpan newValue))
- {
- _timeValue = newValue;
- OnTextChanged (new (in oldValue));
-
- return true;
- }
- }
-
- return false;
- }
-
- ///
- public bool VerifyChar (char input, int position, out MaskedTextResultHint hint)
- {
- hint = MaskedTextResultHint.Success;
-
- // Handle AM/PM toggle
- if (_hasAmPm && position == _amPmPosition)
+ if (pos < 0 || pos >= currentText.Length)
{
- if (char.ToUpperInvariant (input) == 'A' || char.ToUpperInvariant (input) == 'P')
- {
- return true;
- }
- hint = MaskedTextResultHint.InvalidInput;
-
return false;
}
+ StringBuilder sb = new (currentText) { [pos] = ch };
- // Only accept digits for time positions
- if (!char.IsDigit (input))
+ if (!TryParseTimeValue (sb.ToString (), out TimeSpan newValue))
{
- hint = MaskedTextResultHint.InvalidInput;
-
return false;
}
+ _timeValue = newValue;
+ OnTextChanged (new EventArgs (in oldValue));
return true;
}
- ///
+ ///
+ /// Raises the TextChanged event to notify subscribers that the text has changed.
+ ///
+ /// Call this method to trigger the TextChanged event when the text value is updated. Subscribers
+ /// can use this event to respond to changes in the text.
+ /// An EventArgs object that contains the event data for the text change.
public void OnTextChanged (EventArgs args) => TextChanged?.Invoke (this, args);
///
@@ -368,16 +341,17 @@ private void AnalyzePattern ()
}
// Find AM/PM position
- if (_hasAmPm)
+ if (!_hasAmPm)
{
- string amDesignator = _format.AMDesignator;
- string pmDesignator = _format.PMDesignator;
+ return;
+ }
+ string amDesignator = _format.AMDesignator;
+ string pmDesignator = _format.PMDesignator;
- int amIndex = formatted.IndexOf (amDesignator, StringComparison.Ordinal);
- int pmIndex = formatted.IndexOf (pmDesignator, StringComparison.Ordinal);
+ int amIndex = formatted.IndexOf (amDesignator, StringComparison.Ordinal);
+ int pmIndex = formatted.IndexOf (pmDesignator, StringComparison.Ordinal);
- _amPmPosition = Math.Max (amIndex, pmIndex);
- }
+ _amPmPosition = Math.Max (amIndex, pmIndex);
}
///
@@ -417,30 +391,30 @@ private string FormatTimeValue ()
DateTime dt = DateTime.Today.Add (_timeValue);
// For 12-hour format, adjust the hours if needed
- if (_is12Hour && _hasAmPm)
+ if (!_is12Hour || !_hasAmPm)
{
- int hours = _timeValue.Hours % 12;
-
- if (hours == 0)
- {
- hours = 12;
- }
+ return dt.ToString (_normalizedPattern, _format);
+ }
+ int hours = _timeValue.Hours % 12;
- // Convert to 24-hour format for DateTime construction
- int hours24;
+ if (hours == 0)
+ {
+ hours = 12;
+ }
- if (_isPm)
- {
- hours24 = hours == 12 ? 12 : hours + 12;
- }
- else
- {
- hours24 = hours == 12 ? 0 : hours;
- }
+ // Convert to 24-hour format for DateTime construction
+ int hours24;
- dt = new DateTime (BASE_YEAR, BASE_MONTH, BASE_DAY, hours24, _timeValue.Minutes, _timeValue.Seconds);
+ if (_isPm)
+ {
+ hours24 = hours == 12 ? 12 : hours + 12;
+ }
+ else
+ {
+ hours24 = hours == 12 ? 0 : hours;
}
+ dt = new DateTime (BASE_YEAR, BASE_MONTH, BASE_DAY, hours24, _timeValue.Minutes, _timeValue.Seconds);
return dt.ToString (_normalizedPattern, _format);
}
@@ -460,15 +434,15 @@ private bool TryParseTimeValue (string text, out TimeSpan result)
text = text.Trim ();
// Try to parse using the current pattern
- if (DateTime.TryParseExact (text, _normalizedPattern, _format, DateTimeStyles.None, out DateTime dt))
+ if (!DateTime.TryParseExact (text, _normalizedPattern, _format, DateTimeStyles.None, out DateTime dt))
{
- result = dt.TimeOfDay;
-
- return true;
+ return TryManualParse (text, out result);
}
+ result = dt.TimeOfDay;
+
+ return true;
// Fallback: try manual parsing for partial/invalid input
- return TryManualParse (text, out result);
}
///
@@ -496,12 +470,12 @@ private bool TryManualParse (string text, out TimeSpan result)
if (lastPart.EndsWith (_format.PMDesignator, StringComparison.OrdinalIgnoreCase))
{
isPm = true;
- lastPart = lastPart.Substring (0, lastPart.Length - _format.PMDesignator.Length).Trim ();
+ lastPart = lastPart [..^_format.PMDesignator.Length].Trim ();
}
else if (lastPart.EndsWith (_format.AMDesignator, StringComparison.OrdinalIgnoreCase))
{
isPm = false;
- lastPart = lastPart.Substring (0, lastPart.Length - _format.AMDesignator.Length).Trim ();
+ lastPart = lastPart [..^_format.AMDesignator.Length].Trim ();
}
parts [^1] = lastPart;
@@ -557,7 +531,6 @@ private bool TryManualParse (string text, out TimeSpan result)
result = new TimeSpan (hours, minutes, seconds);
_isPm = hours >= 12;
-
return true;
}
catch (ArgumentException)
diff --git a/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs b/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs
index 488c0340ac..752c6e06de 100644
--- a/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/TextValidateFieldTests.cs
@@ -71,6 +71,7 @@ public void Default_Width_Is_Always_Equal_To_The_Provider_DisplayText_Length ()
// A-Alphanumeric, required. a-Alphanumeric, optional.
var field = new TextValidateField { Provider = new NetMaskedTextProvider ("999 000 LLL >LLL |AAA aaa") };
field.Layout ();
+
// Width is DisplayText.Length + 1 to provide a blank cell for the cursor past the last editable char.
Assert.Equal (field.Viewport.Width, field.Provider.DisplayText.Length + 1);
Assert.NotEqual (field.Provider.DisplayText.Length, field.Provider.Text.Length);
diff --git a/Tests/UnitTestsParallelizable/Views/TimeEditorTests.cs b/Tests/UnitTestsParallelizable/Views/TimeEditorTests.cs
index 4b9f59a256..6709b3e94d 100644
--- a/Tests/UnitTestsParallelizable/Views/TimeEditorTests.cs
+++ b/Tests/UnitTestsParallelizable/Views/TimeEditorTests.cs
@@ -1,5 +1,4 @@
using System.Globalization;
-using Terminal.Gui.Tests;
using UnitTests;
namespace ViewsTests;
@@ -12,7 +11,7 @@ public void Constructor_Defaults ()
{
TimeEditor te = new ();
te.Layout ();
-
+
Assert.NotNull (te.Provider);
Assert.IsType (te.Provider);
Assert.Equal (TimeSpan.Zero, te.Value);
@@ -25,14 +24,14 @@ public void Value_Property_GetSet ()
{
TimeEditor te = new ();
TimeSpan testTime = new (14, 30, 45);
-
+
te.Value = testTime;
Assert.Equal (testTime, te.Value);
-
+
// Test setting to zero
te.Value = TimeSpan.Zero;
Assert.Equal (TimeSpan.Zero, te.Value);
-
+
// Test setting to max
te.Value = TimeSpan.FromHours (23) + TimeSpan.FromMinutes (59) + TimeSpan.FromSeconds (59);
Assert.Equal (new TimeSpan (23, 59, 59), te.Value);
@@ -42,22 +41,22 @@ public void Value_Property_GetSet ()
public void Format_Property_Changes_Width ()
{
TimeEditor te = new ();
-
+
// Set initial format explicitly to ensure deterministic test
- DateTimeFormatInfo initialFormat = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var initialFormat = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
initialFormat.LongTimePattern = "HH:mm:ss";
te.Format = initialFormat;
te.Layout ();
-
+
int initialWidth = te.Frame.Width;
Assert.True (initialWidth > 0);
-
+
// Change to a different pattern
- DateTimeFormatInfo customFormat = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var customFormat = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
customFormat.LongTimePattern = "HH:mm";
te.Format = customFormat;
te.Layout ();
-
+
// Width should change to accommodate shorter pattern
int newWidth = te.Frame.Width;
Assert.NotEqual (initialWidth, newWidth);
@@ -68,16 +67,16 @@ public void Format_Property_Changes_Width ()
public void ValueChanging_Event_Can_Cancel ()
{
TimeEditor te = new () { Value = TimeSpan.FromHours (10) };
- bool eventFired = false;
-
+ var eventFired = false;
+
te.ValueChanging += (_, e) =>
- {
- eventFired = true;
- e.Handled = true; // Cancel the change
- };
-
+ {
+ eventFired = true;
+ e.Handled = true; // Cancel the change
+ };
+
te.Value = TimeSpan.FromHours (15);
-
+
Assert.True (eventFired);
Assert.Equal (TimeSpan.FromHours (10), te.Value); // Value should not change
}
@@ -86,20 +85,20 @@ public void ValueChanging_Event_Can_Cancel ()
public void ValueChanged_Event_Fires ()
{
TimeEditor te = new () { Value = TimeSpan.FromHours (10) };
- bool eventFired = false;
+ var eventFired = false;
TimeSpan? oldValue = null;
TimeSpan? newValue = null;
-
+
te.ValueChanged += (_, e) =>
- {
- eventFired = true;
- oldValue = e.OldValue;
- newValue = e.NewValue;
- };
-
+ {
+ eventFired = true;
+ oldValue = e.OldValue;
+ newValue = e.NewValue;
+ };
+
TimeSpan expectedNewValue = TimeSpan.FromHours (15);
te.Value = expectedNewValue;
-
+
Assert.True (eventFired);
Assert.Equal (TimeSpan.FromHours (10), oldValue);
Assert.Equal (expectedNewValue, newValue);
@@ -109,19 +108,19 @@ public void ValueChanged_Event_Fires ()
public void TimeTextProvider_CursorNavigation_SkipsSeparators ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format to ensure consistent behavior
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
// CursorStart should return 0
Assert.Equal (0, provider.CursorStart ());
-
+
// For a format like "HH:mm:ss" (positions: 0,1,:,3,4,:,6,7)
// Position 2 is separator, cursor should skip it
int cursorPos = provider.CursorRight (1);
Assert.NotEqual (2, cursorPos); // Should skip position 2 (separator)
-
+
// CursorLeft from position 3 should skip separator at 2 and go to 1
cursorPos = provider.CursorLeft (3);
Assert.Equal (1, cursorPos);
@@ -131,17 +130,17 @@ public void TimeTextProvider_CursorNavigation_SkipsSeparators ()
public void TimeTextProvider_InsertAt_ReplacesDigit ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format to ensure consistent behavior
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
provider.TimeValue = TimeSpan.Zero; // "00:00:00" in 24h format
-
+
// Insert '1' at position 0 (first hour digit)
bool result = provider.InsertAt ('1', 0);
Assert.True (result);
-
+
// Check that the value was updated
string text = provider.Text;
Assert.StartsWith ("1", text.TrimStart ());
@@ -151,17 +150,17 @@ public void TimeTextProvider_InsertAt_ReplacesDigit ()
public void TimeTextProvider_Delete_ReplacesWithZero ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format to avoid culture-specific issues
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
provider.TimeValue = new TimeSpan (14, 30, 45);
-
+
// Delete at position 0 should replace with '0'
bool result = provider.Delete (0);
Assert.True (result);
-
+
// The hour should now start with 0
string text = provider.Text.Trim ();
Assert.StartsWith ("0", text);
@@ -173,19 +172,19 @@ public void TimeTextProvider_Format_Change_Updates_Pattern ()
TimeTextProvider provider = new ();
TimeSpan testTime = new (14, 30, 45);
provider.TimeValue = testTime;
-
+
string initialDisplay = provider.DisplayText;
-
+
// Change to a custom format
- DateTimeFormatInfo customFormat = (DateTimeFormatInfo)CultureInfo.CurrentCulture.DateTimeFormat.Clone ();
+ var customFormat = (DateTimeFormatInfo)CultureInfo.CurrentCulture.DateTimeFormat.Clone ();
customFormat.LongTimePattern = "HH:mm";
provider.Format = customFormat;
-
+
string newDisplay = provider.DisplayText;
-
+
// Display should change
Assert.NotEqual (initialDisplay, newDisplay);
-
+
// New display should not contain seconds
Assert.DoesNotContain ("45", newDisplay);
}
@@ -194,15 +193,15 @@ public void TimeTextProvider_Format_Change_Updates_Pattern ()
public void TimeTextProvider_Validates_Hours_Minutes_Seconds ()
{
TimeTextProvider provider = new ();
-
+
// Valid time
provider.TimeValue = new TimeSpan (23, 59, 59);
Assert.True (provider.IsValid);
-
+
// Another valid time
provider.TimeValue = new TimeSpan (0, 0, 0);
Assert.True (provider.IsValid);
-
+
// Provider auto-corrects invalid values, so IsValid should always be true
provider.TimeValue = new TimeSpan (12, 30, 15);
Assert.True (provider.IsValid);
@@ -213,16 +212,16 @@ public void TimeEditor_KeyInput_UpdatesValue ()
{
IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
-
+
try
{
TimeEditor te = new () { App = app };
te.Layout ();
te.Value = TimeSpan.Zero;
-
+
// Simulate typing '1'
te.NewKeyDownEvent (Key.D1);
-
+
// The value should have been updated
string text = te.Text.Trim ();
Assert.Contains ("1", text);
@@ -238,22 +237,22 @@ public void TimeEditor_Navigation_Keys ()
{
IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
-
+
try
{
TimeEditor te = new () { App = app };
te.Layout ();
-
+
// Home key should move to start
te.NewKeyDownEvent (Key.Home);
-
+
// End key should move to end
te.NewKeyDownEvent (Key.End);
-
+
// Arrow keys should navigate
te.NewKeyDownEvent (Key.CursorLeft);
te.NewKeyDownEvent (Key.CursorRight);
-
+
// No exceptions should be thrown
Assert.NotNull (te);
}
@@ -269,9 +268,9 @@ public void TimeEditor_IValue_GetValue ()
TimeEditor te = new ();
TimeSpan testTime = new (14, 30, 45);
te.Value = testTime;
-
- object? value = ((Terminal.Gui.ViewBase.IValue)te).GetValue ();
-
+
+ object? value = ((IValue)te).GetValue ();
+
Assert.NotNull (value);
Assert.IsType (value);
Assert.Equal (testTime, (TimeSpan)value);
@@ -281,25 +280,25 @@ public void TimeEditor_IValue_GetValue ()
public void TimeTextProvider_12Hour_Format_With_AM_PM ()
{
TimeTextProvider provider = new ();
-
+
// Set to a 12-hour format
- DateTimeFormatInfo format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
+ var format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
format12h.LongTimePattern = "h:mm:ss tt";
provider.Format = format12h;
-
+
// Set time to 2:30 PM (14:30)
provider.TimeValue = new TimeSpan (14, 30, 0);
-
+
string display = provider.DisplayText.Trim ();
-
+
// Should contain PM
Assert.Contains ("PM", display, StringComparison.OrdinalIgnoreCase);
-
+
// Set time to 2:30 AM (2:30)
provider.TimeValue = new TimeSpan (2, 30, 0);
-
+
display = provider.DisplayText.Trim ();
-
+
// Should contain AM
Assert.Contains ("AM", display, StringComparison.OrdinalIgnoreCase);
}
@@ -308,20 +307,20 @@ public void TimeTextProvider_12Hour_Format_With_AM_PM ()
public void TimeTextProvider_24Hour_Format ()
{
TimeTextProvider provider = new ();
-
+
// Set to a 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
provider.Format = format24h;
-
+
// Set time to 14:30
provider.TimeValue = new TimeSpan (14, 30, 0);
-
+
string display = provider.DisplayText.Trim ();
-
+
// Should contain 14
Assert.Contains ("14", display);
-
+
// Should not contain AM/PM
Assert.DoesNotContain ("AM", display, StringComparison.OrdinalIgnoreCase);
Assert.DoesNotContain ("PM", display, StringComparison.OrdinalIgnoreCase);
@@ -332,29 +331,29 @@ public void TimeEditor_Delete_And_Backspace ()
{
IApplication app = Application.Create ();
app.Init (DriverRegistry.Names.ANSI);
-
+
try
{
TimeEditor te = new () { App = app };
-
+
// Use 24-hour format to ensure consistent behavior
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
te.Format = format24h;
-
+
te.Value = new TimeSpan (12, 34, 56);
te.Layout ();
-
+
// Move to start and delete
te.NewKeyDownEvent (Key.Home);
te.NewKeyDownEvent (Key.Delete);
-
+
// Value should have changed (first digit replaced with 0)
string text = te.Text.Trim ();
Assert.NotEqual ("12:34:56", text);
-
+
// Backspace should also work
te.NewKeyDownEvent (Key.Backspace);
-
+
// Text should have changed again
Assert.NotNull (te.Text);
}
@@ -368,12 +367,12 @@ public void TimeEditor_Delete_And_Backspace ()
public void TimeTextProvider_CursorEnd_Returns_Last_Position ()
{
TimeTextProvider provider = new ();
-
+
int endPos = provider.CursorEnd ();
-
+
// End position should be >= 0
Assert.True (endPos >= 0);
-
+
// For 24-hour format like "HH:mm:ss", end should be at last digit (position 7)
// For 12-hour format with AM/PM, end should be at AM/PM position
int displayLength = provider.DisplayText.Trim ().Length;
@@ -384,14 +383,14 @@ public void TimeTextProvider_CursorEnd_Returns_Last_Position ()
public void TimeEditor_Text_Property_Updates_Value ()
{
TimeEditor te = new ();
-
+
// Use 24-hour format to ensure consistent parsing
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
te.Format = format24h;
-
+
// Set text directly
te.Text = "14:30:45";
-
+
// Value should be updated
Assert.Equal (14, te.Value.Hours);
Assert.Equal (30, te.Value.Minutes);
@@ -402,10 +401,10 @@ public void TimeEditor_Text_Property_Updates_Value ()
public void TimeTextProvider_InsertAt_NonDigit_Returns_False ()
{
TimeTextProvider provider = new ();
-
+
// Try to insert a non-digit character at a digit position
bool result = provider.InsertAt ('x', 0);
-
+
// Should fail
Assert.False (result);
}
@@ -415,15 +414,15 @@ public void TimeEditor_Multiple_Format_Changes ()
{
TimeEditor te = new () { Value = new TimeSpan (14, 30, 45) };
te.Layout ();
-
+
// Change format multiple times
- for (int i = 0; i < 3; i++)
+ for (var i = 0; i < 3; i++)
{
- DateTimeFormatInfo format = (DateTimeFormatInfo)CultureInfo.CurrentCulture.DateTimeFormat.Clone ();
+ var format = (DateTimeFormatInfo)CultureInfo.CurrentCulture.DateTimeFormat.Clone ();
format.LongTimePattern = i % 2 == 0 ? "HH:mm" : "HH:mm:ss";
te.Format = format;
te.Layout ();
-
+
// Value should remain the same
Assert.Equal (14, te.Value.Hours);
Assert.Equal (30, te.Value.Minutes);
@@ -434,17 +433,17 @@ public void TimeEditor_Multiple_Format_Changes ()
public void TimeTextProvider_TryManualParse_PartialInput ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
// Test partial input parsing (minutes only)
provider.Text = "14:30";
Assert.Equal (14, provider.TimeValue.Hours);
Assert.Equal (30, provider.TimeValue.Minutes);
Assert.Equal (0, provider.TimeValue.Seconds);
-
+
// Test with seconds
provider.Text = "14:30:45";
Assert.Equal (14, provider.TimeValue.Hours);
@@ -456,17 +455,17 @@ public void TimeTextProvider_TryManualParse_PartialInput ()
public void TimeTextProvider_TryManualParse_InvalidInput ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
TimeSpan initialValue = provider.TimeValue;
-
+
// Test invalid input (no separator)
provider.Text = "invalid";
Assert.Equal (initialValue, provider.TimeValue);
-
+
// Test incomplete input (only one part)
provider.Text = "14";
Assert.Equal (initialValue, provider.TimeValue);
@@ -476,14 +475,14 @@ public void TimeTextProvider_TryManualParse_InvalidInput ()
public void TimeTextProvider_TryManualParse_AutoCorrection ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
// Test auto-correction for out-of-range values
provider.Text = "25:70:90";
-
+
// Should auto-correct to valid ranges
Assert.Equal (23, provider.TimeValue.Hours); // Max 23
Assert.Equal (59, provider.TimeValue.Minutes); // Max 59
@@ -494,23 +493,23 @@ public void TimeTextProvider_TryManualParse_AutoCorrection ()
public void TimeTextProvider_12Hour_AM_PM_Parsing ()
{
TimeTextProvider provider = new ();
-
+
// Use 12-hour format
- DateTimeFormatInfo format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
+ var format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
provider.Format = format12h;
-
+
// Test PM parsing
provider.Text = "2:30:00 PM";
Assert.Equal (14, provider.TimeValue.Hours);
-
+
// Test AM parsing
provider.Text = "2:30:00 AM";
Assert.Equal (2, provider.TimeValue.Hours);
-
+
// Test 12 PM (noon)
provider.Text = "12:00:00 PM";
Assert.Equal (12, provider.TimeValue.Hours);
-
+
// Test 12 AM (midnight)
provider.Text = "12:00:00 AM";
Assert.Equal (0, provider.TimeValue.Hours);
@@ -520,30 +519,30 @@ public void TimeTextProvider_12Hour_AM_PM_Parsing ()
public void TimeTextProvider_CursorNavigation_Comprehensive ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
// Test CursorStart
Assert.Equal (0, provider.CursorStart ());
-
+
// Test CursorEnd
int endPos = provider.CursorEnd ();
Assert.True (endPos >= 0);
-
+
// Test navigation through all positions
int pos = provider.CursorStart ();
int lastPos = pos;
-
- for (int i = 0; i < 10; i++)
+
+ for (var i = 0; i < 10; i++)
{
int nextPos = provider.CursorRight (pos);
-
+
// Should skip separators
Assert.NotEqual (pos, nextPos);
pos = nextPos;
-
+
if (pos >= provider.CursorEnd ())
{
break;
@@ -557,28 +556,25 @@ public void TimeEditor_ValueChanging_Cancel ()
TimeEditor te = new ();
TimeSpan initialValue = TimeSpan.FromHours (10);
te.Value = initialValue;
-
- bool changingEventFired = false;
- bool changedEventFired = false;
-
+
+ var changingEventFired = false;
+ var changedEventFired = false;
+
te.ValueChanging += (_, e) =>
- {
- changingEventFired = true;
- e.Handled = true; // Cancel the change
- };
-
- te.ValueChanged += (_, e) =>
- {
- changedEventFired = true;
- };
-
+ {
+ changingEventFired = true;
+ e.Handled = true; // Cancel the change
+ };
+
+ te.ValueChanged += (_, e) => { changedEventFired = true; };
+
// Try to set new value
te.Value = TimeSpan.FromHours (15);
-
+
// ValueChanging should have fired, but ValueChanged should not
Assert.True (changingEventFired);
Assert.False (changedEventFired);
-
+
// Value should not have changed
Assert.Equal (initialValue, te.Value);
}
@@ -587,20 +583,20 @@ public void TimeEditor_ValueChanging_Cancel ()
public void TimeTextProvider_Delete_AtSeparatorPosition ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
provider.TimeValue = new TimeSpan (14, 30, 45);
-
+
// Try to delete at separator position (position 2 in "14:30:45")
string beforeText = provider.Text.Trim ();
bool result = provider.Delete (2);
-
+
// Delete at separator should not change anything or should skip to next position
string afterText = provider.Text.Trim ();
-
+
// The behavior depends on implementation, but text should still be valid
Assert.NotNull (afterText);
}
@@ -609,20 +605,20 @@ public void TimeTextProvider_Delete_AtSeparatorPosition ()
public void TimeEditor_ValueChangedUntyped_Event ()
{
TimeEditor te = new ();
- bool eventFired = false;
+ var eventFired = false;
object? oldValue = null;
object? newValue = null;
-
+
te.ValueChangedUntyped += (_, e) =>
- {
- eventFired = true;
- oldValue = e.OldValue;
- newValue = e.NewValue;
- };
-
+ {
+ eventFired = true;
+ oldValue = e.OldValue;
+ newValue = e.NewValue;
+ };
+
TimeSpan testValue = TimeSpan.FromHours (15);
te.Value = testValue;
-
+
Assert.True (eventFired);
Assert.Equal (TimeSpan.Zero, oldValue);
Assert.Equal (testValue, newValue);
@@ -632,11 +628,11 @@ public void TimeEditor_ValueChangedUntyped_Event ()
public void TimeTextProvider_CursorLeft_FromStart ()
{
TimeTextProvider provider = new ();
-
+
// Use 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
-
+
// CursorLeft from start should return start
int pos = provider.CursorLeft (0);
Assert.Equal (0, pos);
@@ -648,7 +644,7 @@ public void TimeTextProvider_CursorRight_FromEnd ()
TimeTextProvider provider = new ();
// Use 24-hour format
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
provider.Format = format24h;
int endPos = provider.CursorEnd ();
@@ -673,14 +669,10 @@ public void TimeEditor_12h_DisplayText_Shows_Full_AM_PM ()
Runnable runnable = new () { Width = 30, Height = 1 };
app.Begin (runnable);
- TimeEditor te = new ()
- {
- Height = 1,
- Value = new TimeSpan (9, 0, 0)
- };
+ TimeEditor te = new () { Height = 1, Value = new TimeSpan (9, 0, 0) };
// Explicitly set 12-hour format AFTER construction to simulate the scenario
- DateTimeFormatInfo format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
+ var format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
format12h.LongTimePattern = "h:mm:ss tt";
te.Format = format12h;
@@ -698,10 +690,7 @@ public void TimeEditor_12h_DisplayText_Shows_Full_AM_PM ()
Assert.True (te.Frame.Width >= te.Provider.DisplayText.Length,
$"Frame width {te.Frame.Width} is too narrow for DisplayText \"{te.Provider.DisplayText}\" ({te.Provider.DisplayText.Length} chars)");
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"09:00:00 AM",
- output,
- app.Driver);
+ DriverAssert.AssertDriverContentsWithFrameAre (@"09:00:00 AM", output, app.Driver);
}
finally
{
@@ -714,10 +703,7 @@ public void TimeEditor_12h_DisplayText_Shows_Full_AM_PM ()
public void TimeEditor_Default_Constructor_Width_Fits_DisplayText ()
{
// Verifies that the default constructor produces a Width that fits the full DisplayText
- TimeEditor te = new ()
- {
- Value = new TimeSpan (9, 0, 0)
- };
+ TimeEditor te = new () { Value = new TimeSpan (9, 0, 0) };
te.Layout ();
output.WriteLine ($"DisplayText: \"{te.Provider!.DisplayText}\"");
@@ -735,7 +721,7 @@ public void TimeEditor_CursorRight_From_BlankCell_DoesNotMoveBackward ()
// Verifies that pressing right arrow from the blank cell past the last editable
// position does NOT move the cursor backward (e.g., from "M" in "PM" back to "A").
TimeTextProvider provider = new ();
- DateTimeFormatInfo format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
+ var format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
format12h.LongTimePattern = "h:mm:ss tt";
provider.Format = format12h;
@@ -753,11 +739,7 @@ public void TimeEditor_CursorRight_From_BlankCell_DoesNotMoveBackward ()
try
{
- TimeEditor te = new ()
- {
- Value = new TimeSpan (9, 0, 0),
- Format = format12h
- };
+ TimeEditor te = new () { Value = new TimeSpan (9, 0, 0), Format = format12h };
te.Layout ();
te.SetFocus ();
@@ -791,7 +773,7 @@ public void NormalizedPattern_24h_Typing_12_Enters_TwoDigitHour ()
{
// Verifies that typing "12" at position 0 in 24h format sets hours to 12
TimeTextProvider provider = new ();
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
provider.Format = format24h;
provider.TimeValue = new TimeSpan (9, 0, 0);
@@ -818,7 +800,7 @@ public void NormalizedPattern_24h_Typing_12_Enters_TwoDigitHour ()
public void NormalizedPattern_DisplayText_Has_No_LeadingSpace ()
{
TimeTextProvider provider = new ();
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
provider.Format = format24h;
provider.TimeValue = new TimeSpan (9, 0, 0);
@@ -836,7 +818,7 @@ public void NormalizedPattern_SingleDigitHourFormat_PadsToTwoDigits ()
{
// Verifies that "h:mm:ss tt" is normalized to "hh:mm:ss tt"
TimeTextProvider provider = new ();
- DateTimeFormatInfo format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
+ var format12h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-US").DateTimeFormat.Clone ();
format12h.LongTimePattern = "h:mm:ss tt";
provider.Format = format12h;
provider.TimeValue = new TimeSpan (9, 0, 0);
@@ -855,7 +837,7 @@ public void NormalizedPattern_FieldPositions_AreConsistent ()
{
// Verifies separator positions don't shift based on time value
TimeTextProvider provider = new ();
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "H:mm:ss"; // Single-digit H
provider.Format = format24h;
@@ -886,26 +868,17 @@ public void TimeEditor_Typing_12_At_Start_Renders_Correctly ()
Runnable runnable = new () { Width = 20, Height = 1 };
app.Begin (runnable);
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
- TimeEditor te = new ()
- {
- Width = 10,
- Height = 1,
- Value = new TimeSpan (9, 0, 0),
- Format = format24h
- };
+ TimeEditor te = new () { Width = 10, Height = 1, Value = new TimeSpan (9, 0, 0), Format = format24h };
runnable.Add (te);
app.LayoutAndDraw ();
output.WriteLine ($"Initial Text: \"{te.Text}\"");
output.WriteLine ($"Initial DisplayText: \"{te.Provider!.DisplayText}\"");
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"09:00:00",
- output,
- app.Driver);
+ DriverAssert.AssertDriverContentsWithFrameAre (@"09:00:00", output, app.Driver);
// Simulate focus and typing "1" then "2"
te.SetFocus ();
@@ -922,10 +895,7 @@ public void TimeEditor_Typing_12_At_Start_Renders_Correctly ()
Assert.Equal (new TimeSpan (12, 0, 0), te.Value);
- DriverAssert.AssertDriverContentsWithFrameAre (
- @"12:00:00",
- output,
- app.Driver);
+ DriverAssert.AssertDriverContentsWithFrameAre (@"12:00:00", output, app.Driver);
}
finally
{
@@ -939,7 +909,7 @@ public void TimeEditor_CursorRight_SkipsSeparator_24h ()
{
// Verifies cursor movement: after typing at pos 1, cursor skips separator to pos 3
TimeTextProvider provider = new ();
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
provider.Format = format24h;
@@ -956,7 +926,7 @@ public void TimeEditor_CursorRight_SkipsSeparator_24h ()
public void TimeEditor_CursorLeft_SkipsSeparator_24h ()
{
TimeTextProvider provider = new ();
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
provider.Format = format24h;
@@ -971,7 +941,7 @@ public void TimeEditor_CursorLeft_SkipsSeparator_24h ()
public void TimeEditor_FullNavigation_24h_AllPositions ()
{
TimeTextProvider provider = new ();
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
provider.Format = format24h;
@@ -996,7 +966,7 @@ public void TimeEditor_FullNavigation_24h_AllPositions ()
public void TimeEditor_InsertAt_AllDigitPositions_24h ()
{
TimeTextProvider provider = new ();
- DateTimeFormatInfo format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
+ var format24h = (DateTimeFormatInfo)CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ();
format24h.LongTimePattern = "HH:mm:ss";
provider.Format = format24h;
provider.TimeValue = TimeSpan.Zero; // "00:00:00"
From 548fe80a7593d8a4ead33c1763a59bed6b8a720e Mon Sep 17 00:00:00 2001
From: Tig
Date: Sat, 7 Mar 2026 16:59:47 -0700
Subject: [PATCH 18/26] Implement CWP and IValue for
TextValidateField/TimeEditor
Refactored TextValidateField and TimeEditor to fully implement the IValue interface and follow the Cancellable Work Pattern (CWP) for value changes.
- TextValidateField now exposes Value, ValueChanging, and ValueChanged events, and synchronizes value changes from both programmatic and user input, with support for cancellation and reversion.
- Introduced SuppressValueEvents to prevent unwanted event recursion.
- TimeEditor now uses a private _value field, leverages CWPPropertyHelper for value changes, and raises strongly-typed TimeSpan events.
- Removed manual event wiring and last-known-value tracking in favor of the new base class pattern.
- These changes improve consistency, extensibility, and support for data binding and undo/redo scenarios.
---
.../Views/TextInput/TextValidateField.cs | 158 +++++++++++++++++-
Terminal.Gui/Views/TextInput/TimeEditor.cs | 156 ++++++++---------
2 files changed, 230 insertions(+), 84 deletions(-)
diff --git a/Terminal.Gui/Views/TextInput/TextValidateField.cs b/Terminal.Gui/Views/TextInput/TextValidateField.cs
index d7153d5671..2139a8da5c 100644
--- a/Terminal.Gui/Views/TextInput/TextValidateField.cs
+++ b/Terminal.Gui/Views/TextInput/TextValidateField.cs
@@ -1,11 +1,19 @@
namespace Terminal.Gui.Views;
/// Masked text editor that validates input through a
-public class TextValidateField : View, IDesignable
+public class TextValidateField : View, IDesignable, IValue
{
private const int DEFAULT_LENGTH = 10;
private ITextValidateProvider? _provider;
+ private string _lastKnownText = string.Empty;
+
+ ///
+ /// Gets or sets whether value change events are suppressed.
+ /// Subclasses set this to when programmatically updating the provider
+ /// to prevent re-entrant event firing.
+ ///
+ protected bool SuppressValueEvents { get; set; }
///
/// Initializes a new instance of the class.
@@ -97,8 +105,19 @@ public ITextValidateProvider? Provider
get => _provider;
set
{
+ if (_provider is { })
+ {
+ _provider.TextChanged -= ProviderOnTextChanged;
+ }
+
_provider = value;
+ if (_provider is { })
+ {
+ _provider.TextChanged += ProviderOnTextChanged;
+ _lastKnownText = _provider.Text;
+ }
+
if (_provider!.Fixed)
{
// Add one so there is always a blank cell after the last editable character for the cursor.
@@ -110,6 +129,43 @@ public ITextValidateProvider? Provider
}
}
+ #region IValue Implementation
+
+ ///
+ /// Gets or sets the value. This is an alias for .
+ ///
+ ///
+ /// This property enables to be used with the pattern
+ /// for generic value access and command propagation.
+ ///
+ public string? Value { get => Text; set => Text = value ?? string.Empty; }
+
+ ///
+ public event EventHandler>? ValueChanging;
+
+ ///
+ public event EventHandler>? ValueChanged;
+
+ ///
+ public event EventHandler>? ValueChangedUntyped;
+
+ ///
+ /// Called when is about to change.
+ /// Allows derived classes to cancel the change.
+ ///
+ /// The event arguments.
+ /// to cancel the change; otherwise .
+ protected virtual bool OnValueChanging (ValueChangingEventArgs args) => false;
+
+ ///
+ /// Called when has changed.
+ /// Allows derived classes to react to value changes.
+ ///
+ /// The event arguments.
+ protected virtual void OnValueChanged (ValueChangedEventArgs args) { }
+
+ #endregion
+
/// Text
public override string Text
{
@@ -121,8 +177,44 @@ public override string Text
return;
}
+ string oldValue = _provider.Text;
+
+ if (oldValue == value)
+ {
+ return;
+ }
+
+ if (!SuppressValueEvents)
+ {
+ ValueChangingEventArgs args = new (oldValue, value);
+
+ if (OnValueChanging (args) || args.Handled)
+ {
+ return;
+ }
+
+ ValueChanging?.Invoke (this, args);
+
+ if (args.Handled)
+ {
+ return;
+ }
+
+ // Allow subscribers to modify the new value
+ value = args.NewValue ?? string.Empty;
+ }
+
+ _lastKnownText = value;
_provider.Text = value;
+ if (!SuppressValueEvents)
+ {
+ ValueChangedEventArgs changedArgs = new (oldValue, value);
+ OnValueChanged (changedArgs);
+ ValueChanged?.Invoke (this, changedArgs);
+ ValueChangedUntyped?.Invoke (this, new ValueChangedEventArgs
-## [DateField](xref:Terminal.Gui.Views.DateField)
+## [DateEditor](xref:Terminal.Gui.Views.DateEditor)
-Provides date editing functionality with specialized cursor behavior for date entry.
+Provides date editing functionality using [TextValidateField](xref:Terminal.Gui.Views.TextValidateField) with culture-aware formatting.
From 8b3a3096ae848d646d4c72fcaa9378dc9207e21b Mon Sep 17 00:00:00 2001
From: Tig
Date: Mon, 9 Mar 2026 06:28:10 -0600
Subject: [PATCH 20/26] code cleanup
---
Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs | 2 +-
Terminal.Gui/Views/TextInput/TextRegexProvider.cs | 2 +-
Terminal.Gui/Views/TextInput/TimeTextProvider.cs | 2 +-
3 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs b/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs
index 080d9cb03f..0eeb091331 100644
--- a/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs
+++ b/Terminal.Gui/Views/TextInput/NetMaskedTextProvider.cs
@@ -135,6 +135,6 @@ public bool InsertAt (char ch, int pos)
///