feat: WIP.

This commit is contained in:
rabbitism
2024-06-18 00:36:55 +08:00
parent 1982f773ca
commit f75460a386
8 changed files with 165 additions and 92 deletions

View File

@@ -5,7 +5,7 @@
<Design.PreviewWith>
<StackPanel Margin="20" Spacing="5">
<u:CalendarMonthView />
<u:Calendar />
<u:CalendarDisplayControl />
</StackPanel>
</Design.PreviewWith>
<!-- Add Resources Here -->
@@ -120,15 +120,15 @@
</Style>
</ControlTheme>
<ControlTheme x:Key="{x:Type u:Calendar}" TargetType="u:Calendar">
<ControlTheme x:Key="{x:Type u:CalendarDisplayControl}" TargetType="u:CalendarDisplayControl">
<Setter Property="MinHeight" Value="300" />
<Setter Property="Template">
<ControlTemplate TargetType="u:Calendar">
<ControlTemplate TargetType="u:CalendarDisplayControl">
<Panel>
<Grid RowDefinitions="Auto, *">
<Grid Grid.Row="0" ColumnDefinitions="Auto, Auto,*, Auto, Auto">
<Button
Name="{x:Static u:Calendar.PART_PreviousYearButton}"
Name="{x:Static u:CalendarDisplayControl.PART_PreviousYearButton}"
Grid.Column="0"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
@@ -141,7 +141,7 @@
</Button>
<Button
Name="{x:Static u:Calendar.PART_PreviousButton}"
Name="{x:Static u:CalendarDisplayControl.PART_PreviousButton}"
Grid.Column="1"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
@@ -155,7 +155,7 @@
<Grid Grid.Column="2" ColumnDefinitions="*, *">
<Button
Name="{x:Static u:Calendar.PART_YearButton}"
Name="{x:Static u:CalendarDisplayControl.PART_YearButton}"
Grid.Column="0"
HorizontalContentAlignment="Center"
Content="2024"
@@ -163,7 +163,7 @@
IsVisible="{TemplateBinding IsMonthMode}"
Theme="{DynamicResource BorderlessButton}" />
<Button
Name="{x:Static u:Calendar.PART_MonthButton}"
Name="{x:Static u:CalendarDisplayControl.PART_MonthButton}"
Grid.Column="1"
HorizontalContentAlignment="Center"
Content="Apr"
@@ -171,7 +171,7 @@
IsVisible="{TemplateBinding IsMonthMode}"
Theme="{DynamicResource BorderlessButton}" />
<Button
Name="{x:Static u:Calendar.PART_HeaderButton}"
Name="{x:Static u:CalendarDisplayControl.PART_HeaderButton}"
Grid.Column="0"
Grid.ColumnSpan="2"
IsVisible="{TemplateBinding IsMonthMode, Converter={x:Static BoolConverters.Not}}"
@@ -181,7 +181,7 @@
</Grid>
<Button
Name="{x:Static u:Calendar.PART_NextButton}"
Name="{x:Static u:CalendarDisplayControl.PART_NextButton}"
Grid.Column="3"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
@@ -193,7 +193,7 @@
Foreground="{DynamicResource CalendarItemIconForeground}" />
</Button>
<Button
Name="{x:Static u:Calendar.PART_NextYearButton}"
Name="{x:Static u:CalendarDisplayControl.PART_NextYearButton}"
Grid.Column="4"
HorizontalContentAlignment="Left"
Foreground="{TemplateBinding Foreground}"
@@ -206,12 +206,12 @@
</Button>
</Grid>
<u:CalendarMonthView
Name="{x:Static u:Calendar.PART_MonthView}"
Name="{x:Static u:CalendarDisplayControl.PART_MonthView}"
Grid.Row="1"
VerticalAlignment="Top"
IsVisible="{TemplateBinding IsMonthMode}" />
<u:CalendarYearView
Name="{x:Static u:Calendar.PART_YearView}"
Name="{x:Static u:CalendarDisplayControl.PART_YearView}"
Grid.Row="1"
Width="{Binding #PART_MonthView.Bounds.Width}"
Height="{Binding #PART_MonthView.Bounds.Height}"

View File

@@ -1,19 +1,17 @@
using Avalonia;
using Avalonia.Controls;
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;
using Irihi.Avalonia.Shared.Helpers;
using Ursa.EventArgs;
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
[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";
@@ -23,10 +21,46 @@ public class CalendarDayButton: ContentControl
public const string PC_Today = ":today";
public const string PC_NotCurrentMonth = ":not-current-month";
public const string PC_Blackout = ":blackout";
internal Calendar? Owner { get; set; }
private static HashSet<string> _pseudoClasses =
[
PseudoClassName.PC_Selected, PC_EndDate, PC_PreviewStartDate,
PC_PreviewEndDate, PseudoClassName.PC_Selected, 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;
@@ -36,30 +70,27 @@ public class CalendarDayButton: ContentControl
PseudoClasses.Set(PC_Today, value);
}
}
private bool _isStartDate;
public bool IsStartDate
{
get => _isStartDate;
set
{
_isStartDate = value;
PseudoClasses.Set(PC_StartDate, value);
SetPseudoClass(PC_StartDate);
}
}
private bool _isEndDate;
public bool IsEndDate
{
get => _isEndDate;
set
{
_isEndDate = value;
PseudoClasses.Set(PC_EndDate, value);
SetPseudoClass(PC_EndDate);
}
}
private bool _isPreviewStartDate;
public bool IsPreviewStartDate
{
get => _isPreviewStartDate;
@@ -69,8 +100,7 @@ public class CalendarDayButton: ContentControl
PseudoClasses.Set(PC_PreviewStartDate, value);
}
}
private bool _isPreviewEndDate;
public bool IsPreviewEndDate
{
get => _isPreviewEndDate;
@@ -80,8 +110,7 @@ public class CalendarDayButton: ContentControl
PseudoClasses.Set(PC_PreviewEndDate, value);
}
}
private bool _isInRange;
public bool IsInRange
{
get => _isInRange;
@@ -92,7 +121,6 @@ public class CalendarDayButton: ContentControl
}
}
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
@@ -102,10 +130,9 @@ public class CalendarDayButton: ContentControl
PseudoClasses.Set(PseudoClassName.PC_Selected, value);
}
}
private bool _isBlackout;
/// <summary>
/// Notice: IsBlackout is not equivalent to not IsEnabled. Blackout dates still react to pointerover actions.
/// Notice: IsBlackout is not equivalent to not IsEnabled. Blackout dates still react to pointerover actions.
/// </summary>
public bool IsBlackout
{
@@ -116,10 +143,10 @@ public class CalendarDayButton: ContentControl
PseudoClasses.Set(PC_Blackout, value);
}
}
private bool _isNotCurrentMonth;
/// <summary>
/// Notice: IsNotCurrentMonth is not equivalent to not IsEnabled. Not current month dates still react to pointerover and press action.
/// Notice: IsNotCurrentMonth is not equivalent to not IsEnabled. Not current month dates still react to pointerover
/// and press action.
/// </summary>
public bool IsNotCurrentMonth
{
@@ -130,33 +157,23 @@ public class CalendarDayButton: ContentControl
PseudoClasses.Set(PC_NotCurrentMonth, value);
}
}
public static readonly RoutedEvent<CalendarDayButtonEventArgs> DateSelectedEvent = RoutedEvent.Register<CalendarDayButton, CalendarDayButtonEventArgs>(
nameof(DateSelected), RoutingStrategies.Bubble);
public event EventHandler<CalendarDayButtonEventArgs> DateSelected
{
add => AddHandler(DateSelectedEvent, value);
remove => RemoveHandler(DateSelectedEvent, value);
}
public static readonly RoutedEvent<CalendarDayButtonEventArgs> DatePreviewedEvent = RoutedEvent.Register<CalendarDayButton, CalendarDayButtonEventArgs>(
nameof(DatePreviewed), RoutingStrategies.Bubble);
public event EventHandler<CalendarDayButtonEventArgs> DatePreviewed
{
add => AddHandler(DateSelectedEvent, value);
remove => RemoveHandler(DateSelectedEvent, value);
}
static CalendarDayButton()
{
PressedMixin.Attach<CalendarDayButton>();
remove => RemoveHandler(DateSelectedEvent, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
/*
PseudoClasses.Set(PC_Today, IsToday);
PseudoClasses.Set(PC_StartDate, IsStartDate);
PseudoClasses.Set(PC_EndDate, IsEndDate);
@@ -164,23 +181,40 @@ public class CalendarDayButton: ContentControl
PseudoClasses.Set(PC_PreviewEndDate, IsPreviewEndDate);
PseudoClasses.Set(PC_InRange, IsInRange);
PseudoClasses.Set(PseudoClassName.PC_Selected, IsSelected);
*/
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (this.DataContext is DateTime d)
{
if (DataContext is DateTime d)
RaiseEvent(new CalendarDayButtonEventArgs(d) { RoutedEvent = DateSelectedEvent, Source = this });
}
}
protected override void OnPointerEntered(PointerEventArgs e)
{
base.OnPointerEntered(e);
if (this.DataContext is DateTime d)
{
if (DataContext is DateTime d)
RaiseEvent(new CalendarDayButtonEventArgs(d) { RoutedEvent = DateSelectedEvent, Source = this });
}
internal void ResetSelection()
{
foreach (var pc in _pseudoClasses)
{
PseudoClasses.Set(pc, false);
}
}
private void SetPseudoClass(string s)
{
if (_pseudoClasses.Contains(s))
{
foreach (var pc in _pseudoClasses)
{
PseudoClasses.Set(pc, false);
}
}
PseudoClasses.Set(s, true);
}
}

View File

@@ -17,7 +17,7 @@ namespace Ursa.Controls;
[TemplatePart(PART_HeaderButton, typeof(Button))]
[TemplatePart(PART_MonthView, typeof(CalendarMonthView))]
[TemplatePart(PART_YearView, typeof(CalendarYearView))]
public class Calendar: TemplatedControl
public class CalendarDisplayControl: TemplatedControl
{
public const string PART_NextYearButton = "PART_NextYearButton";
public const string PART_PreviousYearButton = "PART_PreviousYearButton";
@@ -37,7 +37,7 @@ public class Calendar: TemplatedControl
private Button? _headerButton;
public static readonly StyledProperty<DateTime> SelectedDateProperty = AvaloniaProperty.Register<Calendar, DateTime>(nameof(SelectedDate), DateTime.Now);
public static readonly StyledProperty<DateTime> SelectedDateProperty = AvaloniaProperty.Register<CalendarDisplayControl, DateTime>(nameof(SelectedDate), DateTime.Now);
public DateTime SelectedDate
{
get => GetValue(SelectedDateProperty);
@@ -45,7 +45,7 @@ public class Calendar: TemplatedControl
}
public static readonly StyledProperty<DayOfWeek> FirstDayOfWeekProperty =
AvaloniaProperty.Register<Calendar, DayOfWeek>(nameof(FirstDayOfWeek),
AvaloniaProperty.Register<CalendarDisplayControl, DayOfWeek>(nameof(FirstDayOfWeek),
defaultValue: DateTimeHelper.GetCurrentDateTimeFormatInfo().FirstDayOfWeek);
public DayOfWeek FirstDayOfWeek
{
@@ -53,7 +53,7 @@ public class Calendar: TemplatedControl
set => SetValue(FirstDayOfWeekProperty, value);
}
public static readonly StyledProperty<bool> IsTodayHighlightedProperty = AvaloniaProperty.Register<Calendar, bool>(nameof(IsTodayHighlighted), true);
public static readonly StyledProperty<bool> IsTodayHighlightedProperty = AvaloniaProperty.Register<CalendarDisplayControl, bool>(nameof(IsTodayHighlighted), true);
public bool IsTodayHighlighted
{
get => GetValue(IsTodayHighlightedProperty);
@@ -61,7 +61,7 @@ public class Calendar: TemplatedControl
}
public static readonly StyledProperty<AvaloniaList<DateRange>?> BlackoutDatesProperty =
AvaloniaProperty.Register<Calendar, AvaloniaList<DateRange>?>(
AvaloniaProperty.Register<CalendarDisplayControl, AvaloniaList<DateRange>?>(
nameof(BlackoutDates));
public AvaloniaList<DateRange>? BlackoutDates
@@ -70,7 +70,7 @@ public class Calendar: TemplatedControl
set => SetValue(BlackoutDatesProperty, value);
}
public static readonly StyledProperty<IDateSelector?> BlackoutDateRuleProperty = AvaloniaProperty.Register<Calendar, IDateSelector?>(
public static readonly StyledProperty<IDateSelector?> BlackoutDateRuleProperty = AvaloniaProperty.Register<CalendarDisplayControl, IDateSelector?>(
nameof(BlackoutDateRule));
public IDateSelector? BlackoutDateRule
@@ -81,7 +81,7 @@ public class Calendar: TemplatedControl
private bool _isMonthMode = true;
public static readonly DirectProperty<Calendar, bool> IsMonthModeProperty = AvaloniaProperty.RegisterDirect<Calendar, bool>(
public static readonly DirectProperty<CalendarDisplayControl, bool> IsMonthModeProperty = AvaloniaProperty.RegisterDirect<CalendarDisplayControl, bool>(
nameof(IsMonthMode), o => o.IsMonthMode, (o, v) => o.IsMonthMode = v);
public bool IsMonthMode
@@ -153,14 +153,14 @@ public class Calendar: TemplatedControl
{
SetCurrentValue(IsMonthModeProperty, false);
if (_yearView is null) return;
_headerButton?.SetValue(Button.ContentProperty, _yearView.ContextDate.Year);
_headerButton?.SetValue(ContentControl.ContentProperty, _yearView.ContextDate.Year);
_yearView?.UpdateMode(CalendarYearViewMode.Month);
}
private void OnYearButtonClick(object sender, RoutedEventArgs e)
{
if (_yearView is null) return;
_headerButton?.SetValue(Button.ContentProperty,
_headerButton?.SetValue(ContentControl.ContentProperty,
_yearView?.ContextDate.Year + "-" + (_yearView?.ContextDate.Year + 10));
_yearView?.UpdateMode(CalendarYearViewMode.Year);
SetCurrentValue(IsMonthModeProperty, false);

View File

@@ -9,7 +9,8 @@ using Avalonia.Layout;
namespace Ursa.Controls;
/// <summary>
/// Show days in a month.
/// Show days in a month. CalendarMonthView itself doesn't handle any date range selection logic.
/// it provides a method to mark preview range and selection range. The range limit may out of current displayed month.
/// </summary>
[TemplatePart(PART_Grid, typeof(Grid))]
public class CalendarMonthView : TemplatedControl
@@ -32,7 +33,7 @@ public class CalendarMonthView : TemplatedControl
view.OnDayOfWeekChanged(args));
}
internal Calendar? Owner { get; set; }
internal CalendarDisplayControl? Owner { get; set; }
/// <summary>
/// The DateTime used to generate the month view. This date will be within the month.
@@ -156,6 +157,30 @@ public class CalendarMonthView : TemplatedControl
public event EventHandler<CalendarDayButtonEventArgs>? OnDateSelected;
public event EventHandler<CalendarDayButtonEventArgs>? OnDatePreviewed;
public void MarkDates(DateTime? startDate = null, DateTime? endDate = null, DateTime? previewStartDate = null, DateTime? previewEndDate = null)
{
if (_grid?.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 _grid.Children)
{
if (child is not CalendarDayButton { DataContext: DateTime d } button) continue;
if(d.Month != _contextDate.Month) continue;
button.ResetSelection();
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;
}
}
[Obsolete]
public void MarkSelection(DateTime? start, DateTime? end)
{
if (_grid?.Children is null) return;

View File

@@ -12,39 +12,43 @@ internal enum CalendarYearViewMode
{
Month,
Year,
// The button represents 10 years.
YearRange,
}
YearRange
}
[PseudoClasses(PC_Range, PseudoClassName.PC_Selected)]
public class CalendarYearButton: ContentControl
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 int Year { get; private set; }
internal int Month { get; private set; }
internal int StartYear { get; private set; }
internal int EndYear { get; private set; }
internal CalendarYearViewMode Mode { get; private set; }
public static readonly RoutedEvent<CalendarYearButtonEventArgs> ItemSelectedEvent = RoutedEvent.Register<CalendarYearButton, CalendarYearButtonEventArgs>(
nameof(ItemSelected), RoutingStrategies.Bubble);
public event EventHandler<CalendarDayButtonEventArgs> ItemSelected
{
add => AddHandler(ItemSelectedEvent, value);
remove => RemoveHandler(ItemSelectedEvent, value);
}
internal void SetValues(CalendarYearViewMode mode, DateTime contextDate, int? month = null, int? year = null, int? startYear = null, int? endYear = null)
internal void SetValues(CalendarYearViewMode mode, DateTime contextDate, int? month = null, int? year = null,
int? startYear = null, int? endYear = null)
{
Debug.Assert(!(month is null && year is null && startYear is null && endYear is null));
Mode = mode;
@@ -66,6 +70,5 @@ public class CalendarYearButton: ContentControl
base.OnPointerPressed(e);
RaiseEvent(new CalendarYearButtonEventArgs(Mode, Year, Month, StartYear, EndYear)
{ RoutedEvent = ItemSelectedEvent, Source = this });
}
}

View File

@@ -4,12 +4,14 @@ namespace Ursa.Controls;
public class CalendarYearButtonEventArgs: RoutedEventArgs
{
internal int? Year { get; }
internal int? Month { get; }
internal int? StartYear { get; }
internal int? EndYear { get; }
public int? Year { get; }
public int? Month { get; }
public int? StartYear { get; }
public int? EndYear { get; }
internal CalendarYearViewMode Mode { get; }
internal CalendarYearButtonEventArgs( CalendarYearViewMode mode, int? year, int? month, int? startYear, int? endYear )
/// <inheritdoc />
internal CalendarYearButtonEventArgs(CalendarYearViewMode mode, int? year, int? month, int? startYear, int? endYear )
{
Year = year;
Month = month;

View File

@@ -32,5 +32,14 @@ internal static class DateTimeHelper
{
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;
}
}