Merge pull request #265 from irihitech/calendar
CalendarView and DatePicker and DateRangePicker
This commit is contained in:
91
src/Ursa/Controls/DateTimePicker/CalendarContext.cs
Normal file
91
src/Ursa/Controls/DateTimePicker/CalendarContext.cs
Normal 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"}";
|
||||
}
|
||||
}
|
||||
205
src/Ursa/Controls/DateTimePicker/CalendarDayButton.cs
Normal file
205
src/Ursa/Controls/DateTimePicker/CalendarDayButton.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace Ursa.Controls;
|
||||
|
||||
public class CalendarDayButtonEventArgs(DateTime? date) : RoutedEventArgs
|
||||
{
|
||||
public DateTime? Date { get; private set; } = date;
|
||||
}
|
||||
617
src/Ursa/Controls/DateTimePicker/CalendarView.cs
Normal file
617
src/Ursa/Controls/DateTimePicker/CalendarView.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
13
src/Ursa/Controls/DateTimePicker/CalendarViewMode.cs
Normal file
13
src/Ursa/Controls/DateTimePicker/CalendarViewMode.cs
Normal 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,
|
||||
}
|
||||
68
src/Ursa/Controls/DateTimePicker/CalendarYearButton.cs
Normal file
68
src/Ursa/Controls/DateTimePicker/CalendarYearButton.cs
Normal 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 });
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
191
src/Ursa/Controls/DateTimePicker/DatePicker.cs
Normal file
191
src/Ursa/Controls/DateTimePicker/DatePicker.cs
Normal 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()
|
||||
{
|
||||
|
||||
}
|
||||
}
|
||||
115
src/Ursa/Controls/DateTimePicker/DatePickerBase.cs
Normal file
115
src/Ursa/Controls/DateTimePicker/DatePickerBase.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
45
src/Ursa/Controls/DateTimePicker/DateRange.cs
Normal file
45
src/Ursa/Controls/DateTimePicker/DateRange.cs
Normal 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));
|
||||
}
|
||||
}
|
||||
370
src/Ursa/Controls/DateTimePicker/DateRangePicker.cs
Normal file
370
src/Ursa/Controls/DateTimePicker/DateRangePicker.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
57
src/Ursa/Controls/DateTimePicker/DateTimeHelper.cs
Normal file
57
src/Ursa/Controls/DateTimePicker/DateTimeHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
17
src/Ursa/Controls/DateTimePicker/IDateSelector.cs
Normal file
17
src/Ursa/Controls/DateTimePicker/IDateSelector.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user