Merge pull request #628 from BDisp/date-time-field

Nicely done. Added DateTimeEventArgs and improved data and time validation.
This commit is contained in:
Charlie Kindel
2020-06-06 10:25:31 -06:00
committed by GitHub
6 changed files with 222 additions and 28 deletions

View File

@@ -183,8 +183,8 @@ static class Demo {
scrollView2,
tf,
new Button (10, 19, "Cancel"),
new TimeField (3, 20, DateTime.Now),
new TimeField (23, 20, DateTime.Now, true),
new TimeField (3, 20, DateTime.Now.TimeOfDay),
new TimeField (23, 20, DateTime.Now.TimeOfDay, true),
new DateField (3, 22, DateTime.Now),
new DateField (23, 22, DateTime.Now, true),
progress,

View File

@@ -18,6 +18,7 @@ namespace Terminal.Gui {
/// The <see cref="DateField"/> <see cref="View"/> provides date editing functionality with mouse support.
/// </remarks>
public class DateField : TextField {
DateTime date;
bool isShort;
int longFieldLen = 10;
int shortFieldLen = 8;
@@ -28,6 +29,17 @@ namespace Terminal.Gui {
int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } }
string Format { get { return isShort ? shortFormat : longFormat; } }
/// <summary>
/// DateChanged event, raised when the Date has changed.
/// </summary>
/// <remarks>
/// This event is raised when the <see cref="Date"/> changes.
/// </remarks>
/// <remarks>
/// The passed <see cref="EventArgs"/> is a <see cref="DateTimeEventArgs"/> containing the old, new value and format.
/// </remarks>
public event Action<DateTimeEventArgs<DateTime>> DateChanged;
/// <summary>
/// Initializes a new instance of <see cref="DateField"/> using <see cref="LayoutStyle.Absolute"/> layout.
/// </summary>
@@ -70,8 +82,12 @@ namespace Terminal.Gui {
void DateField_Changed (object sender, ustring e)
{
if (!DateTime.TryParseExact (GetDate (Text).ToString (), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result))
try {
if (!DateTime.TryParseExact (GetDate (Text).ToString (), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result))
Text = e;
} catch (Exception) {
Text = e;
}
}
string GetInvarianteFormat ()
@@ -105,11 +121,19 @@ namespace Terminal.Gui {
/// </remarks>
public DateTime Date {
get {
if (!DateTime.TryParseExact (Text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) return new DateTime ();
return result;
return date;
}
set {
if (ReadOnly)
return;
var oldData = date;
date = value;
this.Text = value.ToString (Format);
var args = new DateTimeEventArgs<DateTime> (oldData, value, Format);
if (oldData != value) {
OnDateChanged (args);
}
}
}
@@ -145,6 +169,10 @@ namespace Terminal.Gui {
bool SetText (ustring text)
{
if (text.IsEmpty) {
return false;
}
ustring [] vals = text.Split (ustring.Make (sepChar));
ustring [] frm = ustring.Make (Format).Split (ustring.Make (sepChar));
bool isValidDate = true;
@@ -174,12 +202,12 @@ namespace Terminal.Gui {
vals [idx] = day.ToString ();
} else
day = Int32.Parse (vals [idx].ToString ());
string date = GetDate (month, day, year, frm);
Text = date;
string d = GetDate (month, day, year, frm);
if (!DateTime.TryParseExact (date, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
if (!DateTime.TryParseExact (d, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
!isValidDate)
return false;
Date = result;
return true;
}
@@ -195,6 +223,8 @@ namespace Terminal.Gui {
if (!isShort && year.ToString ().Length == 2) {
var y = DateTime.Now.Year.ToString ();
date += y.Substring (0, 2) + year.ToString ();
} else if (isShort && year.ToString ().Length == 4) {
date += $"{year.ToString ().Substring (2, 2)}";
} else {
date += $"{year,2:00}";
}
@@ -270,11 +300,17 @@ namespace Terminal.Gui {
switch (kb.Key) {
case Key.DeleteChar:
case Key.ControlD:
if (ReadOnly)
return true;
SetText ('0');
break;
case Key.Delete:
case Key.Backspace:
if (ReadOnly)
return true;
SetText ('0');
DecCursorPosition ();
break;
@@ -304,6 +340,10 @@ namespace Terminal.Gui {
// Ignore non-numeric characters.
if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9'))
return false;
if (ReadOnly)
return true;
if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
IncCursorPosition ();
return true;
@@ -328,5 +368,47 @@ namespace Terminal.Gui {
AdjCursorPosition ();
return true;
}
/// <summary>
/// Virtual method that will invoke the <see cref="DateChanged"/> with a <see cref="DateTimeEventArgs"/>.
/// </summary>
/// <param name="args">The arguments of the <see cref="DateTimeEventArgs"/></param>
public virtual void OnDateChanged (DateTimeEventArgs<DateTime> args)
{
DateChanged?.Invoke (args);
}
}
/// <summary>
/// Handled the <see cref="EventArgs"/> for <see cref="DateField"/> or <see cref="TimeField"/> events.
/// </summary>
public class DateTimeEventArgs<T> : EventArgs {
/// <summary>
/// The old <see cref="DateField"/> or <see cref="TimeField"/> value.
/// </summary>
public T OldValue {get;}
/// <summary>
/// The new <see cref="DateField"/> or <see cref="TimeField"/> value.
/// </summary>
public T NewValue { get; }
/// <summary>
/// The <see cref="DateField"/> or <see cref="TimeField"/> format.
/// </summary>
public string Format { get; }
/// <summary>
/// Initializes a new instance of <see cref="DateTimeEventArgs"/>
/// </summary>
/// <param name="oldValue">The old <see cref="DateField"/> or <see cref="TimeField"/> value.</param>
/// <param name="newValue">The new <see cref="DateField"/> or <see cref="TimeField"/> value.</param>
/// <param name="format">The <see cref="DateField"/> or <see cref="TimeField"/> format.</param>
public DateTimeEventArgs (T oldValue, T newValue, string format)
{
OldValue = oldValue;
NewValue = newValue;
Format = format;
}
}
}

View File

@@ -132,6 +132,10 @@ namespace Terminal.Gui {
return;
var oldText = ustring.Make (text);
if (oldText == value)
return;
text = TextModel.ToRunes (value);
if (!Secret && !isFromHistory) {
if (historyText == null)

View File

@@ -17,6 +17,7 @@ namespace Terminal.Gui {
/// The <see cref="TimeField"/> <see cref="View"/> provides time editing functionality with mouse support.
/// </remarks>
public class TimeField : TextField {
TimeSpan time;
bool isShort;
int longFieldLen = 8;
@@ -28,6 +29,16 @@ namespace Terminal.Gui {
int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } }
string Format { get { return isShort ? shortFormat : longFormat; } }
/// <summary>
/// TimeChanged event, raised when the Date has changed.
/// </summary>
/// <remarks>
/// This event is raised when the <see cref="Time"/> changes.
/// </remarks>
/// <remarks>
/// The passed <see cref="EventArgs"/> is a <see cref="DateTimeEventArgs"/> containing the old, new value and format.
/// </remarks>
public event Action<DateTimeEventArgs<TimeSpan>> TimeChanged;
/// <summary>
/// Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Absolute"/> positioning.
@@ -36,7 +47,7 @@ namespace Terminal.Gui {
/// <param name="y">The y coordinate.</param>
/// <param name="time">Initial time.</param>
/// <param name="isShort">If true, the seconds are hidden. Sets the <see cref="IsShortFormat"/> property.</param>
public TimeField (int x, int y, DateTime time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "")
public TimeField (int x, int y, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "")
{
this.isShort = isShort;
Initialize (time);
@@ -46,7 +57,7 @@ namespace Terminal.Gui {
/// Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.
/// </summary>
/// <param name="time">Initial time</param>
public TimeField (DateTime time) : base (string.Empty)
public TimeField (TimeSpan time) : base (string.Empty)
{
this.isShort = true;
Width = FieldLen + 2;
@@ -56,14 +67,14 @@ namespace Terminal.Gui {
/// <summary>
/// Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.
/// </summary>
public TimeField () : this (time: DateTime.MinValue) { }
public TimeField () : this (time: TimeSpan.MinValue) { }
void Initialize (DateTime time)
void Initialize (TimeSpan time)
{
CultureInfo cultureInfo = CultureInfo.CurrentCulture;
sepChar = cultureInfo.DateTimeFormat.TimeSeparator;
longFormat = $" HH{sepChar}mm{sepChar}ss";
shortFormat = $" HH{sepChar}mm";
longFormat = $" hh\\{sepChar}mm\\{sepChar}ss";
shortFormat = $" hh\\{sepChar}mm";
CursorPosition = 1;
Time = time;
Changed += TimeField_Changed;
@@ -71,8 +82,12 @@ namespace Terminal.Gui {
void TimeField_Changed (object sender, ustring e)
{
if (!DateTime.TryParseExact (Text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result))
try {
if (!TimeSpan.TryParseExact (Text.ToString ().Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result))
Text = e;
} catch (Exception) {
Text = e;
}
}
/// <summary>
@@ -80,13 +95,21 @@ namespace Terminal.Gui {
/// </summary>
/// <remarks>
/// </remarks>
public DateTime Time {
public TimeSpan Time {
get {
if (!DateTime.TryParseExact (Text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) return new DateTime ();
return result;
return time;
}
set {
this.Text = value.ToString (Format);
if (ReadOnly)
return;
var oldTime = time;
time = value;
this.Text = " " + value.ToString (Format.Trim ());
var args = new DateTimeEventArgs<TimeSpan> (oldTime, value, Format);
if (oldTime != value) {
OnTimeChanged (args);
}
}
}
@@ -122,6 +145,10 @@ namespace Terminal.Gui {
bool SetText (ustring text)
{
if (text.IsEmpty) {
return false;
}
ustring [] vals = text.Split (ustring.Make (sepChar));
bool isValidTime = true;
int hour = Int32.Parse (vals [0].ToString ());
@@ -154,12 +181,12 @@ namespace Terminal.Gui {
second = 59;
vals [2] = "59";
}
string time = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}";
Text = time;
string t = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}";
if (!DateTime.TryParseExact (text.ToString (), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
if (!TimeSpan.TryParseExact (t.Trim (), Format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) ||
!isValidTime)
return false;
Time = result;
return true;
}
@@ -191,11 +218,17 @@ namespace Terminal.Gui {
switch (kb.Key) {
case Key.DeleteChar:
case Key.ControlD:
if (ReadOnly)
return true;
SetText ('0');
break;
case Key.Delete:
case Key.Backspace:
if (ReadOnly)
return true;
SetText ('0');
DecCursorPosition ();
break;
@@ -225,6 +258,10 @@ namespace Terminal.Gui {
// Ignore non-numeric characters.
if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9'))
return false;
if (ReadOnly)
return true;
if (SetText (TextModel.ToRunes (ustring.Make ((uint)kb.Key)).First ()))
IncCursorPosition ();
return true;
@@ -249,5 +286,14 @@ namespace Terminal.Gui {
AdjCursorPosition ();
return true;
}
/// <summary>
/// Virtual method that will invoke the <see cref="TimeChanged"/> with a <see cref="DateTimeEventArgs"/>.
/// </summary>
/// <param name="args">The arguments of the <see cref="DateTimeEventArgs"/></param>
public virtual void OnTimeChanged (DateTimeEventArgs<TimeSpan> args)
{
TimeChanged?.Invoke (args);
}
}
}

View File

@@ -1,4 +1,5 @@
using System.Text;
using System;
using System.Text;
using Terminal.Gui;
namespace UICatalog {
@@ -45,8 +46,8 @@ namespace UICatalog {
};
Win.Add (dateField);
var timeField = new TimeField (System.DateTime.Now) {
X = Pos.Right(dateField) + 5,
var timeField = new TimeField (DateTime.Now.TimeOfDay) {
X = Pos.Right (dateField) + 5,
Y = Pos.Bottom (hexView) + 1,
Width = Dim.Percent (40),
ColorScheme = Colors.Dialog,

View File

@@ -6,22 +6,31 @@ namespace UICatalog {
[ScenarioCategory ("Controls")]
[ScenarioCategory ("Bug Repro")] // Issue #246
class TimeAndDate : Scenario {
Label lblOldTime;
Label lblNewTime;
Label lblTimeFmt;
Label lblOldDate;
Label lblNewDate;
Label lblDateFmt;
public override void Setup ()
{
var longTime = new TimeField (DateTime.Now) {
var longTime = new TimeField (DateTime.Now.TimeOfDay) {
X = Pos.Center (),
Y = 2,
IsShortFormat = false,
ReadOnly = false,
};
longTime.TimeChanged += TimeChanged;
Win.Add (longTime);
var shortTime = new TimeField (DateTime.Now) {
var shortTime = new TimeField (DateTime.Now.TimeOfDay) {
X = Pos.Center (),
Y = Pos.Bottom(longTime) + 1,
Y = Pos.Bottom (longTime) + 1,
IsShortFormat = true,
ReadOnly = false,
};
shortTime.TimeChanged += TimeChanged;
Win.Add (shortTime);
var shortDate = new DateField (DateTime.Now) {
@@ -30,6 +39,7 @@ namespace UICatalog {
IsShortFormat = true,
ReadOnly = true,
};
shortDate.DateChanged += DateChanged;
Win.Add (shortDate);
var longDate = new DateField (DateTime.Now) {
@@ -38,8 +48,45 @@ namespace UICatalog {
IsShortFormat = false,
ReadOnly = true,
};
longDate.DateChanged += DateChanged;
Win.Add (longDate);
lblOldTime = new Label ("Old Time: ") {
X = Pos.Center (),
Y = Pos.Bottom (longDate) + 1
};
Win.Add (lblOldTime);
lblNewTime = new Label ("New Time: ") {
X = Pos.Center (),
Y = Pos.Bottom (lblOldTime) + 1
};
Win.Add (lblNewTime);
lblTimeFmt = new Label ("Time Format: ") {
X = Pos.Center (),
Y = Pos.Bottom (lblNewTime) + 1
};
Win.Add (lblTimeFmt);
lblOldDate = new Label ("Old Date: ") {
X = Pos.Center (),
Y = Pos.Bottom (lblTimeFmt) + 2
};
Win.Add (lblOldDate);
lblNewDate = new Label ("New Date: ") {
X = Pos.Center (),
Y = Pos.Bottom (lblOldDate) + 1
};
Win.Add (lblNewDate);
lblDateFmt = new Label ("Date Format: ") {
X = Pos.Center (),
Y = Pos.Bottom (lblNewDate) + 1
};
Win.Add (lblDateFmt);
Win.Add (new Button ("Swap Long/Short & Read/Read Only") {
X = Pos.Center (),
Y = Pos.Bottom (Win) - 5,
@@ -58,5 +105,19 @@ namespace UICatalog {
}
});
}
private void TimeChanged (DateTimeEventArgs<TimeSpan> e)
{
lblOldTime.Text = $"Old Time: {e.OldValue}";
lblNewTime.Text = $"New Time: {e.NewValue}";
lblTimeFmt.Text = $"Time Format: {e.Format}";
}
private void DateChanged (DateTimeEventArgs<DateTime> e)
{
lblOldDate.Text = $"Old Date: {e.OldValue}";
lblNewDate.Text = $"New Date: {e.NewValue}";
lblDateFmt.Text = $"Date Format: {e.Format}";
}
}
}