Browse Source

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

Nicely done. Added DateTimeEventArgs and improved data and time validation.
Charlie Kindel 5 years ago
parent
commit
03104495a5

+ 2 - 2
Example/demo.cs

@@ -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,

+ 88 - 6
Terminal.Gui/Views/DateField.cs

@@ -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;
+		}
 	}
 }

+ 4 - 0
Terminal.Gui/Views/TextField.cs

@@ -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)

+ 60 - 14
Terminal.Gui/Views/TimeField.cs

@@ -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);
+		}
 	}
 }

+ 4 - 3
UICatalog/Scenarios/Text.cs

@@ -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,

+ 64 - 3
UICatalog/Scenarios/TimeAndDate.cs

@@ -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}";
+		}
 	}
 }