소스 검색

Fixes #2019: Introduce DatePicker (#3134)

* Create POC of DatePicker

* Move DatePicker to dialog

* Move DatePicker to separate view

* Support user specified date format

* Added code documentation for public API

* Select day on calendar based on currently selected date

* Add new constuctors for DatePicker

* Fix constructors

* Add month navigation buttons

* Added support for user to specify a range of years in the calendar

* Update default format date in unit tests

* Add some more unit tests

* Improve UICatalog DatePicker example

* Change default date format to CultureInfo.CurrentCulture

* Address code review comments

* Fix DatePicker height and width

* Fix crashes on 'Esc' key during open combobox

* Add DatePicker to localizable strings

* Generate calendar labels based on current culture

* Replace Month enum with localized DateTime month names

* Remove setting culture to polish (used for test purposes)

* Prevent choosing not existing day from calendar

* Update DatePicker layout

* Handle year out of range

* Make DatePicker standalone view and simplfy code and component look

* Handle clicking on no exisitng days in calendar

* Add missing rows to calendar

* Update example in UICatalog

* Dispose SubViews of DatePicker

* Add case for DatePicker

---------

Co-authored-by: Tig <[email protected]>
Maciej 1 년 전
부모
커밋
545c010731

+ 9 - 0
Terminal.Gui/Resources/Strings.Designer.cs

@@ -186,6 +186,15 @@ namespace Terminal.Gui.Resources {
             }
         }
         
+        /// <summary>
+        ///   Looks up a localized string similar to Date Picker.
+        /// </summary>
+        internal static string dpTitle {
+            get {
+                return ResourceManager.GetString("dpTitle", resourceCulture);
+            }
+        }
+        
         /// <summary>
         ///   Looks up a localized string similar to Any Files.
         /// </summary>

+ 3 - 0
Terminal.Gui/Resources/Strings.fr-FR.resx

@@ -177,4 +177,7 @@
   <data name="btnOpen" xml:space="preserve">
     <value>Ouvrir</value>
   </data>
+  <data name="dpTitle" xml:space="preserve">
+    <value>Sélecteur de Date</value>
+  </data>
 </root>

+ 3 - 0
Terminal.Gui/Resources/Strings.ja-JP.resx

@@ -273,4 +273,7 @@
   <data name="fdCtxSortDesc" xml:space="preserve">
     <value>{0}で降順ソート (_S)</value>
   </data>
+  <data name="dpTitle" xml:space="preserve">
+    <value>日付ピッカー</value>
+  </data>
 </root>

+ 3 - 0
Terminal.Gui/Resources/Strings.pt-PT.resx

@@ -177,4 +177,7 @@
   <data name="btnOpen" xml:space="preserve">
     <value>Abrir</value>
   </data>
+  <data name="dpTitle" xml:space="preserve">
+    <value>Seletor de Data</value>
+  </data>
 </root>

+ 3 - 0
Terminal.Gui/Resources/Strings.resx

@@ -277,4 +277,7 @@
   <data name="fdCtxSortDesc" xml:space="preserve">
     <value>_Sort {0} DESC</value>
   </data>
+  <data name="dpTitle" xml:space="preserve">
+    <value>Date Picker</value>
+  </data>
 </root>

+ 3 - 0
Terminal.Gui/Resources/Strings.zh-Hans.resx

@@ -273,4 +273,7 @@
   <data name="fdCtxSortDesc" xml:space="preserve">
     <value>{0}逆序排序 (_S)</value>
   </data>
+  <data name="dpTitle" xml:space="preserve">
+    <value>日期选择器</value>
+  </data>
 </root>

+ 241 - 0
Terminal.Gui/Views/DatePicker.cs

@@ -0,0 +1,241 @@
+//
+// DatePicker.cs: DatePicker control
+//
+// Author: Maciej Winnik
+//
+using System;
+using System.Data;
+using System.Globalization;
+using System.Linq;
+
+namespace Terminal.Gui.Views;
+
+/// <summary>
+/// The <see cref="DatePicker"/> <see cref="View"/> Date Picker.
+/// </summary>
+public class DatePicker : View {
+
+	private DateField _dateField;
+	private Label _dateLabel;
+	private TableView _calendar;
+	private DataTable _table;
+	private Button _nextMonthButton;
+	private Button _previousMonthButton;
+
+	private DateTime _date = DateTime.Now;
+
+	/// <summary>
+	/// Format of date. The default is MM/dd/yyyy.
+	/// </summary>
+	public string Format { get; set; } = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
+
+	/// <summary>
+	/// Get or set the date.
+	/// </summary>
+	public DateTime Date {
+		get => _date;
+		set {
+			_date = value;
+			Text = _date.ToString (Format);
+		}
+	}
+
+	/// <summary>
+	/// Initializes a new instance of <see cref="DatePicker"/>.
+	/// </summary>
+	public DatePicker () => SetInitialProperties (_date);
+
+	/// <summary>
+	/// Initializes a new instance of <see cref="DatePicker"/> with the specified date.
+	/// </summary>
+	public DatePicker (DateTime date)
+	{
+		SetInitialProperties (date);
+	}
+
+	/// <summary>
+	/// Initializes a new instance of <see cref="DatePicker"/> with the specified date and format.
+	/// </summary>
+	public DatePicker (DateTime date, string format)
+	{
+		Format = format;
+		SetInitialProperties (date);
+	}
+
+	private void SetInitialProperties (DateTime date)
+	{
+		Title = "Date Picker";
+		BorderStyle = LineStyle.Single;
+		Date = date;
+		_dateLabel = new Label ("Date: ") {
+			X = 0,
+			Y = 0,
+			Height = 1,
+		};
+
+		_dateField = new DateField (DateTime.Now) {
+			X = Pos.Right (_dateLabel),
+			Y = 0,
+			Width = Dim.Fill (1),
+			Height = 1,
+			IsShortFormat = false
+		};
+
+		_calendar = new TableView () {
+			X = 0,
+			Y = Pos.Bottom (_dateLabel),
+			Height = 11,
+			Style = new TableStyle {
+				ShowHeaders = true,
+				ShowHorizontalBottomline = true,
+				ShowVerticalCellLines = true,
+				ExpandLastColumn = true,
+			}
+		};
+
+		_previousMonthButton = new Button (GetBackButtonText ()) {
+			X = Pos.Center () - 4,
+			Y = Pos.Bottom (_calendar) - 1,
+			Height = 1,
+			Width = CalculateCalendarWidth () / 2
+		};
+
+		_previousMonthButton.Clicked += (sender, e) => {
+			Date = _date.AddMonths (-1);
+			CreateCalendar ();
+			_dateField.Date = Date;
+		};
+
+		_nextMonthButton = new Button (GetForwardButtonText ()) {
+			X = Pos.Right (_previousMonthButton) + 2,
+			Y = Pos.Bottom (_calendar) - 1,
+			Height = 1,
+			Width = CalculateCalendarWidth () / 2
+		};
+
+		_nextMonthButton.Clicked += (sender, e) => {
+			Date = _date.AddMonths (1);
+			CreateCalendar ();
+			_dateField.Date = Date;
+		};
+
+		CreateCalendar ();
+		SelectDayOnCalendar (_date.Day);
+
+		_calendar.CellActivated += (sender, e) => {
+			var dayValue = _table.Rows [e.Row] [e.Col];
+			if (dayValue is null) {
+				return;
+			}
+			bool isDay = int.TryParse (dayValue.ToString (), out int day);
+			if (!isDay) {
+				return;
+			}
+			ChangeDayDate (day);
+			SelectDayOnCalendar (day);
+			Text = _date.ToString (Format);
+
+		};
+
+		Width = CalculateCalendarWidth () + 2;
+		Height = _calendar.Height + 3;
+
+		_dateField.DateChanged += DateField_DateChanged;
+
+		Add (_dateLabel, _dateField, _calendar, _previousMonthButton, _nextMonthButton);
+	}
+
+	private void DateField_DateChanged (object sender, DateTimeEventArgs<DateTime> e)
+	{
+		if (e.NewValue.Date.Day != _date.Day) {
+			SelectDayOnCalendar (e.NewValue.Day);
+		}
+		Date = e.NewValue;
+		CreateCalendar ();
+		SelectDayOnCalendar (_date.Day);
+	}
+
+	private void CreateCalendar ()
+	{
+		_calendar.Table = new DataTableSource (_table = CreateDataTable (_date.Month, _date.Year));
+	}
+
+	private void ChangeDayDate (int day)
+	{
+		_date = new DateTime (_date.Year, _date.Month, day);
+		_dateField.Date = _date;
+		CreateCalendar ();
+	}
+
+	private DataTable CreateDataTable (int month, int year)
+	{
+		_table = new DataTable ();
+		GenerateCalendarLabels ();
+		int amountOfDaysInMonth = DateTime.DaysInMonth (year, month);
+		DateTime dateValue = new DateTime (year, month, 1);
+		var dayOfWeek = dateValue.DayOfWeek;
+
+		_table.Rows.Add (new object [6]);
+		for (int i = 1; i <= amountOfDaysInMonth; i++) {
+			_table.Rows [^1] [(int)dayOfWeek] = i;
+			if (dayOfWeek == DayOfWeek.Saturday && i != amountOfDaysInMonth) {
+				_table.Rows.Add (new object [7]);
+			}
+			dayOfWeek = dayOfWeek == DayOfWeek.Saturday ? DayOfWeek.Sunday : dayOfWeek + 1;
+		}
+		int missingRows = 6 - _table.Rows.Count;
+		for (int i = 0; i < missingRows; i++) {
+			_table.Rows.Add (new object [7]);
+		}
+
+		return _table;
+	}
+
+	private void GenerateCalendarLabels ()
+	{
+		_calendar.Style.ColumnStyles.Clear ();
+		for (int i = 0; i < 7; i++) {
+			var abbreviatedDayName = CultureInfo.CurrentCulture.DateTimeFormat.GetAbbreviatedDayName ((DayOfWeek)i);
+			_calendar.Style.ColumnStyles.Add (i, new ColumnStyle () {
+				MaxWidth = abbreviatedDayName.Length,
+				MinWidth = abbreviatedDayName.Length,
+				MinAcceptableWidth = abbreviatedDayName.Length
+			});
+			_table.Columns.Add (abbreviatedDayName);
+		}
+		_calendar.Width = CalculateCalendarWidth ();
+	}
+
+	private int CalculateCalendarWidth ()
+	{
+		return _calendar.Style.ColumnStyles.Sum (c => c.Value.MinWidth) + 7;
+	}
+
+	private void SelectDayOnCalendar (int day)
+	{
+		for (int i = 0; i < _table.Rows.Count; i++) {
+			for (int j = 0; j < _table.Columns.Count; j++) {
+				if (_table.Rows [i] [j].ToString () == day.ToString ()) {
+					_calendar.SetSelection (j, i, false);
+					return;
+				}
+			}
+		}
+	}
+
+	private string GetForwardButtonText () => Glyphs.RightArrow.ToString () + Glyphs.RightArrow.ToString ();
+
+	private string GetBackButtonText () => Glyphs.LeftArrow.ToString () + Glyphs.LeftArrow.ToString ();
+
+	///<inheritdoc/>
+	protected override void Dispose (bool disposing)
+	{
+		_dateLabel.Dispose ();
+		_calendar.Dispose ();
+		_dateField.Dispose ();
+		_table.Dispose ();
+		_previousMonthButton.Dispose ();
+		_nextMonthButton.Dispose ();
+		base.Dispose (disposing);
+	}
+}

+ 22 - 0
UICatalog/Scenarios/DatePickers.cs

@@ -0,0 +1,22 @@
+using Terminal.Gui;
+using Terminal.Gui.Views;
+
+namespace UICatalog.Scenarios;
+[ScenarioMetadata (Name: "Date Picker", Description: "Demonstrates how to use DatePicker class")]
+[ScenarioCategory ("Controls")]
+[ScenarioCategory ("DateTime")]
+public class DatePickers : Scenario {
+
+
+	public override void Setup ()
+	{
+		var datePicker = new DatePicker () {
+			Y = Pos.Center (),
+			X = Pos.Center ()
+		};
+
+
+		Win.Add (datePicker);
+	}
+}
+

+ 5 - 0
UnitTests/Views/AllViewsTests.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 using System.IO;
 using System.Linq;
 using System.Reflection;
+using Terminal.Gui.Views;
 using Xunit;
 using Xunit.Abstractions;
 
@@ -115,6 +116,10 @@ public class AllViewsTests {
 
 			if (vType is TextView) {
 				top.NewKeyDownEvent (new (KeyCode.Tab | KeyCode.CtrlMask));
+			} else if (vType is DatePicker) {
+				for (int i = 0; i < 4; i++) {
+					top.NewKeyDownEvent (new (KeyCode.Tab | KeyCode.CtrlMask));
+				}
 			} else {
 				top.NewKeyDownEvent (new (KeyCode.Tab));
 			}

+ 54 - 0
UnitTests/Views/DatePickerTests.cs

@@ -0,0 +1,54 @@
+using System;
+using System.Globalization;
+using Terminal.Gui.Views;
+using Xunit;
+
+namespace Terminal.Gui.ViewsTests;
+
+public class DatePickerTests {
+
+	[Fact]
+	public void DatePicker_SetFormat_ShouldChangeFormat ()
+	{
+		var datePicker = new DatePicker {
+			Format = "dd/MM/yyyy"
+		};
+		Assert.Equal ("dd/MM/yyyy", datePicker.Format);
+	}
+
+	[Fact]
+	public void DatePicker_Initialize_ShouldSetCurrentDate ()
+	{
+		var datePicker = new DatePicker ();
+		var format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
+		Assert.Equal (DateTime.Now.ToString (format), datePicker.Text);
+	}
+
+	[Fact]
+	public void DatePicker_SetDate_ShouldChangeText ()
+	{
+		var datePicker = new DatePicker ();
+		var newDate = new DateTime (2024, 1, 15);
+		var format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
+
+		datePicker.Date = newDate;
+		Assert.Equal (newDate.ToString (format), datePicker.Text);
+	}
+
+	[Fact]
+	public void DatePicker_ShowDatePickerDialog_ShouldChangeDate ()
+	{
+		var datePicker = new DatePicker ();
+		var format = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
+		var originalDate = datePicker.Date;
+
+		datePicker.MouseEvent (new MouseEvent () { Flags = MouseFlags.Button1Clicked, X = 4, Y = 1 });
+
+		var newDate = new DateTime (2024, 2, 20);
+		datePicker.Date = newDate;
+
+		Assert.Equal (newDate.ToString (format), datePicker.Text);
+
+		datePicker.Date = originalDate;
+	}
+}