Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
703bfa0
Initial plan
Copilot Jan 24, 2026
e9ca565
Add TimeTextProvider and TimeEditor implementation
Copilot Jan 24, 2026
dad7634
Add comprehensive unit tests for TimeEditor and TimeTextProvider
Copilot Jan 24, 2026
ba57057
Address code review feedback: fix event args, remove AI comment, simp…
Copilot Jan 24, 2026
181ac3c
Add TimeEditor demonstration to UICatalog TimeAndDate scenario
Copilot Jan 24, 2026
6f31b7c
Fix culture-dependent test failure in TimeTextProvider_Delete_Replace…
Copilot Jan 24, 2026
5dc787f
Fix additional culture-dependent tests for macOS/Windows compatibility
Copilot Jan 24, 2026
0177821
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Jan 25, 2026
33897c1
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 3, 2026
949e788
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 3, 2026
f1de6ce
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 3, 2026
dfbfcbd
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 4, 2026
8c65fd6
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 22, 2026
cd97f9e
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 25, 2026
94cd4f0
Fix code review issues: double-firing events, ValueChanging for keybo…
Copilot Feb 25, 2026
dcdef40
Add comprehensive test coverage for TryManualParse, cursor navigation…
Copilot Feb 25, 2026
da9cd14
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 27, 2026
80f4a3a
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Feb 28, 2026
66fa802
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Mar 4, 2026
f00b6db
TextAlignment = Alignment.End is broken
tig Jan 25, 2026
e7f4169
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Mar 5, 2026
cf40e87
Fix _isPm synchronization in TimeValue setter after f00b6db refactoring
Copilot Mar 6, 2026
124ab29
merged
tig Mar 6, 2026
037f16d
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Mar 6, 2026
9248041
Merge branch 'copilot/rewrite-timefield-and-datefield' of tig:gui-cs/…
tig Mar 6, 2026
d4bdf24
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Mar 7, 2026
e69b34f
Fix TextValidateField test failures and TimeEditor EnableForDesign crash
tig Mar 7, 2026
fe89b5e
Fix TimeEditor cursor/insertion by normalizing time format patterns
tig Mar 7, 2026
9f8d51e
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Mar 7, 2026
100977d
Improve TimeEditor width and TextValidateField cursor logic
tig Mar 7, 2026
75ea987
Improve cursor handling at end of masked text fields
tig Mar 7, 2026
ad7a30f
Replace TimeField with new TimeEditor control
tig Mar 7, 2026
5e7e208
Refactor TimeEditor and providers, remove VerifyChar method
tig Mar 7, 2026
548fe80
Implement CWP and IValue<T> for TextValidateField/TimeEditor
tig Mar 7, 2026
980c453
merged
tig Mar 8, 2026
71c62dd
Replace DateField with new DateEditor based on TextValidateField
tig Mar 8, 2026
8b3a309
code cleanup
tig Mar 9, 2026
8eae61a
code cleanup
tig Mar 9, 2026
68d8c7e
Merge branch 'v2_develop' of tig:tig/Terminal.Gui into v2_develop
tig Mar 9, 2026
ab78380
merged
tig Mar 9, 2026
0a83cc5
Refactor TextChanged event handling for providers
tig Mar 9, 2026
6b0fa2a
Fixed test bug.
tig Mar 9, 2026
dab9d58
Update TimeAndDate scenario: remove center alignment, add DatePicker …
tig Mar 9, 2026
65f935f
Merge branch 'v2_develop' into copilot/rewrite-timefield-and-datefield
tig Mar 9, 2026
5b140f6
Fix DateEditor.Value to be non-nullable (DateTime instead of DateTime?)
Copilot Mar 9, 2026
62e5aa0
Fix DatePicker Culture and Value propagation to embedded DateEditor
Copilot Mar 9, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 74 additions & 3 deletions Examples/UICatalog/Scenarios/TimeAndDate.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
#nullable enable
using System;
using System.Globalization;

namespace UICatalog.Scenarios;

Expand All @@ -14,6 +15,7 @@ public class TimeAndDate : Scenario
private Label? _lblOldDate;
private Label? _lblOldTime;
private Label? _lblTimeFmt;
private Label? _lblTimeEditorValue;

public override void Main ()
{
Expand All @@ -23,10 +25,20 @@ public override void Main ()
app.Init ();

using Window win = new () { Title = GetQuitKeyAndName () };

// TimeField examples (existing)
Label tfLabel = new ()
{
X = Pos.Center (),
Y = 1,
Text = "TimeField (Legacy):"
};
win.Add (tfLabel);

TimeField longTime = new ()
{
X = Pos.Center (),
Y = 2,
Y = Pos.Bottom (tfLabel),
IsShortFormat = false,
ReadOnly = false,
Value = DateTime.Now.TimeOfDay
Expand All @@ -44,10 +56,53 @@ public override void Main ()
};
shortTime.ValueChanged += TimeChanged;
win.Add (shortTime);

// TimeEditor examples (new)
Label teLabel = new ()
{
X = Pos.Center (),
Y = Pos.Bottom (shortTime) + 1,
Text = "TimeEditor (New - based on TextValidateField):"
};
win.Add (teLabel);

// Default culture time editor
TimeEditor defaultTimeEditor = new ()
{
X = Pos.Center (),
Y = Pos.Bottom (teLabel),
Value = DateTime.Now.TimeOfDay
};
defaultTimeEditor.ValueChanged += TimeEditorChanged;
win.Add (defaultTimeEditor);

// 24-hour format time editor
TimeEditor time24Editor = new ()
{
X = Pos.Center (),
Y = Pos.Bottom (defaultTimeEditor) + 1,
Value = DateTime.Now.TimeOfDay,
Format = (DateTimeFormatInfo)System.Globalization.CultureInfo.GetCultureInfo ("en-GB").DateTimeFormat.Clone ()
};
time24Editor.ValueChanged += TimeEditorChanged;
win.Add (time24Editor);

// Short time format time editor
DateTimeFormatInfo shortFormat = (DateTimeFormatInfo)System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.Clone ();
shortFormat.LongTimePattern = shortFormat.ShortTimePattern;
TimeEditor shortTimeEditor = new ()
{
X = Pos.Center (),
Y = Pos.Bottom (time24Editor) + 1,
Value = DateTime.Now.TimeOfDay,
Format = shortFormat
};
shortTimeEditor.ValueChanged += TimeEditorChanged;
win.Add (shortTimeEditor);

DateField shortDate = new (DateTime.Now)
{
X = Pos.Center (), Y = Pos.Bottom (shortTime) + 1, ReadOnly = true
X = Pos.Center (), Y = Pos.Bottom (shortTimeEditor) + 1, ReadOnly = true
};
shortDate.ValueChanged += DateChanged;
win.Add (shortDate);
Expand Down Expand Up @@ -91,11 +146,22 @@ public override void Main ()
Text = "Time Format: "
};
win.Add (_lblTimeFmt);

_lblTimeEditorValue = new()
{
X = Pos.Center (),
Y = Pos.Bottom (_lblTimeFmt) + 1,
TextAlignment = Alignment.Center,

Width = Dim.Fill (),
Text = "TimeEditor Value: "
};
win.Add (_lblTimeEditorValue);

_lblOldDate = new()
{
X = Pos.Center (),
Y = Pos.Bottom (_lblTimeFmt) + 2,
Y = Pos.Bottom (_lblTimeEditorValue) + 1,
TextAlignment = Alignment.Center,

Width = Dim.Fill (),
Expand Down Expand Up @@ -155,4 +221,9 @@ private void TimeChanged (object? sender, ValueChangedEventArgs<TimeSpan> e)
{
_lblNewTime!.Text = $"New Time: {e.NewValue}";
}

private void TimeEditorChanged (object? sender, ValueChangedEventArgs<TimeSpan> e)
{
_lblTimeEditorValue!.Text = $"TimeEditor Value: {e.NewValue}";
}
}
182 changes: 182 additions & 0 deletions Terminal.Gui/Views/TextInput/TimeEditor.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
using System.Globalization;
using Terminal.Gui.ViewBase;

namespace Terminal.Gui.Views;

/// <summary>
/// Provides time editing functionality using <see cref="TextValidateField"/> with culture-aware formatting.
/// </summary>
/// <remarks>
/// <para>
/// TimeEditor extends <see cref="TextValidateField"/> with time-specific functionality:
/// <list type="bullet">
/// <item><description>Uses <see cref="TimeTextProvider"/> for validation and formatting</description></item>
/// <item><description>Supports both 12-hour and 24-hour formats via <see cref="DateTimeFormatInfo"/></description></item>
/// <item><description>Cursor automatically skips over separator characters</description></item>
/// <item><description>Supports AM/PM toggling for 12-hour formats</description></item>
/// <item><description>Auto-adjusts width based on time pattern</description></item>
/// </list>
/// </para>
/// <para>
/// <b>Usage Examples:</b>
/// <code>
/// // Use default (current culture's long time pattern)
/// TimeEditor timeEditor = new () { Value = TimeSpan.FromHours (14.5) };
/// // en-US displays: " 2:30:00 PM"
/// // en-GB displays: " 14:30:00"
///
/// // Use specific culture's format
/// timeEditor.Format = CultureInfo.GetCultureInfo ("de-DE").DateTimeFormat;
/// // Displays: " 14:30:00"
///
/// // Want short time? Modify the LongTimePattern
/// DateTimeFormatInfo format = (DateTimeFormatInfo)CultureInfo.CurrentCulture.DateTimeFormat.Clone ();
/// format.LongTimePattern = format.ShortTimePattern;
/// timeEditor.Format = format;
/// // en-US displays: " 2:30 PM"
///
/// // Custom pattern with milliseconds
/// DateTimeFormatInfo customFormat = (DateTimeFormatInfo)CultureInfo.CurrentCulture.DateTimeFormat.Clone ();
/// customFormat.LongTimePattern = "HH:mm:ss.fff";
/// timeEditor.Format = customFormat;
/// // Displays: " 14:30:00.000"
/// </code>
/// </para>
/// </remarks>
public class TimeEditor : TextValidateField, IValue<TimeSpan>

Check failure on line 46 in Terminal.Gui/Views/TextInput/TimeEditor.cs

View workflow job for this annotation

GitHub Actions / build / Build Debug Only

'TimeEditor' does not implement interface member 'IValue.ValueChangedUntyped'

Check failure on line 46 in Terminal.Gui/Views/TextInput/TimeEditor.cs

View workflow job for this annotation

GitHub Actions / build / Build Debug Only

'TimeEditor' does not implement interface member 'IValue.ValueChangedUntyped'

Check failure on line 46 in Terminal.Gui/Views/TextInput/TimeEditor.cs

View workflow job for this annotation

GitHub Actions / build / Build Debug Only

'TimeEditor' does not implement interface member 'IValue.ValueChangedUntyped'

Check failure on line 46 in Terminal.Gui/Views/TextInput/TimeEditor.cs

View workflow job for this annotation

GitHub Actions / build / Build Debug Only

'TimeEditor' does not implement interface member 'IValue.ValueChangedUntyped'

Check failure on line 46 in Terminal.Gui/Views/TextInput/TimeEditor.cs

View workflow job for this annotation

GitHub Actions / Build All Configurations

'TimeEditor' does not implement interface member 'IValue.ValueChangedUntyped'

Check failure on line 46 in Terminal.Gui/Views/TextInput/TimeEditor.cs

View workflow job for this annotation

GitHub Actions / Build All Configurations

'TimeEditor' does not implement interface member 'IValue.ValueChangedUntyped'
{
private TimeTextProvider TimeProvider => (TimeTextProvider)Provider!;
private TimeSpan _lastKnownValue = TimeSpan.Zero;

/// <summary>
/// Initializes a new instance of the <see cref="TimeEditor"/> class.
/// </summary>
public TimeEditor ()
{
Comment thread
tig marked this conversation as resolved.
Provider = new TimeTextProvider ();
Width = Dim.Auto (minimumContentDim: 10);

// Subscribe to provider's text changed to raise our value events
TimeProvider.TextChanged += (_, _) => RaiseValueChangedEvents ();

// Initialize last known value
_lastKnownValue = TimeProvider.TimeValue;
}

/// <summary>
/// Gets or sets the <see cref="DateTimeFormatInfo"/> used for time formatting.
/// </summary>
/// <remarks>
/// <para>
/// The editor uses <see cref="DateTimeFormatInfo.LongTimePattern"/> to determine the display format.
/// To use a short time format, clone the DateTimeFormatInfo and set LongTimePattern to ShortTimePattern.
/// </para>
/// <para>
/// The width automatically adjusts when the format changes to accommodate the new pattern.
/// </para>
Comment thread
tig marked this conversation as resolved.
/// </remarks>
public DateTimeFormatInfo Format
{
get => TimeProvider.Format;
set
{
TimeProvider.Format = value;
Width = TimeProvider.DisplayText.Length + 2;
SetNeedsDraw ();
}
}

/// <summary>
/// Gets or sets the current time value.
/// </summary>
/// <remarks>
/// <para>
/// Setting this property raises <see cref="ValueChanging"/> (cancellable) and <see cref="ValueChanged"/> events.
/// The change can be prevented by handling <see cref="ValueChanging"/> and setting
/// <see cref="ValueChangingEventArgs{T}.Handled"/> to <see langword="true"/>.
/// </para>
/// </remarks>
public TimeSpan Value
{
get => TimeProvider.TimeValue;
set
{
TimeSpan oldValue = TimeProvider.TimeValue;

if (oldValue == value)
{
return;
}

ValueChangingEventArgs<TimeSpan> changingArgs = new (oldValue, value);

if (OnValueChanging (changingArgs) || changingArgs.Handled)
{
return;
}

ValueChanging?.Invoke (this, changingArgs);

if (changingArgs.Handled)
{
return;
}

TimeProvider.TimeValue = value;
Text = TimeProvider.Text;

ValueChangedEventArgs<TimeSpan> changedArgs = new (oldValue, value);
OnValueChanged (changedArgs);
ValueChanged?.Invoke (this, changedArgs);

SetNeedsDraw ();
}
}

/// <inheritdoc/>
public event EventHandler<ValueChangingEventArgs<TimeSpan>>? ValueChanging;

/// <inheritdoc/>
public event EventHandler<ValueChangedEventArgs<TimeSpan>>? ValueChanged;

/// <inheritdoc/>
object? IValue.GetValue () => Value;

/// <summary>
/// Called when the <see cref="Value"/> is changing.
/// Allows derived classes to cancel the change.
/// </summary>
/// <param name="args">The event arguments.</param>
/// <returns><see langword="true"/> to cancel the change; otherwise <see langword="false"/>.</returns>
protected virtual bool OnValueChanging (ValueChangingEventArgs<TimeSpan> args)
{
return false;
}

/// <summary>
/// Called when the <see cref="Value"/> has changed.
/// Allows derived classes to react to value changes.
/// </summary>
/// <param name="args">The event arguments.</param>
protected virtual void OnValueChanged (ValueChangedEventArgs<TimeSpan> args)
{
}
Comment thread
tig marked this conversation as resolved.
Outdated

/// <summary>
/// Raises value events when the text changes through user input.
/// </summary>
private void RaiseValueChangedEvents ()
{
TimeSpan currentValue = TimeProvider.TimeValue;

if (_lastKnownValue == currentValue)
{
return;
}

ValueChangedEventArgs<TimeSpan> changedArgs = new (_lastKnownValue, currentValue);
_lastKnownValue = currentValue;
OnValueChanged (changedArgs);
ValueChanged?.Invoke (this, changedArgs);
}
}
Loading
Loading