Add calendar control

Closes #101
This commit is contained in:
Patrik Svensson
2020-10-16 16:43:33 +02:00
committed by Patrik Svensson
parent 0a0380ae0a
commit 3f2ca49071
14 changed files with 833 additions and 1 deletions

View File

@ -0,0 +1,83 @@
using System;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="Calendar"/>.
/// </summary>
public static class CalendarExtensions
{
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar to add the calendar event to.</param>
/// <param name="date">The calendar event date.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, DateTime date)
{
return AddCalendarEvent(calendar, string.Empty, date.Year, date.Month, date.Day);
}
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar to add the calendar event to.</param>
/// <param name="description">The calendar event description.</param>
/// <param name="date">The calendar event date.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, string description, DateTime date)
{
return AddCalendarEvent(calendar, description, date.Year, date.Month, date.Day);
}
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar to add the calendar event to.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, int year, int month, int day)
{
return AddCalendarEvent(calendar, string.Empty, year, month, day);
}
/// <summary>
/// Adds a calendar event.
/// </summary>
/// <param name="calendar">The calendar.</param>
/// <param name="description">The calendar event description.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar AddCalendarEvent(this Calendar calendar, string description, int year, int month, int day)
{
if (calendar is null)
{
throw new ArgumentNullException(nameof(calendar));
}
calendar.CalendarEvents.Add(new CalendarEvent(description, year, month, day));
return calendar;
}
/// <summary>
/// Sets the calendar's highlight <see cref="Style"/>.
/// </summary>
/// <param name="calendar">The calendar.</param>
/// <param name="style">The highlight style.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static Calendar SetHighlightStyle(this Calendar calendar, Style? style)
{
if (calendar is null)
{
throw new ArgumentNullException(nameof(calendar));
}
calendar.HightlightStyle = style ?? Style.Plain;
return calendar;
}
}
}

View File

@ -0,0 +1,73 @@
using System;
using System.Globalization;
namespace Spectre.Console
{
/// <summary>
/// Contains extension methods for <see cref="IHasCulture"/>.
/// </summary>
public static class HasCultureExtensions
{
/// <summary>
/// Sets the culture.
/// </summary>
/// <typeparam name="T">An object type with a culture.</typeparam>
/// <param name="obj">The object to set the culture for.</param>
/// <param name="culture">The culture to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetCulture<T>(this T obj, CultureInfo culture)
where T : class, IHasCulture
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
if (culture is null)
{
throw new ArgumentNullException(nameof(culture));
}
obj.Culture = culture;
return obj;
}
/// <summary>
/// Sets the culture.
/// </summary>
/// <typeparam name="T">An object type with a culture.</typeparam>
/// <param name="obj">The object to set the culture for.</param>
/// <param name="name">The culture to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetCulture<T>(this T obj, string name)
where T : class, IHasCulture
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Culture = CultureInfo.GetCultureInfo(name);
return obj;
}
/// <summary>
/// Sets the culture.
/// </summary>
/// <typeparam name="T">An object type with a culture.</typeparam>
/// <param name="obj">The object to set the culture for.</param>
/// <param name="name">The culture to set.</param>
/// <returns>The same instance so that multiple calls can be chained.</returns>
public static T SetCulture<T>(this T obj, int name)
where T : class, IHasCulture
{
if (obj is null)
{
throw new ArgumentNullException(nameof(obj));
}
obj.Culture = CultureInfo.GetCultureInfo(name);
return obj;
}
}
}

View File

@ -0,0 +1,15 @@
using System.Globalization;
namespace Spectre.Console
{
/// <summary>
/// Represents something that has a culture.
/// </summary>
public interface IHasCulture
{
/// <summary>
/// Gets or sets the culture.
/// </summary>
CultureInfo Culture { get; set; }
}
}

View File

@ -0,0 +1,88 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace Spectre.Console.Internal.Collections
{
internal sealed class ListWithCallback<T> : IList<T>
{
private readonly List<T> _list;
private readonly Action _callback;
public T this[int index]
{
get => _list[index];
set => _list[index] = value;
}
public int Count => _list.Count;
public bool IsReadOnly => false;
public ListWithCallback(Action callback)
{
_list = new List<T>();
_callback = callback ?? throw new ArgumentNullException(nameof(callback));
}
public void Add(T item)
{
_list.Add(item);
_callback();
}
public void Clear()
{
_list.Clear();
_callback();
}
public bool Contains(T item)
{
return _list.Contains(item);
}
public void CopyTo(T[] array, int arrayIndex)
{
_list.CopyTo(array, arrayIndex);
_callback();
}
public IEnumerator<T> GetEnumerator()
{
return _list.GetEnumerator();
}
public int IndexOf(T item)
{
return _list.IndexOf(item);
}
public void Insert(int index, T item)
{
_list.Insert(index, item);
_callback();
}
public bool Remove(T item)
{
var result = _list.Remove(item);
if (result)
{
_callback();
}
return result;
}
public void RemoveAt(int index)
{
_list.RemoveAt(index);
_callback();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
}
}

View File

@ -0,0 +1,32 @@
using System;
using System.Globalization;
namespace Spectre.Console.Internal
{
internal static class DayOfWeekExtensions
{
public static string GetAbbreviatedDayName(this DayOfWeek day, CultureInfo culture)
{
culture ??= CultureInfo.InvariantCulture;
var name = culture.DateTimeFormat.GetAbbreviatedDayName(day);
if (name.Length > 0 && char.IsLower(name[0]))
{
name = string.Format(culture, "{0}{1}", char.ToUpper(name[0], culture), name.Substring(1));
}
return name;
}
public static DayOfWeek GetNextWeekDay(this DayOfWeek day)
{
var next = (int)day + 1;
if (next > (int)DayOfWeek.Saturday)
{
return DayOfWeek.Sunday;
}
return (DayOfWeek)next;
}
}
}

View File

@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Spectre.Console.Internal;
namespace Spectre.Console

View File

@ -0,0 +1,274 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using Spectre.Console.Internal;
using Spectre.Console.Internal.Collections;
using Spectre.Console.Rendering;
namespace Spectre.Console
{
/// <summary>
/// A renderable calendar.
/// </summary>
public sealed class Calendar : Renderable, IHasCulture, IHasTableBorder
{
private const int NumberOfWeekDays = 7;
private const int ExpectedRowCount = 6;
private readonly ListWithCallback<CalendarEvent> _calendarEvents;
private int _year;
private int _month;
private int _day;
private IRenderable? _table;
private TableBorder _border;
private bool _useSafeBorder;
private Style? _borderStyle;
private bool _dirty;
private CultureInfo _culture;
private Style _highlightStyle;
/// <summary>
/// Gets or sets the calendar year.
/// </summary>
public int Year
{
get => _year;
set => MarkAsDirty(() => _year = value);
}
/// <summary>
/// Gets or sets the calendar month.
/// </summary>
public int Month
{
get => _month;
set => MarkAsDirty(() => _month = value);
}
/// <summary>
/// Gets or sets the calendar day.
/// </summary>
public int Day
{
get => _day;
set => MarkAsDirty(() => _day = value);
}
/// <inheritdoc/>
public TableBorder Border
{
get => _border;
set => MarkAsDirty(() => _border = value);
}
/// <inheritdoc/>
public bool UseSafeBorder
{
get => _useSafeBorder;
set => MarkAsDirty(() => _useSafeBorder = value);
}
/// <inheritdoc/>
public Style? BorderStyle
{
get => _borderStyle;
set => MarkAsDirty(() => _borderStyle = value);
}
/// <summary>
/// Gets or sets the calendar's <see cref="CultureInfo"/>.
/// </summary>
public CultureInfo Culture
{
get => _culture;
set => MarkAsDirty(() => _culture = value);
}
/// <summary>
/// Gets or sets the calendar's highlight <see cref="Style"/>.
/// </summary>
public Style HightlightStyle
{
get => _highlightStyle;
set => MarkAsDirty(() => _highlightStyle = value);
}
/// <summary>
/// Gets a list containing all calendar events.
/// </summary>
public IList<CalendarEvent> CalendarEvents => _calendarEvents;
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="date">The calendar date.</param>
public Calendar(DateTime date)
: this(date.Year, date.Month, date.Day)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
public Calendar(int year, int month)
: this(year, month, 1)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="Calendar"/> class.
/// </summary>
/// <param name="year">The calendar year.</param>
/// <param name="month">The calendar month.</param>
/// <param name="day">The calendar day.</param>
public Calendar(int year, int month, int day)
{
_year = year;
_month = month;
_day = day;
_table = null;
_border = TableBorder.Square;
_useSafeBorder = true;
_borderStyle = null;
_dirty = true;
_culture = CultureInfo.InvariantCulture;
_highlightStyle = new Style(foreground: Color.Blue);
_calendarEvents = new ListWithCallback<CalendarEvent>(() => _dirty = true);
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var table = GetTable();
return table.Measure(context, maxWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{
return GetTable().Render(context, maxWidth);
}
private IRenderable GetTable()
{
// Table needs to be built?
if (_dirty || _table == null)
{
_table = BuildTable();
_dirty = false;
}
return _table;
}
private IRenderable BuildTable()
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var table = new Table
{
Border = _border,
UseSafeBorder = _useSafeBorder,
BorderStyle = _borderStyle,
};
// Add columns
foreach (var order in GetWeekdays())
{
table.AddColumn(new TableColumn(order.GetAbbreviatedDayName(culture)));
}
var row = new List<IRenderable>();
var currentDay = 1;
var weekday = culture.DateTimeFormat.FirstDayOfWeek;
var weekdays = BuildWeekDayTable();
var daysInMonth = DateTime.DaysInMonth(Year, Month);
while (currentDay <= daysInMonth)
{
if (weekdays[currentDay - 1] == weekday)
{
if (_calendarEvents.Any(e => e.Month == Month && e.Day == currentDay))
{
row.Add(new Markup(currentDay.ToString(CultureInfo.InvariantCulture) + "*", _highlightStyle));
}
else
{
row.Add(new Text(currentDay.ToString(CultureInfo.InvariantCulture)));
}
currentDay++;
}
else
{
// Add empty cell
row.Add(Text.Empty);
}
if (row.Count == NumberOfWeekDays)
{
// Flush row
table.AddRow(row.ToArray());
row.Clear();
}
weekday = weekday.GetNextWeekDay();
}
if (row.Count > 0)
{
// Flush row
table.AddRow(row.ToArray());
row.Clear();
}
// We want all calendars to have the same height.
if (table.RowCount < ExpectedRowCount)
{
var diff = Math.Max(0, ExpectedRowCount - table.RowCount);
for (var i = 0; i < diff; i++)
{
table.AddEmptyRow();
}
}
return table;
}
private void MarkAsDirty(Action action)
{
action();
_dirty = true;
}
private DayOfWeek[] GetWeekdays()
{
var culture = Culture ?? CultureInfo.InvariantCulture;
var days = new DayOfWeek[7];
days[0] = culture.DateTimeFormat.FirstDayOfWeek;
for (var i = 1; i < 7; i++)
{
days[i] = days[i - 1].GetNextWeekDay();
}
return days;
}
private DayOfWeek[] BuildWeekDayTable()
{
var result = new List<DayOfWeek>();
for (var day = 0; day < DateTime.DaysInMonth(Year, Month); day++)
{
result.Add(new DateTime(Year, Month, day + 1).DayOfWeek);
}
return result.ToArray();
}
}
}

View File

@ -0,0 +1,54 @@
namespace Spectre.Console
{
/// <summary>
/// Represents a calendar event.
/// </summary>
public sealed class CalendarEvent
{
/// <summary>
/// Gets the description of the calendar event.
/// </summary>
public string Description { get; }
/// <summary>
/// Gets the year of the calendar event.
/// </summary>
public int Year { get; }
/// <summary>
/// Gets the month of the calendar event.
/// </summary>
public int Month { get; }
/// <summary>
/// Gets the day of the calendar event.
/// </summary>
public int Day { get; }
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(int year, int month, int day)
: this(string.Empty, year, month, day)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="CalendarEvent"/> class.
/// </summary>
/// <param name="description">The calendar event description.</param>
/// <param name="year">The year of the calendar event.</param>
/// <param name="month">The month of the calendar event.</param>
/// <param name="day">The day of the calendar event.</param>
public CalendarEvent(string description, int year, int month, int day)
{
Description = description ?? string.Empty;
Year = year;
Month = month;
Day = day;
}
}
}

View File

@ -50,6 +50,29 @@ namespace Spectre.Console
_items = new List<IRenderable>(items.Select(item => new Markup(item)));
}
/// <inheritdoc/>
protected override Measurement Measure(RenderContext context, int maxWidth)
{
var maxPadding = Math.Max(Padding.Left, Padding.Right);
var itemWidths = _items.Select(item => item.Measure(context, maxWidth).Max).ToArray();
var columnCount = CalculateColumnCount(maxWidth, itemWidths, _items.Count, maxPadding);
var rows = _items.Count / columnCount;
var greatestWidth = 0;
for (var row = 0; row < rows; row += columnCount)
{
var widths = itemWidths.Skip(row * columnCount).Take(columnCount).ToList();
var totalWidth = widths.Sum() + (maxPadding * (widths.Count - 1));
if (totalWidth > greatestWidth)
{
greatestWidth = totalWidth;
}
}
return new Measurement(greatestWidth, greatestWidth);
}
/// <inheritdoc/>
protected override IEnumerable<Segment> Render(RenderContext context, int maxWidth)
{