From 2235292681bf2f12153929487e52df647042435f Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sun, 12 May 2024 16:59:45 +0800 Subject: [PATCH] feat: add a single state machine to manage selection. WIP. --- demo/Ursa.Demo/Pages/DatePickerDemo.axaml | 4 +- demo/Ursa.Demo/Pages/DatePickerDemo.axaml.cs | 4 +- src/Ursa.Themes.Semi/Controls/Calendar.axaml | 4 +- src/Ursa/Controls/DateTimePicker/Calendar.cs | 68 +++++- .../DateTimePicker/CalendarMonthView.cs | 221 ++++++++++++------ .../Controls/DateTimePicker/DatePicker.cs | 8 + .../DateTimePicker/DatePickerState.cs | 11 + 7 files changed, 233 insertions(+), 87 deletions(-) create mode 100644 src/Ursa/Controls/DateTimePicker/DatePicker.cs create mode 100644 src/Ursa/Controls/DateTimePicker/DatePickerState.cs diff --git a/demo/Ursa.Demo/Pages/DatePickerDemo.axaml b/demo/Ursa.Demo/Pages/DatePickerDemo.axaml index b3f4a56..44f1296 100644 --- a/demo/Ursa.Demo/Pages/DatePickerDemo.axaml +++ b/demo/Ursa.Demo/Pages/DatePickerDemo.axaml @@ -6,7 +6,7 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.DatePickerDemo"> - + @@ -17,7 +17,7 @@ - + diff --git a/demo/Ursa.Demo/Pages/DatePickerDemo.axaml.cs b/demo/Ursa.Demo/Pages/DatePickerDemo.axaml.cs index fa6e70c..2c6057c 100644 --- a/demo/Ursa.Demo/Pages/DatePickerDemo.axaml.cs +++ b/demo/Ursa.Demo/Pages/DatePickerDemo.axaml.cs @@ -1,6 +1,8 @@ -using Avalonia; +using System.Diagnostics; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Ursa.Controls; namespace Ursa.Demo.Pages; diff --git a/src/Ursa.Themes.Semi/Controls/Calendar.axaml b/src/Ursa.Themes.Semi/Controls/Calendar.axaml index 409efa3..ca83de8 100644 --- a/src/Ursa.Themes.Semi/Controls/Calendar.axaml +++ b/src/Ursa.Themes.Semi/Controls/Calendar.axaml @@ -124,7 +124,7 @@ - + - + diff --git a/src/Ursa/Controls/DateTimePicker/Calendar.cs b/src/Ursa/Controls/DateTimePicker/Calendar.cs index 70de6e7..e6d504a 100644 --- a/src/Ursa/Controls/DateTimePicker/Calendar.cs +++ b/src/Ursa/Controls/DateTimePicker/Calendar.cs @@ -26,7 +26,8 @@ public class Calendar: TemplatedControl public const string PART_MonthView = "PART_MonthView"; public const string PART_YearView = "PART_YearView"; - private Grid? _monthGrid; + private CalendarMonthView? _monthGrid; + private DatePickerState _state = DatePickerState.None; public static readonly StyledProperty SelectedDateProperty = AvaloniaProperty.Register(nameof(SelectedDate), DateTime.Now); @@ -71,17 +72,68 @@ public class Calendar: TemplatedControl set => SetValue(BlackoutDateRuleProperty, value); } + internal DateTime? StartDate; + internal DateTime? EndDate; + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); - _monthGrid = e.NameScope.Find(PART_MonthView); - } - - private void InitializeGrid() - { if (_monthGrid is not null) { - + _monthGrid.OnCalendarDayButtonPressed -= OnCalendarDayButtonPressed; + _monthGrid.OnCalendarDayButtonPointerEnter -= OnCalendarDayButtonPointerEnter; + } + _monthGrid = e.NameScope.Find(PART_MonthView); + if(_monthGrid is not null) + { + _monthGrid.OnCalendarDayButtonPressed += OnCalendarDayButtonPressed; + _monthGrid.OnCalendarDayButtonPointerEnter += OnCalendarDayButtonPointerEnter; } } -} \ No newline at end of file + + private void OnCalendarDayButtonPointerEnter(object sender, CalendarDayButtonEventArgs e) + { + if(_monthGrid is null) + { + return; + } + var date = e.Date; + if (_state is DatePickerState.None) return; + if (_state == DatePickerState.PreviewStart) + { + _monthGrid.MarkPreview(date, EndDate); + } + else if (_state == DatePickerState.PreviewEnd) + { + _monthGrid.MarkPreview(StartDate, date); + } + } + + private void OnCalendarDayButtonPressed(object sender, CalendarDayButtonEventArgs e) + { + if(_monthGrid is null) + { + return; + } + var date = e.Date; + if (_state == DatePickerState.None) + { + _monthGrid.ClearSelection(); + _monthGrid.MarkSelection(date, null); + _state = DatePickerState.PreviewEnd; + StartDate = date; + } + else if (_state == DatePickerState.PreviewStart) + { + _monthGrid.MarkSelection(date, EndDate); + _state = DatePickerState.SelectStart; + StartDate = date; + } + else if (_state == DatePickerState.PreviewEnd) + { + _monthGrid.MarkSelection(StartDate, date); + _state = DatePickerState.None; + EndDate = date; + } + } +} diff --git a/src/Ursa/Controls/DateTimePicker/CalendarMonthView.cs b/src/Ursa/Controls/DateTimePicker/CalendarMonthView.cs index aa9d07c..1e55785 100644 --- a/src/Ursa/Controls/DateTimePicker/CalendarMonthView.cs +++ b/src/Ursa/Controls/DateTimePicker/CalendarMonthView.cs @@ -4,23 +4,52 @@ using Avalonia.Controls; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Interactivity; using Avalonia.Layout; -using Irihi.Avalonia.Shared; namespace Ursa.Controls; /// -/// Show days in a month. +/// Show days in a month. /// [TemplatePart(PART_Grid, typeof(Grid))] -public class CalendarMonthView: TemplatedControl +public class CalendarMonthView : TemplatedControl { public const string PART_Grid = "PART_Grid"; - internal Calendar? Owner { get; set; } - - private Grid? _grid; + + public static readonly StyledProperty FirstDayOfWeekProperty = + AvaloniaProperty.Register( + nameof(FirstDayOfWeek)); + private readonly System.Globalization.Calendar _calendar = new GregorianCalendar(); + + private DateTime _contextDate = DateTime.Today; + + private Grid? _grid; + + static CalendarMonthView() + { + FirstDayOfWeekProperty.Changed.AddClassHandler((view, args) => + view.OnDayOfWeekChanged(args)); + } + + internal Calendar? Owner { get; set; } + + /// + /// The DateTime used to generate the month view. This date will be within the month. + /// + public DateTime ContextDate + { + get => _contextDate; + set => _contextDate = value; + // GenerateGridElements(); + } + + public DayOfWeek FirstDayOfWeek + { + get => GetValue(FirstDayOfWeekProperty); + set => SetValue(FirstDayOfWeekProperty, value); + } + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) { base.OnApplyTemplate(e); @@ -29,80 +58,50 @@ public class CalendarMonthView: TemplatedControl SetDayButtons(DateTime.Today); } - private DateTime _contextDate = DateTime.Today; - /// - /// The DateTime used to generate the month view. This date will be within the month. - /// - public DateTime ContextDate - { - get => _contextDate; - set - { - _contextDate = value; - // GenerateGridElements(); - } - } - - public static readonly StyledProperty FirstDayOfWeekProperty = AvaloniaProperty.Register( - nameof(FirstDayOfWeek)); - - public DayOfWeek FirstDayOfWeek - { - get => GetValue(FirstDayOfWeekProperty); - set => SetValue(FirstDayOfWeekProperty, value); - } - - static CalendarMonthView() - { - FirstDayOfWeekProperty.Changed.AddClassHandler((view, args) => view.OnDayOfWeekChanged(args)); - } - private void OnDayOfWeekChanged(AvaloniaPropertyChangedEventArgs args) { // throw new NotImplementedException(); } - + private void GenerateGridElements() { // Generate Day titles (Sun, Mon, Tue, Wed, Thu, Fri, Sat) based on FirstDayOfWeek and culture. - int count = 7 + 7 * 7; + var count = 7 + 7 * 7; var children = new List(count); - int dayOfWeek = (int)FirstDayOfWeek; + var dayOfWeek = (int)FirstDayOfWeek; var info = DateTimeHelper.GetCurrentDateTimeFormatInfo(); - for (int i = 0; i < 7; i++) + for (var i = 0; i < 7; i++) { - int d = ((dayOfWeek + i) % DateTimeHelper.NumberOfDaysPerWeek); - var cell = new TextBlock(){ Text = info.ShortestDayNames[d] }; + var d = (dayOfWeek + i) % DateTimeHelper.NumberOfDaysPerWeek; + var cell = new TextBlock { Text = info.ShortestDayNames[d] }; cell.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center); cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.ColumnProperty, i); children.Add(cell); } - + // Generate day buttons. - for (int i = 2; i < DateTimeHelper.NumberOfWeeksPerMonth+2; i++) + for (var i = 2; i < DateTimeHelper.NumberOfWeeksPerMonth + 2; i++) + for (var j = 0; j < DateTimeHelper.NumberOfDaysPerWeek; j++) { - for (int j = 0; j < DateTimeHelper.NumberOfDaysPerWeek; j++) - { - var cell = new CalendarDayButton(); - cell.SetValue(Grid.RowProperty, i); - cell.SetValue(Grid.ColumnProperty, j); - cell.PointerPressed += OnDayButtonPressed; - cell.PointerEntered += OnDayButtonPointerEnter; - children.Add(cell); - } + var cell = new CalendarDayButton(); + cell.SetValue(Grid.RowProperty, i); + cell.SetValue(Grid.ColumnProperty, j); + cell.PointerPressed += OnDayButtonPressed; + cell.PointerEntered += OnDayButtonPointerEnter; + children.Add(cell); } - + _grid?.Children.AddRange(children); } - + private void SetDayButtons(DateTime date) { if (_grid is null) return; var children = _grid.Children; var info = DateTimeHelper.GetCurrentDateTimeFormatInfo(); - int dayBefore = PreviousMonthDays(date); + var dayBefore = PreviousMonthDays(date); var dateToSet = date.GetFirstDayOfMonth().AddDays(-dayBefore); for (var i = 8; i < children.Count; i++) { @@ -114,75 +113,149 @@ public class CalendarMonthView: TemplatedControl cell.Content = day.Day.ToString(info); dateToSet = dateToSet.AddDays(1); } + FadeOutDayButtons(); } - + private void OnDayButtonPressed(object sender, PointerPressedEventArgs e) { if (sender is CalendarDayButton { DataContext: DateTime d }) - { OnCalendarDayButtonPressed?.Invoke(this, new CalendarDayButtonEventArgs(d)); - } } - + private void OnDayButtonPointerEnter(object sender, PointerEventArgs e) { - if(sender is CalendarDayButton {DataContext: DateTime d}) - { + if (sender is CalendarDayButton { DataContext: DateTime d }) OnCalendarDayButtonPointerEnter?.Invoke(this, new CalendarDayButtonEventArgs(d)); - } } - + private int PreviousMonthDays(DateTime date) { var firstDay = date.GetFirstDayOfMonth(); var dayOfWeek = _calendar.GetDayOfWeek(firstDay); - var firstDayOfWeek = this.FirstDayOfWeek; - int i = (dayOfWeek - firstDayOfWeek + DateTimeHelper.NumberOfDaysPerWeek) % DateTimeHelper.NumberOfDaysPerWeek; + var firstDayOfWeek = FirstDayOfWeek; + var i = (dayOfWeek - firstDayOfWeek + DateTimeHelper.NumberOfDaysPerWeek) % DateTimeHelper.NumberOfDaysPerWeek; return i == 0 ? DateTimeHelper.NumberOfDaysPerWeek : i; } - + /// - /// Make days out of current month fade out. These buttons are not disabled. They are just visually faded out. + /// Make days out of current month fade out. These buttons are not disabled. They are just visually faded out. /// private void FadeOutDayButtons() { if (_grid is null) return; var children = _grid.Children; for (var i = 8; i < children.Count; i++) - { if (children[i] is CalendarDayButton { DataContext: DateTime d } button && d.Month != _contextDate.Month) - { button.IsNotCurrentMonth = true; - } - } } public event EventHandler? OnCalendarDayButtonPressed; public event EventHandler? OnCalendarDayButtonPointerEnter; - public void MarkSelection(DateTime start, DateTime end) + public void MarkSelection(DateTime? start, DateTime? end) { - if(_grid?.Children is null) return; + if (_grid?.Children is null) return; foreach (var child in _grid.Children) - { if (child is CalendarDayButton { DataContext: DateTime d } button) { + if (d.Month != _contextDate.Month) continue; if (d == start) { button.IsStartDate = true; + button.IsEndDate = false; + button.IsInRange = false; } else if (d == end) { button.IsEndDate = true; + button.IsStartDate = false; + button.IsInRange = false; } else if (d > start && d < end) { button.IsInRange = true; + button.IsStartDate = false; + button.IsEndDate = false; } + else + { + button.IsStartDate = false; + button.IsEndDate = false; + button.IsInRange = false; + } + } + } + + public void MarkPreview(DateTime? start, DateTime? end) + { + if (_grid?.Children is null) return; + foreach (var child in _grid.Children) + { + if (child is not CalendarDayButton { DataContext: DateTime d } button) continue; + if (d == start) + { + button.IsPreviewStartDate = true; + button.IsPreviewEndDate = false; + button.IsInRange = false; + } + else if (d == end) + { + button.IsPreviewEndDate = true; + button.IsPreviewStartDate = false; + button.IsInRange = false; + } + else if (d > start && d < end) + { + button.IsInRange = true; + button.IsPreviewStartDate = false; + button.IsPreviewEndDate = false; + } + else + { + button.IsPreviewStartDate = false; + button.IsPreviewEndDate = false; + button.IsInRange = false; } } } + public void ClearSelection() + { + if (_grid?.Children is null) return; + foreach (var child in _grid.Children) + { + if (child is not CalendarDayButton button) continue; + button.IsStartDate = false; + button.IsEndDate = false; + button.IsInRange = false; + } + } + + public void ClearPreview() + { + if (_grid?.Children is null) return; + foreach (var child in _grid.Children) + { + if (child is not CalendarDayButton button) continue; + button.IsPreviewStartDate = false; + button.IsPreviewEndDate = false; + button.IsInRange = false; + } + } + + public void MarkSelection(DateTime date) + { + if (_grid?.Children is null) return; + foreach (var child in _grid.Children) + { + if (child is not CalendarDayButton { DataContext: DateTime d } button) continue; + button.IsStartDate = false; + button.IsEndDate = false; + button.IsInRange = false; + if (d.Month != _contextDate.Month) continue; + button.IsSelected = d == date; + } + } } \ No newline at end of file diff --git a/src/Ursa/Controls/DateTimePicker/DatePicker.cs b/src/Ursa/Controls/DateTimePicker/DatePicker.cs new file mode 100644 index 0000000..23d2eec --- /dev/null +++ b/src/Ursa/Controls/DateTimePicker/DatePicker.cs @@ -0,0 +1,8 @@ +using Avalonia.Controls.Primitives; + +namespace Ursa.Controls; + +public class DatePicker: TemplatedControl +{ + +} \ No newline at end of file diff --git a/src/Ursa/Controls/DateTimePicker/DatePickerState.cs b/src/Ursa/Controls/DateTimePicker/DatePickerState.cs new file mode 100644 index 0000000..fec187f --- /dev/null +++ b/src/Ursa/Controls/DateTimePicker/DatePickerState.cs @@ -0,0 +1,11 @@ +namespace Ursa.Controls; + +public enum DatePickerState +{ + None, + SelectSingle, + SelectStart, + SelectEnd, + PreviewStart, + PreviewEnd, +} \ No newline at end of file