Merge pull request #265 from irihitech/calendar

CalendarView and DatePicker and DateRangePicker
This commit is contained in:
Dong Bin
2024-06-28 15:59:15 +08:00
committed by GitHub
29 changed files with 2575 additions and 10 deletions

View File

@@ -0,0 +1,91 @@
namespace Ursa.Controls;
public sealed class CalendarContext(int? year = null, int? month = null, int? startYear = null, int? endYear = null): IComparable<CalendarContext>
{
public int? Year { get; } = year;
public int? Month { get; } = month;
public int? StartYear { get; } = startYear;
public int? EndYear { get; } = endYear;
public CalendarContext Clone()
{
return new CalendarContext(Year, Month, StartYear, EndYear);
}
public static CalendarContext Today()
{
return new CalendarContext(DateTime.Today.Year, DateTime.Today.Month);
}
public CalendarContext With(int? year = null, int? month = null, int? startYear = null, int? endYear = null)
{
return new CalendarContext(year ?? Year, month ?? Month, startYear ?? StartYear, endYear ?? EndYear);
}
public CalendarContext NextMonth()
{
var year = Year;
var month = Month;
if (month == 12)
{
year++;
month = 1;
}
else
{
month++;
}
if (month is null)
{
month = 1;
}
return new CalendarContext(year, month, StartYear, EndYear);
}
public CalendarContext PreviousMonth()
{
var year = Year;
var month = Month;
if (month == 1)
{
year--;
month = 12;
}
else
{
month--;
}
if (month is null)
{
month = 1;
}
return new CalendarContext(year, month, StartYear, EndYear);
}
public CalendarContext NextYear()
{
return new CalendarContext(Year + 1, Month, StartYear, EndYear);
}
public CalendarContext PreviousYear()
{
return new CalendarContext(Year - 1, Month, StartYear, EndYear);
}
public int CompareTo(CalendarContext? other)
{
if (ReferenceEquals(this, other)) return 0;
if (ReferenceEquals(null, other)) return 1;
var yearComparison = Nullable.Compare(Year, other.Year);
if (yearComparison != 0) return yearComparison;
return Nullable.Compare(Month, other.Month);
}
public override string ToString()
{
return
$"Start: {StartYear?.ToString() ?? "null"}, End: {EndYear?.ToString() ?? "null"}, Year: {Year?.ToString() ?? "null"}, Month: {Month?.ToString() ?? "null"}";
}
}

View File

@@ -0,0 +1,205 @@
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Common;
namespace Ursa.Controls;
[PseudoClasses(PseudoClassName.PC_Pressed, PseudoClassName.PC_Selected,
PC_StartDate, PC_EndDate, PC_PreviewStartDate, PC_PreviewEndDate, PC_InRange, PC_Today, PC_Blackout,
PC_NotCurrentMonth)]
public class CalendarDayButton : ContentControl
{
public const string PC_StartDate = ":start-date";
public const string PC_EndDate = ":end-date";
public const string PC_PreviewStartDate = ":preview-start-date";
public const string PC_PreviewEndDate = ":preview-end-date";
public const string PC_InRange = ":in-range";
public const string PC_Today = ":today";
public const string PC_NotCurrentMonth = ":not-current-month";
public const string PC_Blackout = ":blackout";
private static HashSet<string> _pseudoClasses =
[
PseudoClassName.PC_Selected, PC_StartDate, PC_EndDate, PC_PreviewStartDate, PC_PreviewEndDate, PC_InRange
];
public static readonly RoutedEvent<CalendarDayButtonEventArgs> DateSelectedEvent =
RoutedEvent.Register<CalendarDayButton, CalendarDayButtonEventArgs>(
nameof(DateSelected), RoutingStrategies.Bubble);
public static readonly RoutedEvent<CalendarDayButtonEventArgs> DatePreviewedEvent =
RoutedEvent.Register<CalendarDayButton, CalendarDayButtonEventArgs>(
nameof(DatePreviewed), RoutingStrategies.Bubble);
private bool _isBlackout;
private bool _isEndDate;
private bool _isInRange;
private bool _isNotCurrentMonth;
private bool _isPreviewEndDate;
private bool _isPreviewStartDate;
private bool _isSelected;
private bool _isStartDate;
private bool _isToday;
static CalendarDayButton()
{
PressedMixin.Attach<CalendarDayButton>();
}
// internal CalendarDisplayControl? Owner { get; set; }
public bool IsToday
{
get => _isToday;
set
{
_isToday = value;
PseudoClasses.Set(PC_Today, value);
}
}
public bool IsStartDate
{
get => _isStartDate;
set
{
_isStartDate = value;
SetPseudoClass(PC_StartDate, value);
}
}
public bool IsEndDate
{
get => _isEndDate;
set
{
_isEndDate = value;
SetPseudoClass(PC_EndDate, value);
}
}
public bool IsPreviewStartDate
{
get => _isPreviewStartDate;
set
{
_isPreviewStartDate = value;
SetPseudoClass(PC_PreviewStartDate, value);
}
}
public bool IsPreviewEndDate
{
get => _isPreviewEndDate;
set
{
_isPreviewEndDate = value;
SetPseudoClass(PC_PreviewEndDate, value);
}
}
public bool IsInRange
{
get => _isInRange;
set
{
_isInRange = value;
SetPseudoClass(PC_InRange, value);
}
}
public bool IsSelected
{
get => _isSelected;
set
{
_isSelected = value;
SetPseudoClass(PseudoClassName.PC_Selected, value);
}
}
/// <summary>
/// Notice: IsBlackout is not equivalent to not IsEnabled. Blackout dates still react to pointerover actions.
/// </summary>
public bool IsBlackout
{
get => _isBlackout;
set
{
_isBlackout = value;
PseudoClasses.Set(PC_Blackout, value);
}
}
/// <summary>
/// Notice: IsNotCurrentMonth is not equivalent to not IsEnabled. Not current month dates still react to pointerover
/// and press action.
/// </summary>
public bool IsNotCurrentMonth
{
get => _isNotCurrentMonth;
set
{
_isNotCurrentMonth = value;
PseudoClasses.Set(PC_NotCurrentMonth, value);
}
}
public event EventHandler<CalendarDayButtonEventArgs> DateSelected
{
add => AddHandler(DateSelectedEvent, value);
remove => RemoveHandler(DateSelectedEvent, value);
}
public event EventHandler<CalendarDayButtonEventArgs> DatePreviewed
{
add => AddHandler(DateSelectedEvent, value);
remove => RemoveHandler(DateSelectedEvent, value);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (DataContext is DateTime d)
RaiseEvent(new CalendarDayButtonEventArgs(d) { RoutedEvent = DateSelectedEvent, Source = this });
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);
if (DataContext is DateTime d)
RaiseEvent(new CalendarDayButtonEventArgs(d) { RoutedEvent = DatePreviewedEvent, Source = this });
}
internal void ResetSelection()
{
foreach (var pc in _pseudoClasses)
{
PseudoClasses.Set(pc, false);
}
}
private void SetPseudoClass(string s, bool value)
{
if (_pseudoClasses.Contains(s) && value)
{
foreach (var pc in _pseudoClasses)
{
PseudoClasses.Set(pc, false);
}
}
PseudoClasses.Set(s, value);
}
}

View File

@@ -0,0 +1,8 @@
using Avalonia.Interactivity;
namespace Ursa.Controls;
public class CalendarDayButtonEventArgs(DateTime? date) : RoutedEventArgs
{
public DateTime? Date { get; private set; } = date;
}

View File

@@ -0,0 +1,617 @@
using System.Diagnostics;
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)
{
Debug.WriteLine(this.Name + " " + args.NewValue.Value);
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);
_dateContextSyncing = true;
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
_dateContextSyncing = false;
UpdateYearButtons();
return;
}
if (Mode == CalendarViewMode.Decade)
{
Mode = CalendarViewMode.Century;
var range = DateTimeHelper.GetCenturyViewRangeByYear(ContextDate.StartYear!.Value);
_dateContextSyncing = true;
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
_dateContextSyncing = false;
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 ?? ContextDate.StartYear.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);
_dateContextSyncing = true;
ContextDate = ContextDate.With(startYear: range.start, endYear: range.end);
_dateContextSyncing = false;
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(bool start = true, bool end = true)
{
if (start)
{
_previewStart = null;
_start = null;
}
if (end)
{
_previewEnd = null;
_end = null;
}
if (_monthGrid?.Children is null) return;
foreach (var child in _monthGrid.Children)
{
if (child is not CalendarDayButton button) continue;
if (start)
{
button.IsPreviewStartDate = false;
button.IsStartDate = false;
}
if (end)
{
button.IsEndDate = false;
button.IsInRange = false;
}
button.IsPreviewEndDate = false;
}
UpdateDayButtons();
}
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();
}
}

View File

@@ -0,0 +1,13 @@
namespace Ursa.Controls;
internal enum CalendarViewMode
{
// Show days in current month.
Month,
// Show Months in current year.
Year,
// The button represents 1 years.
Decade,
// The button represents 10 years.
Century,
}

View File

@@ -0,0 +1,68 @@
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Common;
namespace Ursa.Controls;
[PseudoClasses(PC_Range, PseudoClassName.PC_Selected)]
public class CalendarYearButton : ContentControl
{
public const string PC_Range = ":range";
public static readonly RoutedEvent<CalendarYearButtonEventArgs> ItemSelectedEvent =
RoutedEvent.Register<CalendarYearButton, CalendarYearButtonEventArgs>(
nameof(ItemSelected), RoutingStrategies.Bubble);
static CalendarYearButton()
{
PressedMixin.Attach<CalendarYearButton>();
}
internal CalendarContext CalendarContext { get; set; } = new ();
internal CalendarViewMode Mode { get; private set; }
public event EventHandler<CalendarDayButtonEventArgs> ItemSelected
{
add => AddHandler(ItemSelectedEvent, value);
remove => RemoveHandler(ItemSelectedEvent, value);
}
internal void SetContext(CalendarViewMode mode, CalendarContext context)
{
CalendarContext = context.Clone();
Mode = mode;
switch (Mode)
{
case CalendarViewMode.Year:
Content = DateTimeHelper.GetCurrentDateTimeFormatInfo()
.AbbreviatedMonthNames[(CalendarContext.Month - 1) ?? 0];
break;
case CalendarViewMode.Decade:
Content = CalendarContext.Year <= 0 || CalendarContext.Year > 9999
? null
: CalendarContext.Year?.ToString();
break;
case CalendarViewMode.Century:
Content = CalendarContext.EndYear <= 0 || CalendarContext.StartYear > 9999
? null
: CalendarContext.StartYear + "-" + CalendarContext.EndYear;
break;
default:
Content = null;
break;
}
IsEnabled = Content != null;
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
RaiseEvent(new CalendarYearButtonEventArgs(Mode, this.CalendarContext.Clone())
{ RoutedEvent = ItemSelectedEvent, Source = this });
}
}

View File

@@ -0,0 +1,16 @@
using Avalonia.Interactivity;
namespace Ursa.Controls;
public class CalendarYearButtonEventArgs: RoutedEventArgs
{
internal CalendarContext Context { get; }
internal CalendarViewMode Mode { get; }
/// <inheritdoc />
internal CalendarYearButtonEventArgs(CalendarViewMode mode, CalendarContext context)
{
Context = context;
Mode = mode;
}
}

View File

@@ -0,0 +1,191 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Contracts;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_Button, typeof(Button))]
[TemplatePart(PART_Popup, typeof(Popup))]
[TemplatePart(PART_TextBox, typeof(TextBox))]
[TemplatePart(PART_Calendar, typeof(CalendarView))]
public class DatePicker: DatePickerBase, IClearControl
{
public const string PART_Button = "PART_Button";
public const string PART_Popup = "PART_Popup";
public const string PART_TextBox = "PART_TextBox";
public const string PART_Calendar = "PART_Calendar";
private Button? _button;
private Popup? _popup;
private TextBox? _textBox;
private CalendarView? _calendar;
public static readonly StyledProperty<DateTime?> SelectedDateProperty = AvaloniaProperty.Register<DatePicker, DateTime?>(
nameof(SelectedDate), defaultBindingMode: BindingMode.TwoWay);
public DateTime? SelectedDate
{
get => GetValue(SelectedDateProperty);
set => SetValue(SelectedDateProperty, value);
}
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<DatePicker, string?>(
nameof(Watermark));
public string? Watermark
{
get => GetValue(WatermarkProperty);
set => SetValue(WatermarkProperty, value);
}
static DatePicker()
{
SelectedDateProperty.Changed.AddClassHandler<DatePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
}
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<DateTime?> args)
{
if (args.NewValue.Value is null)
{
_calendar?.ClearSelection();
_textBox?.Clear();
}
else
{
_calendar?.MarkDates(startDate: args.NewValue.Value, endDate: args.NewValue.Value);
_textBox?.SetValue(TextBox.TextProperty, args.NewValue.Value.Value.ToString(DisplayFormat ?? "yyyy-MM-dd"));
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _textBox);
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _textBox);
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
if (_calendar != null)
{
_calendar.DateSelected -= OnDateSelected;
}
_button = e.NameScope.Find<Button>(PART_Button);
_popup = e.NameScope.Find<Popup>(PART_Popup);
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
_calendar = e.NameScope.Find<CalendarView>(PART_Calendar);
Button.ClickEvent.AddHandler(OnButtonClick, RoutingStrategies.Tunnel, true, _button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
if (_calendar != null)
{
_calendar.DateSelected += OnDateSelected;
}
}
private void OnDateSelected(object sender, CalendarDayButtonEventArgs e)
{
SetCurrentValue(SelectedDateProperty, e.Date);
SetCurrentValue(IsDropdownOpenProperty, false);
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
Focus(NavigationMethod.Pointer);
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
private void OnTextBoxPointerPressed(object sender, PointerPressedEventArgs e)
{
if (_calendar is not null)
{
var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = new CalendarContext(date.Year, date.Month);
_calendar.UpdateDayButtons();
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (string.IsNullOrEmpty(_textBox?.Text))
{
SetCurrentValue(SelectedDateProperty, null);
_calendar?.ClearSelection();
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
if (DateTime.TryParse(_textBox?.Text, out var defaultTime))
{
SetCurrentValue(SelectedDateProperty, defaultTime);
_calendar?.MarkDates(startDate: defaultTime, endDate: defaultTime);
}
}
else
{
if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
out var date))
{
SetCurrentValue(SelectedDateProperty, date);
if (_calendar is not null)
{
var d = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = _calendar.ContextDate.With(year: date.Year, month: date.Month);
_calendar.UpdateDayButtons();
}
_calendar?.MarkDates(startDate: date, endDate: date);
}
}
}
private void OnTextBoxGetFocus(object sender, GotFocusEventArgs e)
{
if (_calendar is not null)
{
var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = _calendar.ContextDate.With(year: date.Year, month: date.Month);
_calendar.UpdateDayButtons();
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
{
SetCurrentValue(IsDropdownOpenProperty, false);
e.Handled = true;
return;
}
if (e.Key == Key.Down)
{
SetCurrentValue(IsDropdownOpenProperty, true);
e.Handled = true;
return;
}
if (e.Key == Key.Tab)
{
SetCurrentValue(IsDropdownOpenProperty, false);
return;
}
base.OnKeyDown(e);
}
public void Clear()
{
}
}

View File

@@ -0,0 +1,115 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Irihi.Avalonia.Shared.Contracts;
namespace Ursa.Controls;
public class DatePickerBase : TemplatedControl, IInnerContentControl, IPopupInnerContent
{
public static readonly StyledProperty<string?> DisplayFormatProperty =
AvaloniaProperty.Register<TimePicker, string?>(
nameof(DisplayFormat), "yyyy-MM-dd");
public static readonly StyledProperty<AvaloniaList<DateRange>> BlackoutDatesProperty =
AvaloniaProperty.Register<DatePickerBase, AvaloniaList<DateRange>>(nameof(BlackoutDates));
public static readonly StyledProperty<IDateSelector?> BlackoutDateRuleProperty =
AvaloniaProperty.Register<DatePickerBase, IDateSelector?>(nameof(BlackoutDateRule));
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
AvaloniaProperty.Register<DatePickerBase, DayOfWeek>(
nameof(FirstDayOfWeek), DateTimeHelper.GetCurrentDateTimeFormatInfo().FirstDayOfWeek);
public static readonly StyledProperty<bool> IsTodayHighlightedProperty =
AvaloniaProperty.Register<DatePickerBase, bool>(nameof(IsTodayHighlighted), true);
public static readonly StyledProperty<object?> InnerLeftContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(InnerLeftContent));
public static readonly StyledProperty<object?> InnerRightContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(InnerRightContent));
public static readonly StyledProperty<object?> PopupInnerTopContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(PopupInnerTopContent));
public static readonly StyledProperty<object?> PopupInnerBottomContentProperty =
AvaloniaProperty.Register<DatePickerBase, object?>(
nameof(PopupInnerBottomContent));
public static readonly StyledProperty<bool> IsDropdownOpenProperty = AvaloniaProperty.Register<DatePickerBase, bool>(
nameof(IsDropdownOpen), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> IsReadonlyProperty = AvaloniaProperty.Register<DatePickerBase, bool>(
nameof(IsReadonly));
public AvaloniaList<DateRange> BlackoutDates
{
get => GetValue(BlackoutDatesProperty);
set => SetValue(BlackoutDatesProperty, value);
}
public IDateSelector? BlackoutDateRule
{
get => GetValue(BlackoutDateRuleProperty);
set => SetValue(BlackoutDateRuleProperty, value);
}
public DayOfWeek FirstDayOfWeek
{
get => GetValue(FirstDayOfWeekProperty);
set => SetValue(FirstDayOfWeekProperty, value);
}
public bool IsTodayHighlighted
{
get => GetValue(IsTodayHighlightedProperty);
set => SetValue(IsTodayHighlightedProperty, value);
}
public bool IsReadonly
{
get => GetValue(IsReadonlyProperty);
set => SetValue(IsReadonlyProperty, value);
}
public bool IsDropdownOpen
{
get => GetValue(IsDropdownOpenProperty);
set => SetValue(IsDropdownOpenProperty, value);
}
public object? InnerLeftContent
{
get => GetValue(InnerLeftContentProperty);
set => SetValue(InnerLeftContentProperty, value);
}
public object? InnerRightContent
{
get => GetValue(InnerRightContentProperty);
set => SetValue(InnerRightContentProperty, value);
}
public object? PopupInnerTopContent
{
get => GetValue(PopupInnerTopContentProperty);
set => SetValue(PopupInnerTopContentProperty, value);
}
public object? PopupInnerBottomContent
{
get => GetValue(PopupInnerBottomContentProperty);
set => SetValue(PopupInnerBottomContentProperty, value);
}
public string? DisplayFormat
{
get => GetValue(DisplayFormatProperty);
set => SetValue(DisplayFormatProperty, value);
}
}

View File

@@ -0,0 +1,45 @@
namespace Ursa.Controls;
/// <summary>
/// Represents a date range. It can be a single day or a range of days. The range is inclusive.
/// </summary>
public sealed record DateRange
{
public DateRange(DateTime day)
{
Start = day.Date;
End = day.Date;
}
public DateRange(DateTime start, DateTime end)
{
if (DateTime.Compare(end, start) >= 0)
{
Start = start.Date;
End = end.Date;
}
else
{
Start = start.Date;
End = start.Date;
}
}
public DateTime Start { get; private set; }
public DateTime End { get; private set; }
public bool Contains(DateTime? date)
{
if (date is null) return false;
return date >= Start && date <= End;
}
}
internal static class DateRangeExtension
{
public static bool Contains(this IEnumerable<DateRange>? ranges, DateTime? date)
{
if (date is null || ranges is null) return false;
return ranges.Any(range => range.Contains(date));
}
}

View File

@@ -0,0 +1,370 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Helpers;
namespace Ursa.Controls;
[TemplatePart(PART_Button, typeof(Button))]
[TemplatePart(PART_Popup, typeof(Popup))]
[TemplatePart(PART_StartCalendar, typeof(CalendarView))]
[TemplatePart(PART_EndCalendar, typeof(CalendarView))]
[TemplatePart(PART_StartTextBox, typeof(TextBox))]
[TemplatePart(PART_EndTextBox, typeof(TextBox))]
public class DateRangePicker : DatePickerBase
{
public const string PART_Button = "PART_Button";
public const string PART_Popup = "PART_Popup";
public const string PART_StartCalendar = "PART_StartCalendar";
public const string PART_EndCalendar = "PART_EndCalendar";
public const string PART_StartTextBox = "PART_StartTextBox";
public const string PART_EndTextBox = "PART_EndTextBox";
public static readonly StyledProperty<DateTime?> SelectedStartDateProperty =
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
nameof(SelectedStartDate), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<DateTime?> SelectedEndDateProperty =
AvaloniaProperty.Register<DateRangePicker, DateTime?>(
nameof(SelectedEndDate), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<bool> EnableMonthSyncProperty = AvaloniaProperty.Register<DateRangePicker, bool>(
nameof(EnableMonthSync));
public bool EnableMonthSync
{
get => GetValue(EnableMonthSyncProperty);
set => SetValue(EnableMonthSyncProperty, value);
}
private Button? _button;
private CalendarView? _endCalendar;
private TextBox? _endTextBox;
private Popup? _popup;
private CalendarView? _startCalendar;
private TextBox? _startTextBox;
static DateRangePicker()
{
SelectedStartDateProperty.Changed.AddClassHandler<DateRangePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
SelectedEndDateProperty.Changed.AddClassHandler<DateRangePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
}
public DateTime? SelectedStartDate
{
get => GetValue(SelectedStartDateProperty);
set => SetValue(SelectedStartDateProperty, value);
}
public DateTime? SelectedEndDate
{
get => GetValue(SelectedEndDateProperty);
set => SetValue(SelectedEndDateProperty, value);
}
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<DateTime?> args)
{
if (args.Property == SelectedStartDateProperty)
{
if (args.NewValue.Value is null)
{
_startCalendar?.ClearSelection();
_startTextBox?.Clear();
}
else
{
_startCalendar?.MarkDates(args.NewValue.Value, args.NewValue.Value);
_startTextBox?.SetValue(TextBox.TextProperty,
args.NewValue.Value.Value.ToString(DisplayFormat ?? "yyyy-MM-dd"));
}
}
else if (args.Property == SelectedEndDateProperty)
{
if (args.NewValue.Value is null)
{
_endCalendar?.ClearSelection();
_endTextBox?.Clear();
}
else
{
_endCalendar?.MarkDates(args.NewValue.Value, args.NewValue.Value);
_endTextBox?.SetValue(TextBox.TextProperty,
args.NewValue.Value.Value.ToString(DisplayFormat ?? "yyyy-MM-dd"));
}
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _startTextBox);
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _startTextBox, _endTextBox);
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _startTextBox, _endTextBox);
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
if (_startCalendar != null)
{
_startCalendar.DateSelected -= OnDateSelected;
_startCalendar.DatePreviewed -= OnDatePreviewed;
_startCalendar.ContextDateChanged -= OnContextDateChanged;
}
if (_endCalendar != null)
{
_endCalendar.DateSelected -= OnDateSelected;
_endCalendar.DatePreviewed -= OnDatePreviewed;
_endCalendar.ContextDateChanged -= OnContextDateChanged;
}
_button = e.NameScope.Find<Button>(PART_Button);
_popup = e.NameScope.Find<Popup>(PART_Popup);
_startCalendar = e.NameScope.Find<CalendarView>(PART_StartCalendar);
_endCalendar = e.NameScope.Find<CalendarView>(PART_EndCalendar);
_startTextBox = e.NameScope.Find<TextBox>(PART_StartTextBox);
_endTextBox = e.NameScope.Find<TextBox>(PART_EndTextBox);
Button.ClickEvent.AddHandler(OnButtonClick, RoutingStrategies.Tunnel, true, _button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _startTextBox, _endTextBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _startTextBox, _endTextBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _startTextBox, _endTextBox);
if (_startCalendar != null)
{
_startCalendar.DateSelected += OnDateSelected;
_startCalendar.DatePreviewed += OnDatePreviewed;
_startCalendar.ContextDateChanged += OnContextDateChanged;
}
if (_endCalendar != null)
{
_endCalendar.DateSelected += OnDateSelected;
_endCalendar.DatePreviewed += OnDatePreviewed;
_endCalendar.ContextDateChanged += OnContextDateChanged;
}
}
private void OnContextDateChanged(object sender, CalendarContext e)
{
if(sender == _startCalendar && _startCalendar?.Mode == CalendarViewMode.Month)
{
bool needsUpdate = EnableMonthSync || _startCalendar?.ContextDate.CompareTo(_endCalendar?.ContextDate) >= 0;
if (needsUpdate)
{
_endCalendar?.SyncContextDate(_startCalendar?.ContextDate.NextMonth());
}
}
else if(sender == _endCalendar && _endCalendar?.Mode == CalendarViewMode.Month)
{
bool needsUpdate = EnableMonthSync || _endCalendar?.ContextDate.CompareTo(_startCalendar?.ContextDate) <= 0;
if (needsUpdate)
{
_startCalendar?.SyncContextDate(_endCalendar?.ContextDate.PreviousMonth());
}
}
}
private DateTime? _previewStart;
private DateTime? _previewEnd;
private bool? _start;
private void OnDatePreviewed(object sender, CalendarDayButtonEventArgs e)
{
if (_start == true)
{
_previewStart = e.Date;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
else if(_start == false)
{
_previewEnd = e.Date;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
}
private void OnDateSelected(object sender, CalendarDayButtonEventArgs e)
{
if (_start == true)
{
if (SelectedEndDate < e.Date)
{
SelectedEndDate = null;
}
SetCurrentValue(SelectedStartDateProperty, e.Date);
_startTextBox?.SetValue(TextBox.TextProperty, e.Date?.ToString(DisplayFormat ?? "yyyy-MM-dd"));
_start = false;
_previewStart = null;
_previewEnd = null;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endTextBox?.Focus();
}
else if(_start == false)
{
if (SelectedStartDate > e.Date)
{
SelectedStartDate = null;
}
SetCurrentValue(SelectedEndDateProperty, e.Date);
_endTextBox?.SetValue(TextBox.TextProperty, e.Date?.ToString(DisplayFormat ?? "yyyy-MM-dd"));
_start = null;
_previewStart = null;
_previewEnd = null;
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
if (SelectedStartDate is null)
{
_start = true;
_startTextBox?.Focus();
}
else
{
SetCurrentValue(IsDropdownOpenProperty, false);
}
}
}
private void OnButtonClick(object sender, RoutedEventArgs e)
{
Focus(NavigationMethod.Pointer);
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
_start = true;
}
private void OnTextBoxPointerPressed(object sender, PointerPressedEventArgs e)
{
if (sender == _startTextBox)
{
_start = true;
if (_startCalendar is not null)
{
var date = SelectedStartDate ?? DateTime.Today;
_startCalendar.ContextDate = new CalendarContext(date.Year, date.Month);
_startCalendar.UpdateDayButtons();
}
if (_endCalendar is not null)
{
var date2 = SelectedEndDate;
if (date2 is null || (date2.Value.Year==SelectedStartDate?.Year && date2.Value.Month == SelectedStartDate?.Month))
{
date2 = SelectedStartDate ?? DateTime.Today;
date2 = date2.Value.AddMonths(1);
}
_endCalendar.ContextDate = new CalendarContext(date2?.Year, date2?.Month);
_endCalendar.UpdateDayButtons();
}
}
else if (sender == _endTextBox)
{
_start = false;
if (_endCalendar is not null)
{
var date = SelectedEndDate ?? DateTime.Today;
_endCalendar.ContextDate = new CalendarContext(date.Year, date.Month);
_endCalendar.UpdateDayButtons();
}
if (_startCalendar is not null)
{
var date2 = SelectedStartDate;
if (date2 is null || (date2.Value.Year==SelectedEndDate?.Year && date2.Value.Month == SelectedEndDate?.Month))
{
date2 = SelectedStartDate ?? DateTime.Today;
date2 = date2.Value.AddMonths(-1);
}
_startCalendar.ContextDate = new CalendarContext(date2?.Year, date2?.Month);
_startCalendar.UpdateDayButtons();
}
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
private void OnTextChanged(object sender, TextChangedEventArgs e)
{
if (sender == _startTextBox)
{
OnTextChangedInternal(_startTextBox, SelectedStartDateProperty);
}
else if (sender == _endTextBox)
{
OnTextChangedInternal(_endTextBox, SelectedEndDateProperty);
}
}
private void OnTextChangedInternal(TextBox? textBox, AvaloniaProperty property)
{
if (string.IsNullOrEmpty(textBox?.Text))
{
SetCurrentValue(property, null);
_startCalendar?.ClearSelection(start: true);
_endCalendar?.ClearSelection(end: true);
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
if (DateTime.TryParse(textBox?.Text, out var defaultTime))
{
SetCurrentValue(property, defaultTime);
_startCalendar?.MarkDates(startDate: defaultTime, endDate: defaultTime);
_endCalendar?.MarkDates(startDate: defaultTime, endDate: defaultTime);
}
}
else
{
if (DateTime.TryParseExact(textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
out var date))
{
SetCurrentValue(property, date);
if (_startCalendar is not null)
{
var date1 = SelectedStartDate ?? DateTime.Today;
_startCalendar.ContextDate = new CalendarContext(date1.Year, date.Month);
//_startCalendar.SyncContextDate(new CalendarContext(date1.Year, date1.Month));
_startCalendar.UpdateDayButtons();
_startCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
if (_endCalendar is not null)
{
var date2 = SelectedEndDate ?? SelectedStartDate ?? DateTime.Today;
if (SelectedEndDate is null) date2 = date2.AddMonths(1);
_endCalendar.ContextDate = new CalendarContext(date2.Year, date2.Month);
//_endCalendar.SyncContextDate(new CalendarContext(date2.Year, date2.Month));
_endCalendar.UpdateDayButtons();
_endCalendar?.MarkDates(SelectedStartDate, SelectedEndDate, _previewStart, _previewEnd);
}
}
}
}
private void OnTextBoxGetFocus(object sender, GotFocusEventArgs e)
{
if (_startCalendar is not null && _startCalendar?.Mode == CalendarViewMode.Month)
{
var date = SelectedStartDate ?? DateTime.Today;
//_startCalendar.ContextDate = new CalendarContext(date.Year, date.Month);
//_startCalendar.UpdateDayButtons();
}
if (_endCalendar is not null && _endCalendar?.Mode == CalendarViewMode.Month)
{
var date2 = SelectedStartDate ?? DateTime.Today;
date2 = date2.AddMonths(1);
//_endCalendar.ContextDate = new CalendarContext(date2.Year, date2.Month);
//_endCalendar.UpdateDayButtons();
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
//base.OnLostFocus(e);
//SetCurrentValue(IsDropdownOpenProperty, false);
//_start = null;
}
}

View File

@@ -0,0 +1,57 @@
using System.Globalization;
namespace Ursa.Controls;
internal static class DateTimeHelper
{
public const int NumberOfDaysPerWeek = 7;
public const int NumberOfWeeksPerMonth = 6;
public static DateTimeFormatInfo GetCurrentDateTimeFormatInfo()
{
if (CultureInfo.CurrentCulture.Calendar is GregorianCalendar) return CultureInfo.CurrentCulture.DateTimeFormat;
System.Globalization.Calendar? calendar =
CultureInfo.CurrentCulture.OptionalCalendars.OfType<GregorianCalendar>().FirstOrDefault();
string cultureName = calendar is null ? CultureInfo.InvariantCulture.Name : CultureInfo.CurrentCulture.Name;
var dt = new CultureInfo(cultureName).DateTimeFormat;
dt.Calendar = calendar ?? new GregorianCalendar();
return dt;
}
public static DateTime GetFirstDayOfMonth(this DateTime date)
{
return new DateTime(date.Year, date.Month, 1);
}
public static DateTime GetLastDayOfMonth(this DateTime date)
{
return new DateTime(date.Year, date.Month, DateTime.DaysInMonth(date.Year, date.Month));
}
public static int CompareYearMonth(DateTime dt1, DateTime dt2)
{
return (dt1.Year - dt2.Year) * 12 + dt1.Month - dt2.Month;
}
public static DateTime Min(DateTime d1, DateTime d2)
{
return d1.Ticks > d2.Ticks ? d2 : d1;
}
public static DateTime Max(DateTime d1, DateTime d2)
{
return d1.Ticks < d2.Ticks ? d2 : d1;
}
public static (int start, int end) GetDecadeViewRangeByYear(int year)
{
int start = year / 10 * 10;
return new ValueTuple<int, int>(start, start + 10);
}
public static (int start, int end) GetCenturyViewRangeByYear(int year)
{
int start = year / 100 * 100;
return new ValueTuple<int, int>(start, start + 100);
}
}

View File

@@ -0,0 +1,17 @@
namespace Ursa.Controls;
public interface IDateSelector
{
public bool Match(DateTime? date);
}
public class WeekendDateSelector: IDateSelector
{
public static WeekendDateSelector Instance { get; } = new WeekendDateSelector();
public bool Match(DateTime? date)
{
if (date is null) return false;
return date.Value.DayOfWeek == DayOfWeek.Saturday || date.Value.DayOfWeek == DayOfWeek.Sunday;
}
}