diff --git a/.gitignore b/.gitignore
index f10938059..c4865c0ac 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,4 +4,5 @@ obj
*.userprefs
*~
packages
-.vs
\ No newline at end of file
+.vs
+*.csproj.user
diff --git a/Example/demo.cs b/Example/demo.cs
index 34a78ec2e..c6fc780f9 100644
--- a/Example/demo.cs
+++ b/Example/demo.cs
@@ -142,8 +142,10 @@ static class Demo {
new Button (10, 19, "Cancel"),
new TimeField (3, 20, DateTime.Now),
new TimeField (23, 20, DateTime.Now, true),
+ new DateField(3, 22, DateTime.Now),
+ new DateField(23, 22, DateTime.Now, true),
progress,
- new Label (3, 22, "Press F9 (on Unix, ESC+9 is an alias) to activate the menubar")
+ new Label (3, 24, "Press F9 (on Unix, ESC+9 is an alias) to activate the menubar")
);
diff --git a/Terminal.Gui/Views/DateField.cs b/Terminal.Gui/Views/DateField.cs
new file mode 100644
index 000000000..1a13c0fe1
--- /dev/null
+++ b/Terminal.Gui/Views/DateField.cs
@@ -0,0 +1,243 @@
+//
+// DateField.cs: text entry for date
+//
+// Author: Barry Nolte
+//
+// Licensed under the MIT license
+//
+using System;
+using System.Globalization;
+using System.Linq;
+using NStack;
+
+namespace Terminal.Gui {
+
+ ///
+ /// Date edit widget
+ ///
+ ///
+ /// This widget provides date editing functionality, and mouse support.
+ ///
+ public class DateField : TextField {
+ bool isShort;
+
+ int longFieldLen = 10;
+ int shortFieldLen = 8;
+ int FieldLen { get { return isShort ? shortFieldLen : longFieldLen; } }
+ char sepChar; // = '/';
+ string longFormat; // = " MM/dd/yyyy";
+ string shortFormat; // = " MM/dd/yy";
+ string Format { get { return isShort ? shortFormat : longFormat; } }
+
+
+ ///
+ /// Public constructor that creates a date edit field at an absolute position and fixed size.
+ ///
+ /// 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, "")
+ {
+ CultureInfo cultureInfo = CultureInfo.CurrentCulture;
+ sepChar = cultureInfo.DateTimeFormat.DateSeparator.ToCharArray()[0];
+ longFormat = $" {cultureInfo.DateTimeFormat.ShortDatePattern}";
+ shortFormat = GetShortFormat(longFormat);
+ this.isShort = isShort;
+ CursorPosition = 1;
+ Date = date;
+ Changed += DateField_Changed;
+ }
+
+ private void DateField_Changed(object sender, ustring e)
+ {
+ if (!DateTime.TryParseExact(Text.ToString(), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result))
+ Text = e;
+ }
+
+ string GetShortFormat(string lf)
+ {
+ return lf.Replace("yyyy", "yy");
+ }
+
+ ///
+ /// Gets or sets the date in the widget.
+ ///
+ ///
+ ///
+ public DateTime Date {
+ get {
+ if (!DateTime.TryParseExact(Text.ToString(), Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result)) return new DateTime();
+ return result;
+ }
+ set {
+ this.Text = value.ToString(Format);
+ }
+ }
+
+ bool SetText(Rune key)
+ {
+ var text = TextModel.ToRunes(Text);
+ 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(ustring.Make(newText));
+ }
+
+ bool SetText(ustring text)
+ {
+ // FIXED: This validation could be made better by calculating the actual min/max values
+ // for month/day/year. This has a 'good' chance of keeping things valid
+ ustring[] vals = text.Split(ustring.Make(sepChar));
+ ustring[] frm = ustring.Make(Format).Split(ustring.Make(sepChar));
+ bool isValidDate = true;
+ int idx = GetFormatIndex(frm, "y");
+ int year = Int32.Parse(vals[idx].ToString());
+ int month;
+ int day;
+ idx = GetFormatIndex(frm, "M");
+ if (Int32.Parse(vals[idx].ToString()) < 1) {
+ isValidDate = false;
+ month = 1;
+ vals[idx] = "1";
+ } else if (Int32.Parse(vals[idx].ToString()) > 12) {
+ isValidDate = false;
+ month = 12;
+ vals[idx] = "12";
+ } else
+ month = Int32.Parse(vals[idx].ToString());
+ idx = GetFormatIndex(frm, "d");
+ if (Int32.Parse(vals[idx].ToString()) < 1) {
+ isValidDate = false;
+ day = 1;
+ vals[idx] = "1";
+ } else if (Int32.Parse(vals[idx].ToString()) > 31) {
+ isValidDate = false;
+ day = DateTime.DaysInMonth(year, month);
+ vals[idx] = day.ToString();
+ } else
+ day = Int32.Parse(vals[idx].ToString());
+ string date = GetData(month, day, year, frm);
+ Text = date;
+
+ if (!DateTime.TryParseExact(date, Format, CultureInfo.CurrentCulture, DateTimeStyles.None, out DateTime result) ||
+ !isValidDate)
+ return false;
+ return true;
+ }
+
+ string GetData(int month, int day, int year, ustring[] fm)
+ {
+ string data = " ";
+ for (int i = 0; i < fm.Length; i++) {
+ if (fm[i].Contains("M"))
+ data += $"{month,2:00}";
+ else if (fm[i].Contains("d"))
+ data += $"{day,2:00}";
+ else
+ data += isShort ? $"{year,2:00}" : $"{year,4:0000}";
+ if (i < 2)
+ data += $"{sepChar}";
+ }
+ return data;
+ }
+
+ int GetFormatIndex(ustring[] 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)
+ return;
+ if (Text[++CursorPosition] == sepChar)
+ CursorPosition++;
+ }
+
+ void DecCursorPosition()
+ {
+ if (CursorPosition == 1)
+ return;
+ if (Text[--CursorPosition] == sepChar)
+ CursorPosition--;
+ }
+
+ void AdjCursorPosition()
+ {
+ if (Text[CursorPosition] == sepChar)
+ CursorPosition++;
+ }
+
+ public override bool ProcessKey(KeyEvent kb)
+ {
+ switch (kb.Key) {
+ case Key.DeleteChar:
+ case Key.ControlD:
+ SetText('0');
+ break;
+
+ case Key.Delete:
+ case Key.Backspace:
+ SetText('0');
+ DecCursorPosition();
+ break;
+
+ // Home, C-A
+ case Key.Home:
+ case Key.ControlA:
+ CursorPosition = 1;
+ break;
+
+ case Key.CursorLeft:
+ case Key.ControlB:
+ DecCursorPosition();
+ break;
+
+ case Key.End:
+ case Key.ControlE: // End
+ CursorPosition = FieldLen;
+ break;
+
+ case Key.CursorRight:
+ case Key.ControlF:
+ IncCursorPosition();
+ break;
+
+ default:
+ // Ignore non-numeric characters.
+ if (kb.Key < (Key)((int)'0') || kb.Key > (Key)((int)'9'))
+ return false;
+ if (SetText(TextModel.ToRunes(ustring.Make((uint)kb.Key)).First()))
+ IncCursorPosition();
+ return true;
+ }
+ return true;
+ }
+
+ public override bool MouseEvent(MouseEvent ev)
+ {
+ if (!ev.Flags.HasFlag(MouseFlags.Button1Clicked))
+ return false;
+ if (!HasFocus)
+ SuperView.SetFocus(this);
+
+ var point = ev.X;
+ if (point > FieldLen)
+ point = FieldLen;
+ if (point < 1)
+ point = 1;
+ CursorPosition = point;
+ AdjCursorPosition();
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/Terminal.Gui/Views/TextField.cs b/Terminal.Gui/Views/TextField.cs
index 215c66064..16187a339 100644
--- a/Terminal.Gui/Views/TextField.cs
+++ b/Terminal.Gui/Views/TextField.cs
@@ -35,7 +35,7 @@ namespace Terminal.Gui {
/// Client code can hook up to this event, it is
/// raised when the text in the entry changes.
///
- public event EventHandler Changed;
+ public event EventHandler Changed;
///
/// Public constructor that creates a text field, with layout controlled with X, Y, Width and Height.
@@ -98,7 +98,10 @@ namespace Terminal.Gui {
}
set {
+ ustring oldText = ustring.Make(text);
text = TextModel.ToRunes (value);
+ Changed?.Invoke(this, oldText);
+
if (point > text.Count)
point = Math.Max (text.Count-1, 0);
@@ -193,8 +196,6 @@ namespace Terminal.Gui {
void SetText (List newText)
{
text = newText;
- if (Changed != null)
- Changed (this, EventArgs.Empty);
}
void SetText (IEnumerable newText)