Files
Ursa.Avalonia/src/Ursa/Controls/DateTimePicker/CalendarView.cs
2024-06-23 04:34:23 +08:00

592 lines
23 KiB
C#

using System.Globalization;
using System.Runtime.CompilerServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Irihi.Avalonia.Shared.Helpers;
using Calendar = System.Globalization.Calendar;
namespace Ursa.Controls;
[TemplatePart(PART_FastNextButton, typeof(Button))]
[TemplatePart(PART_FastPreviousButton, typeof(Button))]
[TemplatePart(PART_NextButton, typeof(Button))]
[TemplatePart(PART_PreviousButton, typeof(Button))]
[TemplatePart(PART_YearButton, typeof(Button))]
[TemplatePart(PART_MonthButton, typeof(Button))]
[TemplatePart(PART_HeaderButton, typeof(Button))]
[TemplatePart(PART_MonthGrid, typeof(Grid))]
[TemplatePart(PART_YearGrid, typeof(Grid))]
[PseudoClasses(PC_Month)]
public class CalendarView : TemplatedControl
{
public const string PART_FastNextButton = "PART_FastNextButton";
public const string PART_FastPreviousButton = "PART_FastPreviousButton";
public const string PART_NextButton = "PART_NextButton";
public const string PART_PreviousButton = "PART_PreviousButton";
public const string PART_YearButton = "PART_YearButton";
public const string PART_MonthButton = "PART_MonthButton";
public const string PART_HeaderButton = "PART_HeaderButton";
public const string PART_MonthGrid = "PART_MonthGrid";
public const string PART_YearGrid = "PART_YearGrid";
public const string PC_Month = ":month";
private const string ShortestDayName = "ShortestDayName";
internal static readonly DirectProperty<CalendarView, CalendarViewMode> ModeProperty =
AvaloniaProperty.RegisterDirect<CalendarView, CalendarViewMode>(
nameof(Mode), o => o.Mode, (o, v) => o.Mode = v);
public static readonly StyledProperty<bool> IsTodayHighlightedProperty =
DatePickerBase.IsTodayHighlightedProperty.AddOwner<CalendarView>();
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
DatePickerBase.FirstDayOfWeekProperty.AddOwner<CalendarView>();
private readonly Calendar _calendar = new GregorianCalendar();
private Button? _fastNextButton;
private Button? _fastPreviousButton;
private Button? _headerButton;
private CalendarViewMode _mode;
private Button? _monthButton;
private Grid? _monthGrid;
private Button? _nextButton;
private Button? _previousButton;
private Button? _yearButton;
private Grid? _yearGrid;
private DateTime? _start;
private DateTime? _end;
private DateTime? _previewStart;
private DateTime? _previewEnd;
static CalendarView()
{
FirstDayOfWeekProperty.Changed.AddClassHandler<CalendarView, DayOfWeek>((view, args) =>
view.OnFirstDayOfWeekChanged(args));
ModeProperty.Changed.AddClassHandler<CalendarView, CalendarViewMode>((view, args) =>
{
view.PseudoClasses.Set(PC_Month, args.NewValue.Value == CalendarViewMode.Month);
});
ContextDateProperty.Changed.AddClassHandler<CalendarView, CalendarContext>((view, args) =>
view.OnContextDateChanged(args));
}
private void OnContextDateChanged(AvaloniaPropertyChangedEventArgs<CalendarContext> args)
{
if (!_dateContextSyncing)
{
ContextDateChanged?.Invoke(this, args.NewValue.Value);
}
//UpdateDayButtons();
//UpdateYearButtons();
}
internal CalendarViewMode Mode
{
get => _mode;
set => SetAndRaise(ModeProperty, ref _mode, value);
}
private CalendarContext _contextDate = new();
public static readonly DirectProperty<CalendarView, CalendarContext> ContextDateProperty = AvaloniaProperty.RegisterDirect<CalendarView, CalendarContext>(
nameof(ContextDate), o => o.ContextDate, (o, v) => o.ContextDate = v);
public CalendarContext ContextDate
{
get => _contextDate;
internal set => SetAndRaise(ContextDateProperty, ref _contextDate, value);
}
public bool IsTodayHighlighted
{
get => GetValue(IsTodayHighlightedProperty);
set => SetValue(IsTodayHighlightedProperty, value);
}
public DayOfWeek FirstDayOfWeek
{
get => GetValue(FirstDayOfWeekProperty);
set => SetValue(FirstDayOfWeekProperty, value);
}
public event EventHandler<CalendarDayButtonEventArgs>? DateSelected;
public event EventHandler<CalendarDayButtonEventArgs>? DatePreviewed;
internal event EventHandler<CalendarContext>? ContextDateChanged;
private void OnFirstDayOfWeekChanged(AvaloniaPropertyChangedEventArgs<DayOfWeek> args)
{
UpdateMonthViewHeader(args.NewValue.Value);
UpdateDayButtons();
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
Button.ClickEvent.RemoveHandler(OnHeaderYearButtonClick, _yearButton);
Button.ClickEvent.RemoveHandler(OnHeaderMonthButtonClick, _monthButton);
Button.ClickEvent.RemoveHandler(OnHeaderButtonClick, _headerButton);
Button.ClickEvent.RemoveHandler(OnFastPrevious, _fastPreviousButton);
Button.ClickEvent.RemoveHandler(OnPrevious, _previousButton);
Button.ClickEvent.RemoveHandler(OnNext, _nextButton);
Button.ClickEvent.RemoveHandler(OnFastNext, _fastNextButton);
_monthGrid = e.NameScope.Find<Grid>(PART_MonthGrid);
_yearGrid = e.NameScope.Find<Grid>(PART_YearGrid);
_yearButton = e.NameScope.Find<Button>(PART_YearButton);
_monthButton = e.NameScope.Find<Button>(PART_MonthButton);
_headerButton = e.NameScope.Find<Button>(PART_HeaderButton);
_fastPreviousButton = e.NameScope.Find<Button>(PART_FastPreviousButton);
_previousButton = e.NameScope.Find<Button>(PART_PreviousButton);
_nextButton = e.NameScope.Find<Button>(PART_NextButton);
_fastNextButton = e.NameScope.Find<Button>(PART_FastNextButton);
Button.ClickEvent.AddHandler(OnHeaderYearButtonClick, _yearButton);
Button.ClickEvent.AddHandler(OnHeaderMonthButtonClick, _monthButton);
Button.ClickEvent.AddHandler(OnHeaderButtonClick, _headerButton);
Button.ClickEvent.AddHandler(OnFastPrevious, _fastPreviousButton);
Button.ClickEvent.AddHandler(OnPrevious, _previousButton);
Button.ClickEvent.AddHandler(OnNext, _nextButton);
Button.ClickEvent.AddHandler(OnFastNext, _fastNextButton);
ContextDate = new CalendarContext(DateTime.Today.Year, DateTime.Today.Month);
PseudoClasses.Set(PC_Month, Mode == CalendarViewMode.Month);
InitializeGridButtons();
UpdateDayButtons();
UpdateYearButtons();
}
private void OnFastNext(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.With(year: ContextDate.Year + 1);
UpdateDayButtons();
}
}
private void OnNext(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.NextMonth();
UpdateDayButtons();
}
else if (Mode == CalendarViewMode.Year)
{
ContextDate = ContextDate.NextYear();
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Decade)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear + 10, endYear: ContextDate.EndYear + 10);
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Century)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear + 100, endYear: ContextDate.EndYear + 100);
UpdateYearButtons();
}
}
private void OnPrevious(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.PreviousMonth();
UpdateDayButtons();
}
else if (Mode == CalendarViewMode.Year)
{
ContextDate = ContextDate.With(year: ContextDate.Year - 1);
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Decade)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear - 10, endYear: ContextDate.EndYear - 10);
UpdateYearButtons();
}
else if (Mode == CalendarViewMode.Century)
{
ContextDate = ContextDate.With(startYear: ContextDate.StartYear - 100, endYear: ContextDate.EndYear - 100);
UpdateYearButtons();
}
}
private void OnFastPrevious(object sender, RoutedEventArgs e)
{
if (Mode == CalendarViewMode.Month)
{
ContextDate = ContextDate.PreviousYear();
UpdateDayButtons();
}
}
/// <summary>
/// Rule:
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnHeaderButtonClick(object sender, RoutedEventArgs e)
{
// Header button should be hidden in Month mode.
if (Mode == CalendarViewMode.Month) return;
if (Mode == CalendarViewMode.Year)
{
Mode = CalendarViewMode.Decade;
var range = DateTimeHelper.GetDecadeViewRangeByYear(ContextDate.Year!.Value);
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
UpdateYearButtons();
return;
}
if (Mode == CalendarViewMode.Decade)
{
Mode = CalendarViewMode.Century;
var range = DateTimeHelper.GetCenturyViewRangeByYear(ContextDate.StartYear!.Value);
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
UpdateYearButtons();
return;
}
if (Mode == CalendarViewMode.Century) return;
}
/// <summary>
/// Generate Buttons and labels for MonthView.
/// Generate Buttons for YearView.
/// This method should be called only once.
/// </summary>
private void InitializeGridButtons()
{
// Generate Day titles (Sun, Mon, Tue, Wed, Thu, Fri, Sat) based on FirstDayOfWeek and culture.
var count = 7 + 7 * 7;
var children = new List<Control>(count);
var dayOfWeek = (int)FirstDayOfWeek;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo();
for (var i = 0; i < 7; i++)
{
var d = (dayOfWeek + i) % DateTimeHelper.NumberOfDaysPerWeek;
var cell = new TextBlock { Text = info.ShortestDayNames[d], Tag = ShortestDayName };
cell.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
cell.SetValue(Grid.RowProperty, 0);
cell.SetValue(Grid.ColumnProperty, i);
children.Add(cell);
}
// Generate day buttons.
for (var i = 2; i < DateTimeHelper.NumberOfWeeksPerMonth + 2; i++)
for (var j = 0; j < DateTimeHelper.NumberOfDaysPerWeek; j++)
{
var cell = new CalendarDayButton();
cell.SetValue(Grid.RowProperty, i);
cell.SetValue(Grid.ColumnProperty, j);
cell.AddHandler(CalendarDayButton.DateSelectedEvent, OnCellDateSelected);
cell.AddHandler(CalendarDayButton.DatePreviewedEvent, OnCellDatePreviewed);
children.Add(cell);
}
_monthGrid?.Children.AddRange(children);
// Generate month/year buttons.
for (var i = 0; i < 12; i++)
{
var button = new CalendarYearButton();
Grid.SetRow(button, i / 3);
Grid.SetColumn(button, i % 3);
button.AddHandler(CalendarYearButton.ItemSelectedEvent, OnYearItemSelected);
_yearGrid?.Children.Add(button);
}
}
internal void UpdateDayButtons()
{
if (_monthGrid is null || Mode != CalendarViewMode.Month) return;
var children = _monthGrid.Children;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo();
var date = new DateTime(ContextDate.Year.Value, ContextDate.Month.Value, 1);
var dayBefore = PreviousMonthDays(date);
var dateToSet = date.GetFirstDayOfMonth().AddDays(-dayBefore);
for (var i = 7; i < children.Count; i++)
{
var day = dateToSet;
var cell = children[i] as CalendarDayButton;
if (cell is null) continue;
cell.DataContext = day;
if (IsTodayHighlighted) cell.IsToday = day == DateTime.Today;
cell.Content = day.Day.ToString(info);
dateToSet = dateToSet.AddDays(1);
}
FadeOutDayButtons();
MarkDates(_start, _end, _previewStart, _previewEnd);
UpdateHeaderButtons();
}
private void UpdateYearButtons()
{
if (_yearGrid is null) return;
var mode = Mode;
var contextDate = ContextDate;
if (mode == CalendarViewMode.Century && contextDate.StartYear.HasValue)
{
var range = DateTimeHelper.GetCenturyViewRangeByYear(contextDate.StartYear.Value);
var start = range.start - 10;
for (var i = 0; i < 12; i++)
{
var child = _yearGrid.Children[i] as CalendarYearButton;
child?.SetContext(CalendarViewMode.Century,
new CalendarContext(startYear: start, endYear: start + 10));
start += 10;
}
}
else if (mode == CalendarViewMode.Decade && contextDate.StartYear.HasValue)
{
var range = DateTimeHelper.GetDecadeViewRangeByYear(contextDate.StartYear.Value);
var year = range.start - 1;
for (var i = 0; i < 12; i++)
{
var child = _yearGrid.Children[i] as CalendarYearButton;
child?.SetContext(CalendarViewMode.Decade,
new CalendarContext(year: year));
year++;
}
}
else if (mode == CalendarViewMode.Year)
{
for (var i = 0; i < 12; i++)
{
var child = _yearGrid.Children[i] as CalendarYearButton;
child?.SetContext(CalendarViewMode.Year, new CalendarContext(month: i + 1));
}
}
UpdateHeaderButtons();
}
private void FadeOutDayButtons()
{
if (_monthGrid is null) return;
var children = _monthGrid.Children;
for (var i = 7; i < children.Count; i++)
if (children[i] is CalendarDayButton { DataContext: DateTime d } button)
button.IsNotCurrentMonth = d.Month != ContextDate.Month;
}
private void UpdateMonthViewHeader(DayOfWeek day)
{
var dayOfWeek = (int)day;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo();
var texts = _monthGrid?.Children.Where(a => a is TextBlock { Tag: ShortestDayName }).ToList();
if (texts is not null)
for (var i = 0; i < 7; i++)
{
var d = (dayOfWeek + i) % DateTimeHelper.NumberOfDaysPerWeek;
texts[i].SetValue(TextBlock.TextProperty, info.ShortestDayNames[d]);
texts[i].SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
texts[i].SetValue(Grid.RowProperty, 0);
texts[i].SetValue(Grid.ColumnProperty, i);
}
}
private int PreviousMonthDays(DateTime date)
{
var firstDay = date.GetFirstDayOfMonth();
var dayOfWeek = _calendar.GetDayOfWeek(firstDay);
var firstDayOfWeek = FirstDayOfWeek;
var i = (dayOfWeek - firstDayOfWeek + DateTimeHelper.NumberOfDaysPerWeek) % DateTimeHelper.NumberOfDaysPerWeek;
return i == 0 ? DateTimeHelper.NumberOfDaysPerWeek : i;
}
private void OnCellDatePreviewed(object sender, CalendarDayButtonEventArgs e)
{
DatePreviewed?.Invoke(this, e);
}
private void OnCellDateSelected(object sender, CalendarDayButtonEventArgs e)
{
if (e.Date.HasValue && e.Date.Value.Month != ContextDate.Month)
{
ContextDate = ContextDate.With(year: e.Date.Value.Year, month: e.Date.Value.Month);
UpdateDayButtons();
}
DateSelected?.Invoke(this, e);
}
/// <summary>
/// Click on Month Header button. Calendar switch from month mode to year mode.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnHeaderMonthButtonClick(object sender, RoutedEventArgs e)
{
SetCurrentValue(ModeProperty, CalendarViewMode.Year);
UpdateYearButtons();
}
/// <summary>
/// Click on Year Header button. Calendar switch from month mode to decade mode.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnHeaderYearButtonClick(object sender, RoutedEventArgs e)
{
if (_yearGrid is null) return;
SetCurrentValue(ModeProperty, CalendarViewMode.Decade);
var range = DateTimeHelper.GetDecadeViewRangeByYear(ContextDate.Year!.Value);
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
UpdateYearButtons();
}
/// <summary>
/// Click on CalendarYearButton in YearView.
/// Mode switch rules are:
/// 1. Month -> Not supported, buttons are hidden.
/// 2. Year -> Month: Set the date to the selected year and switch to Month mode.
/// 3. Decade -> Year: Set the date to the selected year and switch to Year mode.
/// 4. Century -> Decade: Set the date to the selected year and switch to Decade mode.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
private void OnYearItemSelected(object sender, CalendarYearButtonEventArgs e)
{
if (_yearGrid is null) return;
var buttons = _yearGrid.Children.OfType<CalendarYearButton>().ToList();
if (Mode == CalendarViewMode.Century)
{
Mode = CalendarViewMode.Decade;
ContextDate = e.Context.With(year: null);
}
else if (Mode == CalendarViewMode.Decade)
{
Mode = CalendarViewMode.Year;
ContextDate = e.Context.Clone();
}
else if (Mode == CalendarViewMode.Year)
{
Mode = CalendarViewMode.Month;
ContextDate = ContextDate.With(null, e.Context.Month, null, null);
UpdateDayButtons();
}
else if (Mode == CalendarViewMode.Month)
{
return;
}
UpdateHeaderButtons();
UpdateYearButtons();
}
private void UpdateHeaderButtons()
{
if (Mode == CalendarViewMode.Century)
{
IsVisibleProperty.SetValue(true, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(false, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
_headerButton?.SetValue(ContentControl.ContentProperty,
ContextDate.StartYear + "-" + ContextDate.EndYear);
}
else if (Mode == CalendarViewMode.Decade)
{
IsVisibleProperty.SetValue(true, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(false, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
_headerButton?.SetValue(ContentControl.ContentProperty,
ContextDate.StartYear + "-" + ContextDate.EndYear);
}
else if (Mode == CalendarViewMode.Year)
{
IsVisibleProperty.SetValue(true, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(false, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
_headerButton?.SetValue(ContentControl.ContentProperty, ContextDate.Year);
}
else if (Mode == CalendarViewMode.Month)
{
IsVisibleProperty.SetValue(false, _headerButton, _yearGrid);
IsVisibleProperty.SetValue(true, _yearButton, _monthButton, _monthGrid, _fastPreviousButton,
_fastNextButton);
// _headerButton?.SetValue(ContentControl.ContentProperty, ContextCalendar.Year);
_yearButton?.SetValue(ContentControl.ContentProperty, ContextDate.Year);
_monthButton?.SetValue(ContentControl.ContentProperty,
DateTimeHelper.GetCurrentDateTimeFormatInfo().AbbreviatedMonthNames[ContextDate.Month-1 ?? 0]);
}
bool canForward = !(ContextDate.EndYear <= 0) && !(ContextDate.Year <= 0);
bool canNext = !(ContextDate.StartYear > 9999) && !(ContextDate.EndYear > 9999);
IsEnabledProperty.SetValue(canForward, _previousButton, _fastPreviousButton);
IsEnabledProperty.SetValue(canNext, _nextButton, _fastNextButton);
}
public void MarkDates(DateTime? startDate = null, DateTime? endDate = null, DateTime? previewStartDate = null, DateTime? previewEndDate = null)
{
_start = startDate;
_end = endDate;
_previewStart = previewStartDate;
_previewEnd = previewEndDate;
if (_monthGrid?.Children is null) return;
DateTime start = startDate ?? DateTime.MaxValue;
DateTime end = endDate ?? DateTime.MinValue;
DateTime previewStart = previewStartDate ?? DateTime.MaxValue;
DateTime previewEnd = previewEndDate ?? DateTime.MinValue;
DateTime rangeStart = DateTimeHelper.Min(start, previewStart);
DateTime rangeEnd = DateTimeHelper.Max(end, previewEnd);
foreach (var child in _monthGrid.Children)
{
if (child is not CalendarDayButton { DataContext: DateTime d } button) continue;
button.ResetSelection();
if(d.Month != ContextDate.Month) continue;
if (d < rangeEnd && d > rangeStart) button.IsInRange = true;
if (d == previewStart) button.IsPreviewStartDate = true;
if (d == previewEnd) button.IsPreviewEndDate = true;
if (d == startDate) button.IsStartDate = true;
if (d == endDate) button.IsEndDate = true;
if (d == startDate && d == endDate) button.IsSelected = true;
}
}
public void ClearSelection()
{
_start = null;
_end = null;
_previewStart = null;
_previewEnd = null;
if (_monthGrid?.Children is null) return;
foreach (var child in _monthGrid.Children)
{
if (child is not CalendarDayButton button) continue;
button.IsStartDate = false;
button.IsEndDate = false;
button.IsInRange = false;
}
}
protected override void OnPointerExited(PointerEventArgs e)
{
base.OnPointerExited(e);
DatePreviewed?.Invoke(this, new CalendarDayButtonEventArgs(null));
}
private bool _dateContextSyncing = false;
/// <summary>
/// Used for syncing the context date for DateRangePicker. mark a flag to avoid infinitely loop.
/// </summary>
/// <param name="context"></param>
internal void SyncContextDate(CalendarContext? context)
{
if (context is null) return;
_dateContextSyncing = true;
ContextDate = context;
_dateContextSyncing = false;
UpdateDayButtons();
UpdateYearButtons();
}
}