// // DateField.cs: text entry for date // // Author: Barry Nolte // // Licensed under the MIT license // using System; using System.Globalization; using System.Linq; using System.Text; namespace Terminal.Gui; /// /// Simple Date editing /// /// /// 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; int _fieldLen => _isShort ? _shortFieldLen : _longFieldLen; string _format => _isShort ? _shortFormat : _longFormat; /// /// DateChanged event, raised when the property has changed. /// /// /// This event is raised when the property changes. /// /// /// The passed event arguments containing the old value, new value, and format string. /// public event EventHandler> DateChanged; /// /// Initializes a new instance of using layout. /// /// The x coordinate. /// 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, "") => SetInitialProperties (date, isShort); /// /// Initializes a new instance of using layout. /// public DateField () : this (DateTime.MinValue) { } /// /// Initializes a new instance of using layout. /// /// public DateField (DateTime date) : base ("") { Width = _fieldLen + 2; SetInitialProperties (date); } 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; Date = date; CursorPosition = 1; TextChanged += DateField_Changed; // 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); } /// public override bool OnProcessKeyDown (Key a) { // Ignore non-numeric characters. if (a >= Key.D0 && a <= Key.D9) { if (!ReadOnly) { if (SetText ((Rune)a)) { IncCursorPosition (); } } return true; } return false; } void DateField_Changed (object sender, TextChangedEventArgs e) { try { 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) { Text = e.OldValue; } } string GetInvarianteFormat () => $"MM{_sepChar}dd{_sepChar}yyyy"; string GetLongFormat (string lf) { 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"); } if (frm [i].Contains ("d") && frm [i].GetRuneCount () < 2) { lf = lf.Replace ("d", "dd"); } if (frm [i].Contains ("y") && frm [i].GetRuneCount () < 4) { lf = lf.Replace ("yy", "yyyy"); } } return $" {lf}"; } string GetShortFormat (string lf) => lf.Replace ("yyyy", "yy"); /// /// Gets or sets the date of the . /// /// /// public DateTime Date { get => _date; set { if (ReadOnly) { return; } var oldData = _date; _date = value; Text = value.ToString (_format); var args = new DateTimeEventArgs (oldData, value, _format); if (oldData != value) { OnDateChanged (args); } } } /// /// Get or set the date format for the widget. /// public bool IsShortFormat { get => _isShort; set { _isShort = value; if (_isShort) { Width = 10; } else { Width = 12; } bool ro = ReadOnly; if (ro) { ReadOnly = false; } SetText (Text); ReadOnly = ro; SetNeedsDisplay (); } } /// public override int CursorPosition { get => base.CursorPosition; 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) { newText = newText.Concat (text.GetRange (CursorPosition + 1, text.Count - (CursorPosition + 1))).ToList (); } return SetText (StringExtensions.ToString (newText)); } bool SetText (string text) { if (string.IsNullOrEmpty (text)) { return false; } 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) { month = 1; vals [idx] = "1"; } else if (Int32.Parse (vals [idx]) > 12) { month = 12; vals [idx] = "12"; } else { month = Int32.Parse (vals [idx]); } idx = GetFormatIndex (frm, "d"); if (Int32.Parse (vals [idx]) < 1) { day = 1; vals [idx] = "1"; } else if (Int32.Parse (vals [idx]) > 31) { day = DateTime.DaysInMonth (year, month); vals [idx] = day.ToString (); } else { day = Int32.Parse (vals [idx]); } string d = GetDate (month, day, year, frm); 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 = " "; for (int i = 0; i < fm.Length; i++) { if (fm [i].Contains ("M")) { date += $"{month,2:00}"; } else if (fm [i].Contains ("d")) { date += $"{day,2:00}"; } else { if (_isShort && year.ToString ().Length == 4) { date += $"{year.ToString ().Substring (2, 2)}"; } else if (_isShort) { date += $"{year,2:00}"; } else { date += $"{year,4:0000}"; } } if (i < 2) { date += $"{_sepChar}"; } } return date; } string GetInvarianteDate (string text, bool isShort) { 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++) { if (frm [i].Contains ("M")) { date [0] = vals [i].Trim (); } else if (frm [i].Contains ("d")) { date [1] = vals [i].Trim (); } else { 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 { 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]}"; } int GetFormatIndex (string [] fm, string t) { int idx = -1; for (int i = 0; i < fm.Length; i++) { if (fm [i].Contains (t)) { idx = i; break; } } return idx; } void IncCursorPosition () { if (CursorPosition >= _fieldLen) { CursorPosition = _fieldLen; return; } if (Text [++CursorPosition] == _sepChar.ToCharArray () [0]) { CursorPosition++; } } void DecCursorPosition () { if (CursorPosition <= 1) { CursorPosition = 1; return; } if (Text [--CursorPosition] == _sepChar.ToCharArray () [0]) { CursorPosition--; } } void AdjCursorPosition () { if (Text [CursorPosition] == _sepChar.ToCharArray () [0]) { CursorPosition++; } } 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; } /// public override void DeleteCharLeft (bool useOldCursorPos = true) { if (ReadOnly) { return; } ClearAllSelection (); SetText ((Rune)'0'); DecCursorPosition (); return; } /// public override void DeleteCharRight () { if (ReadOnly) { return; } ClearAllSelection (); SetText ((Rune)'0'); return; } /// public override bool MouseEvent (MouseEvent ev) { if (!ev.Flags.HasFlag (MouseFlags.Button1Clicked)) { return false; } if (!HasFocus) { SetFocus (); } int point = ev.X; if (point > _fieldLen) { point = _fieldLen; } if (point < 1) { point = 1; } CursorPosition = point; AdjCursorPosition (); return true; } /// /// Event firing method for the event. /// /// Event arguments public virtual void OnDateChanged (DateTimeEventArgs args) => DateChanged?.Invoke (this, args); }