Skip to content

Commit

Permalink
Fixes #2019: Introduce DatePicker (#3134)
Browse files Browse the repository at this point in the history
* 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]>
  • Loading branch information
MaciekWin3 and tig authored Jan 13, 2024
1 parent 6ad36b7 commit 545c010
Show file tree
Hide file tree
Showing 10 changed files with 346 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Terminal.Gui/Resources/Strings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Terminal.Gui/Resources/Strings.fr-FR.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions Terminal.Gui/Resources/Strings.ja-JP.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions Terminal.Gui/Resources/Strings.pt-PT.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions Terminal.Gui/Resources/Strings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 3 additions & 0 deletions Terminal.Gui/Resources/Strings.zh-Hans.resx
Original file line number Diff line number Diff line change
Expand Up @@ -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 changes: 241 additions & 0 deletions Terminal.Gui/Views/DatePicker.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 22 additions & 0 deletions UICatalog/Scenarios/DatePickers.cs
Original file line number Diff line number Diff line change
@@ -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 changes: 5 additions & 0 deletions UnitTests/Views/AllViewsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.IO;
using System.Linq;
using System.Reflection;
using Terminal.Gui.Views;
using Xunit;
using Xunit.Abstractions;

Expand Down Expand Up @@ -115,6 +116,10 @@ public void AllViews_Enter_Leave_Events ()

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));
}
Expand Down
54 changes: 54 additions & 0 deletions UnitTests/Views/DatePickerTests.cs
Original file line number Diff line number Diff line change
@@ -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;
}
}

0 comments on commit 545c010

Please sign in to comment.