feat: add a single state machine to manage selection. WIP.

This commit is contained in:
rabbitism
2024-05-12 16:59:45 +08:00
parent e18efdb1e2
commit 2235292681
7 changed files with 233 additions and 87 deletions

View File

@@ -6,7 +6,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.DatePickerDemo"> x:Class="Ursa.Demo.Pages.DatePickerDemo">
<StackPanel Margin="20" HorizontalAlignment="Left"> <StackPanel Margin="20" HorizontalAlignment="Left">
<u:CalendarMonthView/> <u:CalendarMonthView />
<u:CalendarDayButton /> <u:CalendarDayButton />
<u:CalendarDayButton IsSelected="True" /> <u:CalendarDayButton IsSelected="True" />
<u:CalendarDayButton IsBlackout="True" /> <u:CalendarDayButton IsBlackout="True" />
@@ -17,7 +17,7 @@
<u:CalendarDayButton IsInRange="True" /> <u:CalendarDayButton IsInRange="True" />
<u:CalendarDayButton IsEndDate="True" /> <u:CalendarDayButton IsEndDate="True" />
</StackPanel> </StackPanel>
<StackPanel Orientation="Horizontal"> <StackPanel Orientation="Horizontal" DataValidationErrors.Errors="{Binding items}">
<u:CalendarDayButton IsPreviewStartDate="True" /> <u:CalendarDayButton IsPreviewStartDate="True" />
<u:CalendarDayButton IsInRange="True" /> <u:CalendarDayButton IsInRange="True" />
<u:CalendarDayButton IsInRange="True" /> <u:CalendarDayButton IsInRange="True" />

View File

@@ -1,6 +1,8 @@
using Avalonia; using System.Diagnostics;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Ursa.Controls;
namespace Ursa.Demo.Pages; namespace Ursa.Demo.Pages;

View File

@@ -124,7 +124,7 @@
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate TargetType="u:Calendar"> <ControlTemplate TargetType="u:Calendar">
<Panel> <Panel>
<Grid Name="{x:Static u:Calendar.PART_MonthView}" RowDefinitions="Auto, *"> <Grid RowDefinitions="Auto, *">
<Grid Grid.Row="0" ColumnDefinitions="Auto, Auto,*, Auto, Auto"> <Grid Grid.Row="0" ColumnDefinitions="Auto, Auto,*, Auto, Auto">
<Button <Button
Name="{x:Static u:Calendar.PART_PreviousYearButton}" Name="{x:Static u:Calendar.PART_PreviousYearButton}"
@@ -185,7 +185,7 @@
Foreground="{DynamicResource CalendarItemIconForeground}" /> Foreground="{DynamicResource CalendarItemIconForeground}" />
</Button> </Button>
</Grid> </Grid>
<u:CalendarMonthView Grid.Row="1"></u:CalendarMonthView> <u:CalendarMonthView Grid.Row="1" Name="{x:Static u:Calendar.PART_MonthView}"></u:CalendarMonthView>
</Grid> </Grid>
</Panel> </Panel>

View File

@@ -26,7 +26,8 @@ public class Calendar: TemplatedControl
public const string PART_MonthView = "PART_MonthView"; public const string PART_MonthView = "PART_MonthView";
public const string PART_YearView = "PART_YearView"; public const string PART_YearView = "PART_YearView";
private Grid? _monthGrid; private CalendarMonthView? _monthGrid;
private DatePickerState _state = DatePickerState.None;
public static readonly StyledProperty<DateTime> SelectedDateProperty = AvaloniaProperty.Register<Calendar, DateTime>(nameof(SelectedDate), DateTime.Now); public static readonly StyledProperty<DateTime> SelectedDateProperty = AvaloniaProperty.Register<Calendar, DateTime>(nameof(SelectedDate), DateTime.Now);
@@ -71,17 +72,68 @@ public class Calendar: TemplatedControl
set => SetValue(BlackoutDateRuleProperty, value); set => SetValue(BlackoutDateRuleProperty, value);
} }
internal DateTime? StartDate;
internal DateTime? EndDate;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
_monthGrid = e.NameScope.Find<Grid>(PART_MonthView);
}
private void InitializeGrid()
{
if (_monthGrid is not null) if (_monthGrid is not null)
{ {
_monthGrid.OnCalendarDayButtonPressed -= OnCalendarDayButtonPressed;
_monthGrid.OnCalendarDayButtonPointerEnter -= OnCalendarDayButtonPointerEnter;
}
_monthGrid = e.NameScope.Find<CalendarMonthView>(PART_MonthView);
if(_monthGrid is not null)
{
_monthGrid.OnCalendarDayButtonPressed += OnCalendarDayButtonPressed;
_monthGrid.OnCalendarDayButtonPointerEnter += OnCalendarDayButtonPointerEnter;
} }
} }
}
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;
}
}
}

View File

@@ -4,23 +4,52 @@ using Avalonia.Controls;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout; using Avalonia.Layout;
using Irihi.Avalonia.Shared;
namespace Ursa.Controls; namespace Ursa.Controls;
/// <summary> /// <summary>
/// Show days in a month. /// Show days in a month.
/// </summary> /// </summary>
[TemplatePart(PART_Grid, typeof(Grid))] [TemplatePart(PART_Grid, typeof(Grid))]
public class CalendarMonthView: TemplatedControl public class CalendarMonthView : TemplatedControl
{ {
public const string PART_Grid = "PART_Grid"; public const string PART_Grid = "PART_Grid";
internal Calendar? Owner { get; set; }
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
private Grid? _grid; AvaloniaProperty.Register<CalendarMonthView, DayOfWeek>(
nameof(FirstDayOfWeek));
private readonly System.Globalization.Calendar _calendar = new GregorianCalendar(); private readonly System.Globalization.Calendar _calendar = new GregorianCalendar();
private DateTime _contextDate = DateTime.Today;
private Grid? _grid;
static CalendarMonthView()
{
FirstDayOfWeekProperty.Changed.AddClassHandler<CalendarMonthView, DayOfWeek>((view, args) =>
view.OnDayOfWeekChanged(args));
}
internal Calendar? Owner { get; set; }
/// <summary>
/// The DateTime used to generate the month view. This date will be within the month.
/// </summary>
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) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
@@ -29,80 +58,50 @@ public class CalendarMonthView: TemplatedControl
SetDayButtons(DateTime.Today); SetDayButtons(DateTime.Today);
} }
private DateTime _contextDate = DateTime.Today;
/// <summary>
/// The DateTime used to generate the month view. This date will be within the month.
/// </summary>
public DateTime ContextDate
{
get => _contextDate;
set
{
_contextDate = value;
// GenerateGridElements();
}
}
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty = AvaloniaProperty.Register<CalendarMonthView, DayOfWeek>(
nameof(FirstDayOfWeek));
public DayOfWeek FirstDayOfWeek
{
get => GetValue(FirstDayOfWeekProperty);
set => SetValue(FirstDayOfWeekProperty, value);
}
static CalendarMonthView()
{
FirstDayOfWeekProperty.Changed.AddClassHandler<CalendarMonthView, DayOfWeek>((view, args) => view.OnDayOfWeekChanged(args));
}
private void OnDayOfWeekChanged(AvaloniaPropertyChangedEventArgs<DayOfWeek> args) private void OnDayOfWeekChanged(AvaloniaPropertyChangedEventArgs<DayOfWeek> args)
{ {
// throw new NotImplementedException(); // throw new NotImplementedException();
} }
private void GenerateGridElements() private void GenerateGridElements()
{ {
// Generate Day titles (Sun, Mon, Tue, Wed, Thu, Fri, Sat) based on FirstDayOfWeek and culture. // 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<Control>(count); var children = new List<Control>(count);
int dayOfWeek = (int)FirstDayOfWeek; var dayOfWeek = (int)FirstDayOfWeek;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo(); 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 d = (dayOfWeek + i) % DateTimeHelper.NumberOfDaysPerWeek;
var cell = new TextBlock(){ Text = info.ShortestDayNames[d] }; var cell = new TextBlock { Text = info.ShortestDayNames[d] };
cell.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center); cell.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Center);
cell.SetValue(Grid.RowProperty, 0); cell.SetValue(Grid.RowProperty, 0);
cell.SetValue(Grid.ColumnProperty, i); cell.SetValue(Grid.ColumnProperty, i);
children.Add(cell); children.Add(cell);
} }
// Generate day buttons. // 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);
var cell = new CalendarDayButton(); cell.SetValue(Grid.ColumnProperty, j);
cell.SetValue(Grid.RowProperty, i); cell.PointerPressed += OnDayButtonPressed;
cell.SetValue(Grid.ColumnProperty, j); cell.PointerEntered += OnDayButtonPointerEnter;
cell.PointerPressed += OnDayButtonPressed; children.Add(cell);
cell.PointerEntered += OnDayButtonPointerEnter;
children.Add(cell);
}
} }
_grid?.Children.AddRange(children); _grid?.Children.AddRange(children);
} }
private void SetDayButtons(DateTime date) private void SetDayButtons(DateTime date)
{ {
if (_grid is null) return; if (_grid is null) return;
var children = _grid.Children; var children = _grid.Children;
var info = DateTimeHelper.GetCurrentDateTimeFormatInfo(); var info = DateTimeHelper.GetCurrentDateTimeFormatInfo();
int dayBefore = PreviousMonthDays(date); var dayBefore = PreviousMonthDays(date);
var dateToSet = date.GetFirstDayOfMonth().AddDays(-dayBefore); var dateToSet = date.GetFirstDayOfMonth().AddDays(-dayBefore);
for (var i = 8; i < children.Count; i++) for (var i = 8; i < children.Count; i++)
{ {
@@ -114,75 +113,149 @@ public class CalendarMonthView: TemplatedControl
cell.Content = day.Day.ToString(info); cell.Content = day.Day.ToString(info);
dateToSet = dateToSet.AddDays(1); dateToSet = dateToSet.AddDays(1);
} }
FadeOutDayButtons(); FadeOutDayButtons();
} }
private void OnDayButtonPressed(object sender, PointerPressedEventArgs e) private void OnDayButtonPressed(object sender, PointerPressedEventArgs e)
{ {
if (sender is CalendarDayButton { DataContext: DateTime d }) if (sender is CalendarDayButton { DataContext: DateTime d })
{
OnCalendarDayButtonPressed?.Invoke(this, new CalendarDayButtonEventArgs(d)); OnCalendarDayButtonPressed?.Invoke(this, new CalendarDayButtonEventArgs(d));
}
} }
private void OnDayButtonPointerEnter(object sender, PointerEventArgs e) 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)); OnCalendarDayButtonPointerEnter?.Invoke(this, new CalendarDayButtonEventArgs(d));
}
} }
private int PreviousMonthDays(DateTime date) private int PreviousMonthDays(DateTime date)
{ {
var firstDay = date.GetFirstDayOfMonth(); var firstDay = date.GetFirstDayOfMonth();
var dayOfWeek = _calendar.GetDayOfWeek(firstDay); var dayOfWeek = _calendar.GetDayOfWeek(firstDay);
var firstDayOfWeek = this.FirstDayOfWeek; var firstDayOfWeek = FirstDayOfWeek;
int i = (dayOfWeek - firstDayOfWeek + DateTimeHelper.NumberOfDaysPerWeek) % DateTimeHelper.NumberOfDaysPerWeek; var i = (dayOfWeek - firstDayOfWeek + DateTimeHelper.NumberOfDaysPerWeek) % DateTimeHelper.NumberOfDaysPerWeek;
return i == 0 ? DateTimeHelper.NumberOfDaysPerWeek : i; return i == 0 ? DateTimeHelper.NumberOfDaysPerWeek : i;
} }
/// <summary> /// <summary>
/// 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.
/// </summary> /// </summary>
private void FadeOutDayButtons() private void FadeOutDayButtons()
{ {
if (_grid is null) return; if (_grid is null) return;
var children = _grid.Children; var children = _grid.Children;
for (var i = 8; i < children.Count; i++) for (var i = 8; i < children.Count; i++)
{
if (children[i] is CalendarDayButton { DataContext: DateTime d } button && d.Month != _contextDate.Month) if (children[i] is CalendarDayButton { DataContext: DateTime d } button && d.Month != _contextDate.Month)
{
button.IsNotCurrentMonth = true; button.IsNotCurrentMonth = true;
}
}
} }
public event EventHandler<CalendarDayButtonEventArgs>? OnCalendarDayButtonPressed; public event EventHandler<CalendarDayButtonEventArgs>? OnCalendarDayButtonPressed;
public event EventHandler<CalendarDayButtonEventArgs>? OnCalendarDayButtonPointerEnter; public event EventHandler<CalendarDayButtonEventArgs>? 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) foreach (var child in _grid.Children)
{
if (child is CalendarDayButton { DataContext: DateTime d } button) if (child is CalendarDayButton { DataContext: DateTime d } button)
{ {
if (d.Month != _contextDate.Month) continue;
if (d == start) if (d == start)
{ {
button.IsStartDate = true; button.IsStartDate = true;
button.IsEndDate = false;
button.IsInRange = false;
} }
else if (d == end) else if (d == end)
{ {
button.IsEndDate = true; button.IsEndDate = true;
button.IsStartDate = false;
button.IsInRange = false;
} }
else if (d > start && d < end) else if (d > start && d < end)
{ {
button.IsInRange = true; 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;
}
}
} }

View File

@@ -0,0 +1,8 @@
using Avalonia.Controls.Primitives;
namespace Ursa.Controls;
public class DatePicker: TemplatedControl
{
}

View File

@@ -0,0 +1,11 @@
namespace Ursa.Controls;
public enum DatePickerState
{
None,
SelectSingle,
SelectStart,
SelectEnd,
PreviewStart,
PreviewEnd,
}