Przeglądaj źródła

Fixes #3166. TimeField should use TextChanging instead of TextChanged event. (#3167)

* Reduces indentation by remove the namespace braces.

* Prefix private fields with underscore.

* Rename to SetInitialProperties.

* Reduces indentation and removes unused using.

* Using TextChanging instead of TextChanged event.

* Fixes #3160. TextField doesn't update correctly the CursorPosition on Paste.

* Rearrange code.

* Fix key bindings.

* Fix Non-numerics are ignored.

* Add format normalization.

* Improve cursor position adjustments.

* Ensures clear selection if it isn't selecting.

* Provides more text validation.

* Unit test with selection.

---------

Co-authored-by: Tig <[email protected]>
BDisp 1 rok temu
rodzic
commit
96f7597050
2 zmienionych plików z 496 dodań i 365 usunięć
  1. 337 277
      Terminal.Gui/Views/TimeField.cs
  2. 159 88
      UnitTests/Views/TimeFieldTests.cs

+ 337 - 277
Terminal.Gui/Views/TimeField.cs

@@ -9,334 +9,394 @@ using System.Globalization;
 using System.Linq;
 using System.Text;
 
-namespace Terminal.Gui {
+namespace Terminal.Gui;
+/// <summary>
+///   Time editing <see cref="View"/>
+/// </summary>
+/// <remarks>
+///   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;
+	int _shortFieldLen = 5;
+	string _sepChar;
+	string _longFormat;
+	string _shortFormat;
+
+	int _fieldLen => _isShort ? _shortFieldLen : _longFieldLen;
+	string _format => _isShort ? _shortFormat : _longFormat;
+
 	/// <summary>
-	///   Time editing <see cref="View"/>
+	///   TimeChanged event, raised when the Date has changed.
 	/// </summary>
 	/// <remarks>
-	///   The <see cref="TimeField"/> <see cref="View"/> provides time editing functionality with mouse support.
+	///   This event is raised when the <see cref="Time"/> changes.
 	/// </remarks>
-	public class TimeField : TextField {
-		TimeSpan time;
-		bool isShort;
-
-		int longFieldLen = 8;
-		int shortFieldLen = 5;
-		string sepChar;
-		string longFormat;
-		string shortFormat;
-
-		int fieldLen => isShort ? shortFieldLen : longFieldLen;
-		string format => 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{T}"/> containing the old value, new value, and format string.
-		/// </remarks>
-		public event EventHandler<DateTimeEventArgs<TimeSpan>> TimeChanged;
-
-		/// <summary>
-		///    Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Absolute"/> positioning.
-		/// </summary>
-		/// <param name="x">The x coordinate.</param>
-		/// <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, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "")
-		{
-			Initialize (time, isShort);
-		}
-
-		/// <summary>
-		///    Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.
-		/// </summary>
-		/// <param name="time">Initial time</param>
-		public TimeField (TimeSpan time) : base (string.Empty)
-		{
-			Width = fieldLen + 2;
-			Initialize (time);
-		}
-
-		/// <summary>
-		///    Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.
-		/// </summary>
-		public TimeField () : this (time: TimeSpan.MinValue) { }
-
-		void Initialize (TimeSpan time, bool isShort = false)
-		{
-			CultureInfo cultureInfo = CultureInfo.CurrentCulture;
-			sepChar = cultureInfo.DateTimeFormat.TimeSeparator;
-			longFormat = $" hh\\{sepChar}mm\\{sepChar}ss";
-			shortFormat = $" hh\\{sepChar}mm";
-			this.isShort = isShort;
-			Time = time;
-			CursorPosition = 1;
-			TextChanged += TextField_TextChanged;
-
-			// Things this view knows how to do
-			AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; });
-			AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (false); return true; });
-			AddCommand (Command.LeftHome, () => MoveHome ());
-			AddCommand (Command.Left, () => MoveLeft ());
-			AddCommand (Command.RightEnd, () => MoveEnd ());
-			AddCommand (Command.Right, () => MoveRight ());
-
-			// Default keybindings for this view
-			KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight);
-			KeyBindings.Add (KeyCode.D | KeyCode.CtrlMask, Command.DeleteCharRight);
-
-			KeyBindings.Add (KeyCode.Delete, Command.DeleteCharLeft);
-			KeyBindings.Add (KeyCode.Backspace, Command.DeleteCharLeft);
-
-			KeyBindings.Add (KeyCode.Home, Command.LeftHome);
-			KeyBindings.Add (KeyCode.A | KeyCode.CtrlMask, Command.LeftHome);
+	/// <remarks>
+	///   The passed <see cref="EventArgs"/> is a <see cref="DateTimeEventArgs{T}"/> containing the old value, new value, and format string.
+	/// </remarks>
+	public event EventHandler<DateTimeEventArgs<TimeSpan>> TimeChanged;
 
-			KeyBindings.Add (KeyCode.CursorLeft, Command.Left);
-			KeyBindings.Add (KeyCode.B | KeyCode.CtrlMask, Command.Left);
+	/// <summary>
+	///    Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Absolute"/> positioning.
+	/// </summary>
+	/// <param name="x">The x coordinate.</param>
+	/// <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, TimeSpan time, bool isShort = false) : base (x, y, isShort ? 7 : 10, "")
+	{
+		SetInitialProperties (time, isShort);
+	}
 
-			KeyBindings.Add (KeyCode.End, Command.RightEnd);
-			KeyBindings.Add (KeyCode.E | KeyCode.CtrlMask, Command.RightEnd);
+	/// <summary>
+	///    Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.
+	/// </summary>
+	/// <param name="time">Initial time</param>
+	public TimeField (TimeSpan time) : base (string.Empty)
+	{
+		Width = _fieldLen + 2;
+		SetInitialProperties (time);
+	}
 
-			KeyBindings.Add (KeyCode.CursorRight, Command.Right);
-			KeyBindings.Add (KeyCode.F | KeyCode.CtrlMask, Command.Right);
-		}
+	/// <summary>
+	///    Initializes a new instance of <see cref="TimeField"/> using <see cref="LayoutStyle.Computed"/> positioning.
+	/// </summary>
+	public TimeField () : this (time: TimeSpan.MinValue) { }
+
+	void SetInitialProperties (TimeSpan time, bool isShort = false)
+	{
+		CultureInfo cultureInfo = CultureInfo.CurrentCulture;
+		_sepChar = cultureInfo.DateTimeFormat.TimeSeparator;
+		_longFormat = $" hh\\{_sepChar}mm\\{_sepChar}ss";
+		_shortFormat = $" hh\\{_sepChar}mm";
+		this._isShort = isShort;
+		Time = time;
+		CursorPosition = 1;
+		TextChanging += TextField_TextChanging;
+
+		// Things this view knows how to do
+		AddCommand (Command.DeleteCharRight, () => { DeleteCharRight (); return true; });
+		AddCommand (Command.DeleteCharLeft, () => { DeleteCharLeft (false); return true; });
+		AddCommand (Command.LeftHome, () => MoveHome ());
+		AddCommand (Command.Left, () => MoveLeft ());
+		AddCommand (Command.RightEnd, () => MoveEnd ());
+		AddCommand (Command.Right, () => MoveRight ());
+
+		// Default keybindings for this view
+		KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight);
+		KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight);
+
+		KeyBindings.Add (Key.Backspace, Command.DeleteCharLeft);
+		KeyBindings.Add (Key.D.WithAlt, Command.DeleteCharLeft);
+
+		KeyBindings.Add (Key.Home, Command.LeftHome);
+		KeyBindings.Add (Key.A.WithCtrl, Command.LeftHome);
+
+		KeyBindings.Add (Key.CursorLeft, Command.Left);
+		KeyBindings.Add (Key.B.WithCtrl, Command.Left);
+
+		KeyBindings.Add (Key.End, Command.RightEnd);
+		KeyBindings.Add (Key.E.WithCtrl, Command.RightEnd);
+
+		KeyBindings.Add (Key.CursorRight, Command.Right);
+		KeyBindings.Add (Key.F.WithCtrl, Command.Right);
+	}
 
-		void TextField_TextChanged (object sender, TextChangedEventArgs e)
-		{
-			try {
-				if (!TimeSpan.TryParseExact (Text.Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result))
-					Text = e.OldValue;
-			} catch (Exception) {
-				Text = e.OldValue;
+	void TextField_TextChanging (object sender, TextChangingEventArgs e)
+	{
+		try {
+			int spaces = 0;
+			for (int i = 0; i < e.NewText.Length; i++) {
+				if (e.NewText [i] == ' ') {
+					spaces++;
+				} else {
+					break;
+				}
 			}
-		}
-
-		/// <summary>
-		///   Gets or sets the time of the <see cref="TimeField"/>.
-		/// </summary>
-		/// <remarks>
-		/// </remarks>
-		public TimeSpan Time {
-			get {
-				return time;
+			spaces += _fieldLen;
+			string trimedText = e.NewText [..spaces];
+			spaces -= _fieldLen;
+			trimedText = trimedText.Replace (new string (' ', spaces), " ");
+			if (trimedText != e.NewText) {
+				e.NewText = trimedText;
 			}
-			set {
-				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);
-				}
+			if (!TimeSpan.TryParseExact (e.NewText.Trim (), _format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result)) {
+				e.Cancel = true;
 			}
+			AdjCursorPosition (CursorPosition, true);
+		} catch (Exception) {
+			e.Cancel = true;
 		}
+	}
 
-		/// <summary>
-		/// Get or sets whether <see cref="TimeField"/> uses the short or long time format.
-		/// </summary>
-		public bool IsShortFormat {
-			get => isShort;
-			set {
-				isShort = value;
-				if (isShort)
-					Width = 7;
-				else
-					Width = 10;
-				var ro = ReadOnly;
-				if (ro)
-					ReadOnly = false;
-				SetText (Text);
-				ReadOnly = ro;
-				SetNeedsDisplay ();
-			}
+	/// <summary>
+	///   Gets or sets the time of the <see cref="TimeField"/>.
+	/// </summary>
+	/// <remarks>
+	/// </remarks>
+	public TimeSpan Time {
+		get {
+			return _time;
 		}
+		set {
+			if (ReadOnly)
+				return;
 
-		/// <inheritdoc/>
-		public override int CursorPosition {
-			get => base.CursorPosition;
-			set {
-				base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1);
+			var oldTime = _time;
+			_time = value;
+			this.Text = " " + value.ToString (_format.Trim ());
+			var args = new DateTimeEventArgs<TimeSpan> (oldTime, value, _format);
+			if (oldTime != value) {
+				OnTimeChanged (args);
 			}
 		}
+	}
 
-		bool SetText (Rune key)
-		{
-			var text = Text.EnumerateRunes ().ToList ();
-			var newText = text.GetRange (0, CursorPosition);
-			newText.Add (key);
-			if (CursorPosition < fieldLen)
-				newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList ();
-			return SetText (StringExtensions.ToString (newText));
+	/// <summary>
+	/// Get or sets whether <see cref="TimeField"/> uses the short or long time format.
+	/// </summary>
+	public bool IsShortFormat {
+		get => _isShort;
+		set {
+			_isShort = value;
+			if (_isShort)
+				Width = 7;
+			else
+				Width = 10;
+			var ro = ReadOnly;
+			if (ro)
+				ReadOnly = false;
+			SetText (Text);
+			ReadOnly = ro;
+			SetNeedsDisplay ();
 		}
+	}
 
-		bool SetText (string text)
-		{
-			if (string.IsNullOrEmpty (text)) {
-				return false;
-			}
+	/// <inheritdoc/>
+	public override int CursorPosition {
+		get => base.CursorPosition;
+		set {
+			base.CursorPosition = Math.Max (Math.Min (value, _fieldLen), 1);
+		}
+	}
 
-			string [] vals = text.Split (sepChar);
-			bool isValidTime = true;
-			int hour = Int32.Parse (vals [0]);
-			int minute = Int32.Parse (vals [1]);
-			int second = isShort ? 0 : vals.Length > 2 ? Int32.Parse (vals [2].ToString ()) : 0;
-			if (hour < 0) {
-				isValidTime = false;
-				hour = 0;
-				vals [0] = "0";
-			} else if (hour > 23) {
-				isValidTime = false;
-				hour = 23;
-				vals [0] = "23";
-			}
-			if (minute < 0) {
-				isValidTime = false;
-				minute = 0;
-				vals [1] = "0";
-			} else if (minute > 59) {
-				isValidTime = false;
-				minute = 59;
-				vals [1] = "59";
-			}
-			if (second < 0) {
-				isValidTime = false;
-				second = 0;
-				vals [2] = "0";
-			} else if (second > 59) {
-				isValidTime = false;
-				second = 59;
-				vals [2] = "59";
-			}
-			string t = isShort ? $" {hour,2:00}{sepChar}{minute,2:00}" : $" {hour,2:00}{sepChar}{minute,2:00}{sepChar}{second,2:00}";
+	bool SetText (Rune key)
+	{
+		var text = Text.EnumerateRunes ().ToList ();
+		var newText = text.GetRange (0, CursorPosition);
+		newText.Add (key);
+		if (CursorPosition < _fieldLen)
+			newText = [.. newText, .. text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))];
+		return SetText (StringExtensions.ToString (newText));
+	}
 
-			if (!TimeSpan.TryParseExact (t.Trim (), format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) ||
-				!isValidTime)
-				return false;
-			Time = result;
-			return true;
+	bool SetText (string text)
+	{
+		if (string.IsNullOrEmpty (text)) {
+			return false;
 		}
 
-		void IncCursorPosition ()
-		{
-			if (CursorPosition == fieldLen)
-				return;
-			if (Text [++CursorPosition] == sepChar.ToCharArray () [0])
-				CursorPosition++;
+		text = NormalizeFormat (text);
+		string [] vals = text.Split (_sepChar);
+		bool isValidTime = true;
+		int hour = Int32.Parse (vals [0]);
+		int minute = Int32.Parse (vals [1]);
+		int second = _isShort ? 0 : vals.Length > 2 ? Int32.Parse (vals [2]) : 0;
+		if (hour < 0) {
+			isValidTime = false;
+			hour = 0;
+			vals [0] = "0";
+		} else if (hour > 23) {
+			isValidTime = false;
+			hour = 23;
+			vals [0] = "23";
 		}
+		if (minute < 0) {
+			isValidTime = false;
+			minute = 0;
+			vals [1] = "0";
+		} else if (minute > 59) {
+			isValidTime = false;
+			minute = 59;
+			vals [1] = "59";
+		}
+		if (second < 0) {
+			isValidTime = false;
+			second = 0;
+			vals [2] = "0";
+		} else if (second > 59) {
+			isValidTime = false;
+			second = 59;
+			vals [2] = "59";
+		}
+		string t = _isShort ? $" {hour,2:00}{_sepChar}{minute,2:00}" : $" {hour,2:00}{_sepChar}{minute,2:00}{_sepChar}{second,2:00}";
 
-		void DecCursorPosition ()
-		{
-			if (CursorPosition == 1)
-				return;
-			if (Text [--CursorPosition] == sepChar.ToCharArray () [0])
-				CursorPosition--;
+		if (!TimeSpan.TryParseExact (t.Trim (), _format.Trim (), CultureInfo.CurrentCulture, TimeSpanStyles.None, out TimeSpan result) ||
+			!isValidTime) {
+			return false;
 		}
+		Time = result;
+		return true;
+	}
 
-		void AdjCursorPosition ()
-		{
-			if (Text [CursorPosition] == sepChar.ToCharArray () [0])
-				CursorPosition++;
+	string NormalizeFormat (string text, string fmt = null, string sepChar = null)
+	{
+		if (string.IsNullOrEmpty (fmt)) {
+			fmt = _format;
+		}
+		fmt = fmt.Replace ("\\", "");
+		if (string.IsNullOrEmpty (sepChar)) {
+			sepChar = _sepChar;
+		}
+		if (fmt.Length != text.Length) {
+			return text;
 		}
 
-		///<inheritdoc/>
-		public override bool OnProcessKeyDown (Key a)
-		{
-			// Ignore non-numeric characters.
-			if (a.KeyCode is >= (KeyCode)(int)KeyCode.D0 and <= (KeyCode)(int)KeyCode.D9) {
-				if (!ReadOnly) {
-					if (SetText ((Rune)a)) {
-						IncCursorPosition ();
-					}
-				}
-				return true;
+		var fmtText = text.ToCharArray ();
+		for (int i = 0; i < text.Length; i++) {
+			var c = fmt [i];
+			if (c.ToString () == sepChar && text [i].ToString () != sepChar) {
+				fmtText [i] = c;
 			}
+		}
 
-			if (a.IsKeyCodeAtoZ) {
-				return true;
-			}
-			
-			return false;
+		return new string (fmtText);
+	}
+
+	void IncCursorPosition ()
+	{
+		if (CursorPosition >= _fieldLen) {
+			CursorPosition = _fieldLen;
+			return;
 		}
+		CursorPosition++;
+		AdjCursorPosition (CursorPosition);
+	}
 
-		bool MoveRight ()
-		{
-			IncCursorPosition ();
-			return true;
+	void DecCursorPosition ()
+	{
+		if (CursorPosition <= 1) {
+			CursorPosition = 1;
+			return;
 		}
+		CursorPosition--;
+		AdjCursorPosition (CursorPosition, false);
+	}
 
-		new bool MoveEnd ()
-		{
-			CursorPosition = fieldLen;
-			return true;
+	void AdjCursorPosition (int point, bool increment = true)
+	{
+		var newPoint = point;
+		if (point > _fieldLen) {
+			newPoint = _fieldLen;
+		}
+		if (point < 1) {
+			newPoint = 1;
+		}
+		if (newPoint != point) {
+			CursorPosition = newPoint;
 		}
 
-		bool MoveLeft ()
-		{
-			DecCursorPosition ();
-			return true;
+		while (Text [CursorPosition] == _sepChar [0]) {
+			if (increment) {
+				CursorPosition++;
+			} else {
+				CursorPosition--;
+			}
 		}
+	}
 
-		bool MoveHome ()
-		{
-			// Home, C-A
-			CursorPosition = 1;
+	///<inheritdoc/>
+	public override bool OnProcessKeyDown (Key a)
+	{
+		// Ignore non-numeric characters.
+		if (a.KeyCode is >= (KeyCode)(int)KeyCode.D0 and <= (KeyCode)(int)KeyCode.D9) {
+			if (!ReadOnly) {
+				if (SetText ((Rune)a)) {
+					IncCursorPosition ();
+				}
+			}
 			return true;
 		}
 
-		/// <inheritdoc/>
-		public override void DeleteCharLeft (bool useOldCursorPos = true)
-		{
-			if (ReadOnly)
-				return;
+		return false;
+	}
 
-			SetText ((Rune)'0');
-			DecCursorPosition ();
+	bool MoveRight ()
+	{
+		ClearAllSelection ();
+		IncCursorPosition ();
+		return true;
+	}
+
+	new bool MoveEnd ()
+	{
+		ClearAllSelection ();
+		CursorPosition = _fieldLen;
+		return true;
+	}
+
+	bool MoveLeft ()
+	{
+		ClearAllSelection ();
+		DecCursorPosition ();
+		return true;
+	}
+
+	bool MoveHome ()
+	{
+		// Home, C-A
+		ClearAllSelection ();
+		CursorPosition = 1;
+		return true;
+	}
+
+	/// <inheritdoc/>
+	public override void DeleteCharLeft (bool useOldCursorPos = true)
+	{
+		if (ReadOnly) {
 			return;
 		}
 
-		/// <inheritdoc/>
-		public override void DeleteCharRight ()
-		{
-			if (ReadOnly)
-				return;
+		ClearAllSelection ();
+		SetText ((Rune)'0');
+		DecCursorPosition ();
+		return;
+	}
 
-			SetText ((Rune)'0');
+	/// <inheritdoc/>
+	public override void DeleteCharRight ()
+	{
+		if (ReadOnly) {
 			return;
 		}
 
-		///<inheritdoc/>
-		public override bool MouseEvent (MouseEvent ev)
-		{
-			if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked))
-				return false;
-			if (!HasFocus)
-				SetFocus ();
-
-			var point = ev.X;
-			if (point > fieldLen)
-				point = fieldLen;
-			if (point < 1)
-				point = 1;
-			CursorPosition = point;
-			AdjCursorPosition ();
-			return true;
-		}
+		ClearAllSelection ();
+		SetText ((Rune)'0');
+		return;
+	}
+
+	///<inheritdoc/>
+	public override bool MouseEvent (MouseEvent ev)
+	{
+		var result = base.MouseEvent (ev);
 
-		/// <summary>
-		/// Event firing method that invokes the <see cref="TimeChanged"/> event.
-		/// </summary>
-		/// <param name="args">The event arguments</param>
-		public virtual void OnTimeChanged (DateTimeEventArgs<TimeSpan> args)
-		{
-			TimeChanged?.Invoke (this, args);
+		if (result && SelectedLength == 0 && ev.Flags.HasFlag (MouseFlags.Button1Pressed)) {
+			int point = ev.X;
+			AdjCursorPosition (point, true);
 		}
+		return result;
+	}
+
+	/// <summary>
+	/// Event firing method that invokes the <see cref="TimeChanged"/> event.
+	/// </summary>
+	/// <param name="args">The event arguments</param>
+	public virtual void OnTimeChanged (DateTimeEventArgs<TimeSpan> args)
+	{
+		TimeChanged?.Invoke (this, args);
 	}
-}
+}

+ 159 - 88
UnitTests/Views/TimeFieldTests.cs

@@ -1,99 +1,170 @@
 using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
 using Xunit;
 
-namespace Terminal.Gui.ViewsTests {
-	public class TimeFieldTests {
-		[Fact]
-		public void Constructors_Defaults ()
-		{
-			var tf = new TimeField ();
-			Assert.False (tf.IsShortFormat);
-			Assert.Equal (TimeSpan.MinValue, tf.Time);
-			Assert.Equal (1, tf.CursorPosition);
-			Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame);
+namespace Terminal.Gui.ViewsTests;
+public class TimeFieldTests {
+	[Fact]
+	public void Constructors_Defaults ()
+	{
+		var tf = new TimeField ();
+		Assert.False (tf.IsShortFormat);
+		Assert.Equal (TimeSpan.MinValue, tf.Time);
+		Assert.Equal (1, tf.CursorPosition);
+		Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame);
 
-			var time = DateTime.Now.TimeOfDay;
-			tf = new TimeField (time);
-			Assert.False (tf.IsShortFormat);
-			Assert.Equal (time, tf.Time);
-			Assert.Equal (1, tf.CursorPosition);
-			Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame);
+		var time = DateTime.Now.TimeOfDay;
+		tf = new TimeField (time);
+		Assert.False (tf.IsShortFormat);
+		Assert.Equal (time, tf.Time);
+		Assert.Equal (1, tf.CursorPosition);
+		Assert.Equal (new Rect (0, 0, 10, 1), tf.Frame);
 
-			tf = new TimeField (1, 2, time);
-			Assert.False (tf.IsShortFormat);
-			Assert.Equal (time, tf.Time);
-			Assert.Equal (1, tf.CursorPosition);
-			Assert.Equal (new Rect (1, 2, 10, 1), tf.Frame);
+		tf = new TimeField (1, 2, time);
+		Assert.False (tf.IsShortFormat);
+		Assert.Equal (time, tf.Time);
+		Assert.Equal (1, tf.CursorPosition);
+		Assert.Equal (new Rect (1, 2, 10, 1), tf.Frame);
 
-			tf = new TimeField (3, 4, time, true);
-			Assert.True (tf.IsShortFormat);
-			Assert.Equal (time, tf.Time);
-			Assert.Equal (1, tf.CursorPosition);
-			Assert.Equal (new Rect (3, 4, 7, 1), tf.Frame);
+		tf = new TimeField (3, 4, time, true);
+		Assert.True (tf.IsShortFormat);
+		Assert.Equal (time, tf.Time);
+		Assert.Equal (1, tf.CursorPosition);
+		Assert.Equal (new Rect (3, 4, 7, 1), tf.Frame);
 
-			tf.IsShortFormat = false;
-			Assert.Equal (new Rect (3, 4, 10, 1), tf.Frame);
-			Assert.Equal (10, tf.Width);
-		}
+		tf.IsShortFormat = false;
+		Assert.Equal (new Rect (3, 4, 10, 1), tf.Frame);
+		Assert.Equal (10, tf.Width);
+	}
+
+	[Fact]
+	public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format ()
+	{
+		var tf = new TimeField ();
+		Assert.Equal (1, tf.CursorPosition);
+		tf.CursorPosition = 0;
+		Assert.Equal (1, tf.CursorPosition);
+		tf.CursorPosition = 9;
+		Assert.Equal (8, tf.CursorPosition);
+		tf.IsShortFormat = true;
+		tf.CursorPosition = 0;
+		Assert.Equal (1, tf.CursorPosition);
+		tf.CursorPosition = 6;
+		Assert.Equal (5, tf.CursorPosition);
+	}
+
+	[Fact]
+	public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format_After_Selection ()
+	{
+		var tf = new TimeField ();
+		// Start selection
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft | KeyCode.ShiftMask)));
+		Assert.Equal (1, tf.SelectedStart);
+		Assert.Equal (1, tf.SelectedLength);
+		Assert.Equal (0, tf.CursorPosition);
+		// Without selection
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft)));
+		Assert.Equal (-1, tf.SelectedStart);
+		Assert.Equal (0, tf.SelectedLength);
+		Assert.Equal (1, tf.CursorPosition);
+		tf.CursorPosition = 8;
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask)));
+		Assert.Equal (8, tf.SelectedStart);
+		Assert.Equal (1, tf.SelectedLength);
+		Assert.Equal (9, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight)));
+		Assert.Equal (-1, tf.SelectedStart);
+		Assert.Equal (0, tf.SelectedLength);
+		Assert.Equal (8, tf.CursorPosition);
+		Assert.False (tf.IsShortFormat);
+		tf.IsShortFormat = true;
+		Assert.Equal (5, tf.CursorPosition);
+		// Start selection
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask)));
+		Assert.Equal (5, tf.SelectedStart);
+		Assert.Equal (1, tf.SelectedLength);
+		Assert.Equal (6, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight)));
+		Assert.Equal (-1, tf.SelectedStart);
+		Assert.Equal (0, tf.SelectedLength);
+		Assert.Equal (5, tf.CursorPosition);
+	}
+
+	[Fact]
+	public void KeyBindings_Command ()
+	{
+		TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19"));
+		tf.ReadOnly = true;
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete)));
+		Assert.Equal (" 12:12:19", tf.Text);
+		tf.ReadOnly = false;
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.D | KeyCode.CtrlMask)));
+		Assert.Equal (" 02:12:19", tf.Text);
+		tf.CursorPosition = 4;
+		tf.ReadOnly = true;
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete)));
+		Assert.Equal (" 02:12:19", tf.Text);
+		tf.ReadOnly = false;
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.Backspace)));
+		Assert.Equal (" 02:02:19", tf.Text);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.Home)));
+		Assert.Equal (1, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.End)));
+		Assert.Equal (8, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.A | KeyCode.CtrlMask)));
+		Assert.Equal (1, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.E | KeyCode.CtrlMask)));
+		Assert.Equal (8, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft)));
+		Assert.Equal (7, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight)));
+		Assert.Equal (8, tf.CursorPosition);
+		// Non-numerics are ignored
+		Assert.False (tf.NewKeyDownEvent (new (KeyCode.A)));
+		tf.ReadOnly = true;
+		tf.CursorPosition = 1;
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1)));
+		Assert.Equal (" 02:02:19", tf.Text);
+		tf.ReadOnly = false;
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1)));
+		Assert.Equal (" 12:02:19", tf.Text);
+		Assert.Equal (2, tf.CursorPosition);
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.D | KeyCode.AltMask)));
+		Assert.Equal (" 10:02:19", tf.Text);
+	}
 
-		[Fact]
-		public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format ()
-		{
-			var tf = new TimeField ();
-			Assert.Equal (1, tf.CursorPosition);
-			tf.CursorPosition = 0;
-			Assert.Equal (1, tf.CursorPosition);
-			tf.CursorPosition = 9;
-			Assert.Equal (8, tf.CursorPosition);
-			tf.IsShortFormat = true;
-			tf.CursorPosition = 0;
-			Assert.Equal (1, tf.CursorPosition);
-			tf.CursorPosition = 6;
-			Assert.Equal (5, tf.CursorPosition);
-		}
+	[Fact]
+	public void Typing_With_Selection_Normalize_Format ()
+	{
+		TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19"));
+		// Start selection at before the first separator :
+		tf.CursorPosition = 2;
+		// Now select the separator :
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask)));
+		Assert.Equal (2, tf.SelectedStart);
+		Assert.Equal (1, tf.SelectedLength);
+		Assert.Equal (3, tf.CursorPosition);
+		// Type 3 over the separator
+		Assert.True (tf.NewKeyDownEvent (new (KeyCode.D3)));
+		// The format was normalized and replaced again with :
+		Assert.Equal (" 12:12:19", tf.Text);
+		Assert.Equal (4, tf.CursorPosition);
+	}
 
-		[Fact]
-		public void KeyBindings_Command ()
-		{
-			TimeField tf = new TimeField (TimeSpan.Parse ("12:12:19"));
-			tf.ReadOnly = true;
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete)));
-			Assert.Equal (" 12:12:19", tf.Text);
-			tf.ReadOnly = false;
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.D | KeyCode.CtrlMask)));
-			Assert.Equal (" 02:12:19", tf.Text);
-			tf.CursorPosition = 4;
-			tf.ReadOnly = true;
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.Delete)));
-			Assert.Equal (" 02:12:19", tf.Text);
-			tf.ReadOnly = false;
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.Backspace)));
-			Assert.Equal (" 02:02:19", tf.Text);
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.Home)));
-			Assert.Equal (1, tf.CursorPosition);
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.End)));
-			Assert.Equal (8, tf.CursorPosition);
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.A | KeyCode.CtrlMask)));
-			Assert.Equal (1, tf.CursorPosition);
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.E | KeyCode.CtrlMask)));
-			Assert.Equal (8, tf.CursorPosition);
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorLeft)));
-			Assert.Equal (7, tf.CursorPosition);
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.CursorRight)));
-			Assert.Equal (8, tf.CursorPosition);
-			// Non-numerics are ignored
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.A)));
-			tf.ReadOnly = true;
-			tf.CursorPosition = 1;
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1)));
-			Assert.Equal (" 02:02:19", tf.Text);
-			tf.ReadOnly = false;
-			Assert.True (tf.NewKeyDownEvent (new (KeyCode.D1)));
-			Assert.Equal (" 12:02:19", tf.Text);
-		}
+	[Fact, AutoInitShutdown]
+	public void Copy_Paste ()
+	{
+		TimeField tf1 = new TimeField (TimeSpan.Parse ("12:12:19"));
+		TimeField tf2 = new TimeField (TimeSpan.Parse ("12:59:01"));
+		// Select all text
+		Assert.True (tf2.NewKeyDownEvent (new (KeyCode.End | KeyCode.ShiftMask)));
+		Assert.Equal (1, tf2.SelectedStart);
+		Assert.Equal (8, tf2.SelectedLength);
+		Assert.Equal (9, tf2.CursorPosition);
+		// Copy from tf2
+		Assert.True (tf2.NewKeyDownEvent (new (KeyCode.C | KeyCode.CtrlMask)));
+		// Paste into tf1
+		Assert.True (tf1.NewKeyDownEvent (new (KeyCode.V | KeyCode.CtrlMask)));
+		Assert.Equal (" 12:59:01", tf1.Text);
+		Assert.Equal (9, tf1.CursorPosition);
 	}
 }