diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs index 4511f1bfd..5fc87abf3 100644 --- a/Terminal.Gui/Views/DateField.cs +++ b/Terminal.Gui/Views/DateField.cs @@ -10,7 +10,7 @@ using System.Globalization; using System.Linq; using System.Text; -namespace Terminal.Gui; +namespace Terminal.Gui; /// /// Simple Date editing @@ -19,17 +19,17 @@ namespace Terminal.Gui; /// The provides date editing functionality with mouse support. /// public class DateField : TextField { - DateTime date; - bool isShort; - int longFieldLen = 10; - int shortFieldLen = 8; - string sepChar; - string longFormat; - string shortFormat; + DateTime _date; + bool _isShort; + int _longFieldLen = 10; + int _shortFieldLen = 8; + string _sepChar; + string _longFormat; + string _shortFormat; - int fieldLen => isShort ? shortFieldLen : longFieldLen; + int _fieldLen => _isShort ? _shortFieldLen : _longFieldLen; - string format => isShort ? shortFormat : longFormat; + string _format => _isShort ? _shortFormat : _longFormat; /// /// DateChanged event, raised when the property has changed. @@ -49,7 +49,7 @@ public class DateField : TextField { /// The y coordinate. /// Initial date contents. /// If true, shows only two digits for the year. - public DateField (int x, int y, DateTime date, bool isShort = false) : base (x, y, isShort ? 10 : 12, "") => Initialize (date, isShort); + public DateField (int x, int y, DateTime date, bool isShort = false) : base (x, y, isShort ? 10 : 12, "") => SetInitialProperties (date, isShort); /// /// Initializes a new instance of using layout. @@ -62,17 +62,17 @@ public class DateField : TextField { /// public DateField (DateTime date) : base ("") { - Width = fieldLen + 2; - Initialize (date); + Width = _fieldLen + 2; + SetInitialProperties (date); } - void Initialize (DateTime date, bool isShort = false) + void SetInitialProperties (DateTime date, bool isShort = false) { var cultureInfo = CultureInfo.CurrentCulture; - sepChar = cultureInfo.DateTimeFormat.DateSeparator; - longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern); - shortFormat = GetShortFormat (longFormat); - this.isShort = isShort; + _sepChar = cultureInfo.DateTimeFormat.DateSeparator; + _longFormat = GetLongFormat (cultureInfo.DateTimeFormat.ShortDatePattern); + _shortFormat = GetShortFormat (_longFormat); + this._isShort = isShort; Date = date; CursorPosition = 1; TextChanged += DateField_Changed; @@ -95,8 +95,8 @@ public class DateField : TextField { KeyBindings.Add (KeyCode.Delete, Command.DeleteCharRight); KeyBindings.Add (Key.D.WithCtrl, Command.DeleteCharRight); - KeyBindings.Add (Key.Delete, Command.DeleteCharLeft); 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); @@ -130,7 +130,14 @@ public class DateField : TextField { void DateField_Changed (object sender, TextChangedEventArgs e) { try { - if (!DateTime.TryParseExact (GetDate (Text), GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) { + var date = GetInvarianteDate (Text, _isShort); + if ($" {date}" != Text) { + Text = $" {date}"; + } + if (_isShort) { + date = GetInvarianteDate (Text, false); + } + if (!DateTime.TryParseExact (date, GetInvarianteFormat (), CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) { Text = e.OldValue; } } catch (Exception) { @@ -138,11 +145,11 @@ public class DateField : TextField { } } - string GetInvarianteFormat () => $"MM{sepChar}dd{sepChar}yyyy"; + string GetInvarianteFormat () => $"MM{_sepChar}dd{_sepChar}yyyy"; string GetLongFormat (string lf) { - string [] frm = lf.Split (sepChar); + string [] frm = lf.Split (_sepChar); for (int i = 0; i < frm.Length; i++) { if (frm [i].Contains ("M") && frm [i].GetRuneCount () < 2) { lf = lf.Replace ("M", "MM"); @@ -165,16 +172,16 @@ public class DateField : TextField { /// /// public DateTime Date { - get => date; + get => _date; set { if (ReadOnly) { return; } - var oldData = date; - date = value; - Text = value.ToString (format); - var args = new DateTimeEventArgs (oldData, value, format); + var oldData = _date; + _date = value; + Text = value.ToString (_format); + var args = new DateTimeEventArgs (oldData, value, _format); if (oldData != value) { OnDateChanged (args); } @@ -185,10 +192,10 @@ public class DateField : TextField { /// Get or set the date format for the widget. /// public bool IsShortFormat { - get => isShort; + get => _isShort; set { - isShort = value; - if (isShort) { + _isShort = value; + if (_isShort) { Width = 10; } else { Width = 12; @@ -206,15 +213,23 @@ public class DateField : TextField { /// public override int CursorPosition { get => base.CursorPosition; - set => base.CursorPosition = Math.Max (Math.Min (value, fieldLen), 1); + set => base.CursorPosition = Math.Max (Math.Min (value, _fieldLen), 1); } bool SetText (Rune key) { + if (CursorPosition > _fieldLen) { + CursorPosition = _fieldLen; + return false; + } else if (CursorPosition < 1) { + CursorPosition = 1; + return false; + } + var text = Text.EnumerateRunes ().ToList (); var newText = text.GetRange (0, CursorPosition); newText.Add (key); - if (CursorPosition < fieldLen) { + if (CursorPosition < _fieldLen) { newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); } return SetText (StringExtensions.ToString (newText)); @@ -226,20 +241,24 @@ public class DateField : TextField { return false; } - string [] vals = text.Split (sepChar); - string [] frm = format.Split (sepChar); - bool isValidDate = true; - int idx = GetFormatIndex (frm, "y"); - int year = Int32.Parse (vals [idx]); + text = NormalizeFormat (text); + string [] vals = text.Split (_sepChar); + string [] frm = _format.Split (_sepChar); + int year; int month; int day; + int idx = GetFormatIndex (frm, "y"); + if (Int32.Parse (vals [idx]) < 1) { + year = 1; + vals [idx] = "1"; + } else { + year = Int32.Parse (vals [idx]); + } idx = GetFormatIndex (frm, "M"); if (Int32.Parse (vals [idx]) < 1) { - isValidDate = false; month = 1; vals [idx] = "1"; } else if (Int32.Parse (vals [idx]) > 12) { - isValidDate = false; month = 12; vals [idx] = "12"; } else { @@ -247,11 +266,9 @@ public class DateField : TextField { } idx = GetFormatIndex (frm, "d"); if (Int32.Parse (vals [idx]) < 1) { - isValidDate = false; day = 1; vals [idx] = "1"; } else if (Int32.Parse (vals [idx]) > 31) { - isValidDate = false; day = DateTime.DaysInMonth (year, month); vals [idx] = day.ToString (); } else { @@ -259,14 +276,36 @@ public class DateField : TextField { } string d = GetDate (month, day, year, frm); - if (!DateTime.TryParseExact (d, format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var result) || - !isValidDate) { + if (!DateTime.TryParseExact (d, _format, CultureInfo.CurrentCulture, DateTimeStyles.None, out var result)) { return false; } Date = result; return true; } + string NormalizeFormat (string text, string fmt = null, string sepChar = null) + { + if (string.IsNullOrEmpty (fmt)) { + fmt = _format; + } + if (string.IsNullOrEmpty (sepChar)) { + sepChar = _sepChar; + } + if (fmt.Length != text.Length) { + return text; + } + + 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; + } + } + + return new string (fmtText); + } + string GetDate (int month, int day, int year, string [] fm) { string date = " "; @@ -276,26 +315,25 @@ public class DateField : TextField { } else if (fm [i].Contains ("d")) { date += $"{day,2:00}"; } else { - if (!isShort && year.ToString ().Length == 2) { - string y = DateTime.Now.Year.ToString (); - date += y.Substring (0, 2) + year.ToString (); - } else if (isShort && year.ToString ().Length == 4) { + if (_isShort && year.ToString ().Length == 4) { date += $"{year.ToString ().Substring (2, 2)}"; - } else { + } else if (_isShort) { date += $"{year,2:00}"; + } else { + date += $"{year,4:0000}"; } } if (i < 2) { - date += $"{sepChar}"; + date += $"{_sepChar}"; } } return date; } - string GetDate (string text) + string GetInvarianteDate (string text, bool isShort) { - string [] vals = text.Split (sepChar); - string [] frm = format.Split (sepChar); + string [] vals = text.Split (_sepChar); + string [] frm = (isShort ? $"MM{_sepChar}dd{_sepChar}yy" : GetInvarianteFormat ()).Split (_sepChar); string [] date = { null, null, null }; for (int i = 0; i < frm.Length; i++) { @@ -304,16 +342,25 @@ public class DateField : TextField { } else if (frm [i].Contains ("d")) { date [1] = vals [i].Trim (); } else { - string year = vals [i].Trim (); - if (year.GetRuneCount () == 2) { - string y = DateTime.Now.Year.ToString (); - date [2] = y.Substring (0, 2) + year.ToString (); + string yearString; + if (isShort && vals [i].Length > 2) { + yearString = vals [i].Substring (0, 2); + } else if (!isShort && vals [i].Length > 4) { + yearString = vals [i].Substring (0, 4); } else { - date [2] = vals [i].Trim (); + yearString = vals [i].Trim (); + } + var year = int.Parse (yearString); + if (isShort && year.ToString ().Length == 4) { + date [2] = year.ToString ().Substring (2, 2); + } else if (isShort) { + date [2] = year.ToString (); + } else { + date [2] = $"{year,4:0000}"; } } } - return date [0] + sepChar + date [1] + sepChar + date [2]; + return $"{date [0]}{_sepChar}{date [1]}{_sepChar}{date [2]}"; } int GetFormatIndex (string [] fm, string t) @@ -330,45 +377,50 @@ public class DateField : TextField { void IncCursorPosition () { - if (CursorPosition == fieldLen) { + if (CursorPosition >= _fieldLen) { + CursorPosition = _fieldLen; return; } - if (Text [++CursorPosition] == sepChar.ToCharArray () [0]) { + if (Text [++CursorPosition] == _sepChar.ToCharArray () [0]) { CursorPosition++; } } void DecCursorPosition () { - if (CursorPosition == 1) { + if (CursorPosition <= 1) { + CursorPosition = 1; return; } - if (Text [--CursorPosition] == sepChar.ToCharArray () [0]) { + if (Text [--CursorPosition] == _sepChar.ToCharArray () [0]) { CursorPosition--; } } void AdjCursorPosition () { - if (Text [CursorPosition] == sepChar.ToCharArray () [0]) { + if (Text [CursorPosition] == _sepChar.ToCharArray () [0]) { CursorPosition++; } } bool MoveRight () { + ClearAllSelection (); IncCursorPosition (); return true; } new bool MoveEnd () { - CursorPosition = fieldLen; + ClearAllSelection (); + CursorPosition = _fieldLen; return true; } bool MoveLeft () { + ClearAllSelection (); DecCursorPosition (); return true; } @@ -376,6 +428,7 @@ public class DateField : TextField { bool MoveHome () { // Home, C-A + ClearAllSelection (); CursorPosition = 1; return true; } @@ -387,6 +440,7 @@ public class DateField : TextField { return; } + ClearAllSelection (); SetText ((Rune)'0'); DecCursorPosition (); return; @@ -399,6 +453,7 @@ public class DateField : TextField { return; } + ClearAllSelection (); SetText ((Rune)'0'); return; } @@ -414,8 +469,8 @@ public class DateField : TextField { } int point = ev.X; - if (point > fieldLen) { - point = fieldLen; + if (point > _fieldLen) { + point = _fieldLen; } if (point < 1) { point = 1; diff --git a/UnitTests/Views/DateFieldTests.cs b/UnitTests/Views/DateFieldTests.cs index 237e318c5..a44daa173 100644 --- a/UnitTests/Views/DateFieldTests.cs +++ b/UnitTests/Views/DateFieldTests.cs @@ -57,6 +57,31 @@ namespace Terminal.Gui.ViewsTests { Assert.Equal (8, df.CursorPosition); } + [Fact] + public void CursorPosition_Min_Is_Always_One_Max_Is_Always_Max_Format_After_Selection () + { + var df = new DateField (); + // Start selection + Assert.True (df.NewKeyDownEvent (new (KeyCode.CursorLeft | KeyCode.ShiftMask))); + Assert.Equal (1, df.SelectedStart); + Assert.Equal (1, df.SelectedLength); + Assert.Equal (0, df.CursorPosition); + // Without selection + Assert.True (df.NewKeyDownEvent (new (KeyCode.CursorLeft))); + Assert.Equal (-1, df.SelectedStart); + Assert.Equal (0, df.SelectedLength); + Assert.Equal (1, df.CursorPosition); + df.CursorPosition = 10; + Assert.True (df.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask))); + Assert.Equal (10, df.SelectedStart); + Assert.Equal (1, df.SelectedLength); + Assert.Equal (11, df.CursorPosition); + Assert.True (df.NewKeyDownEvent (new (KeyCode.CursorRight))); + Assert.Equal (-1, df.SelectedStart); + Assert.Equal (0, df.SelectedLength); + Assert.Equal (10, df.CursorPosition); + } + [Fact] public void KeyBindings_Command () { @@ -97,6 +122,63 @@ namespace Terminal.Gui.ViewsTests { df.ReadOnly = false; Assert.True (df.NewKeyDownEvent (new (KeyCode.D1))); Assert.Equal (" 12/02/1971", df.Text); + Assert.Equal (2, df.CursorPosition); + Assert.True (df.NewKeyDownEvent (new (KeyCode.D | KeyCode.AltMask))); + Assert.Equal (" 10/02/1971", df.Text); + CultureInfo.CurrentCulture = cultureBackup; + } + + [Fact] + public void Typing_With_Selection_Normalize_Format () + { + CultureInfo cultureBackup = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + DateField df = new DateField (DateTime.Parse ("12/12/1971")); + // Start selection at before the first separator / + df.CursorPosition = 2; + // Now select the separator / + Assert.True (df.NewKeyDownEvent (new (KeyCode.CursorRight | KeyCode.ShiftMask))); + Assert.Equal (2, df.SelectedStart); + Assert.Equal (1, df.SelectedLength); + Assert.Equal (3, df.CursorPosition); + // Type 3 over the separator + Assert.True (df.NewKeyDownEvent (new (KeyCode.D3))); + // The format was normalized and replaced again with / + Assert.Equal (" 12/12/1971", df.Text); + Assert.Equal (4, df.CursorPosition); + CultureInfo.CurrentCulture = cultureBackup; + } + + [Fact, AutoInitShutdown] + public void Copy_Paste () + { + CultureInfo cultureBackup = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + DateField df1 = new DateField (DateTime.Parse ("12/12/1971")); + DateField df2 = new DateField (DateTime.Parse ("12/31/2023")); + // Select all text + Assert.True (df2.NewKeyDownEvent (new (KeyCode.End | KeyCode.ShiftMask))); + Assert.Equal (1, df2.SelectedStart); + Assert.Equal (10, df2.SelectedLength); + Assert.Equal (11, df2.CursorPosition); + // Copy from df2 + Assert.True (df2.NewKeyDownEvent (new (KeyCode.C | KeyCode.CtrlMask))); + // Paste into df1 + Assert.True (df1.NewKeyDownEvent (new (KeyCode.V | KeyCode.CtrlMask))); + Assert.Equal (" 12/31/2023", df1.Text); + Assert.Equal (11, df1.CursorPosition); + CultureInfo.CurrentCulture = cultureBackup; + } + + [Fact] + public void Date_Start_From_01_01_0001_And_End_At_12_31_9999 () + { + CultureInfo cultureBackup = CultureInfo.CurrentCulture; + CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; + DateField df = new DateField (DateTime.Parse ("01/01/0001")); + Assert.Equal (" 01/01/0001", df.Text); + df.Date = DateTime.Parse ("12/31/9999"); + Assert.Equal (" 12/31/9999", df.Text); CultureInfo.CurrentCulture = cultureBackup; } }