Merge pull request #575 from irihitech/commitinput

Date/Time picker tests and cleanup.
This commit is contained in:
Dong Bin
2025-02-18 22:15:42 +08:00
committed by GitHub
26 changed files with 1780 additions and 135 deletions

View File

@@ -8,6 +8,10 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Ursa.Demo.Pages.DatePickerDemo">
<StackPanel Margin="20" HorizontalAlignment="Left">
<u:Form>
<u:DatePicker Width="200" u:FormItem.Label="_Test"/>
<TextBox Width="200" u:FormItem.Label="_West"/>
</u:Form>
<u:CalendarView DateSelected="CalendarView_OnOnDateSelected" DatePreviewed="CalendarView_OnOnDatePreviewed"/>
<TextBlock Text="{Binding #singlePicker.SelectedDate}" ></TextBlock>
<u:DatePicker Name="singlePicker" Width="200" Classes="ClearButton" />

View File

@@ -1,5 +1,8 @@
using System.Runtime.CompilerServices;
using Avalonia.Metadata;
[assembly:InternalsVisibleTo("HeadlessTest.Ursa")]
[assembly:InternalsVisibleTo("Test.Ursa")]
[assembly:XmlnsPrefix("https://irihi.tech/ursa", "u")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa")]
[assembly:XmlnsDefinition("https://irihi.tech/ursa", "Ursa.Controls")]

View File

@@ -1,6 +1,6 @@
namespace Ursa.Controls;
public sealed class CalendarContext(int? year = null, int? month = null, int? startYear = null, int? endYear = null): IComparable<CalendarContext>
internal 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;

View File

@@ -95,13 +95,13 @@ public class CalendarView : TemplatedControl
private CalendarContext _contextDate = new();
public static readonly DirectProperty<CalendarView, CalendarContext> ContextDateProperty = AvaloniaProperty.RegisterDirect<CalendarView, CalendarContext>(
internal static readonly DirectProperty<CalendarView, CalendarContext> ContextDateProperty = AvaloniaProperty.RegisterDirect<CalendarView, CalendarContext>(
nameof(ContextDate), o => o.ContextDate, (o, v) => o.ContextDate = v);
public CalendarContext ContextDate
internal CalendarContext ContextDate
{
get => _contextDate;
internal set => SetAndRaise(ContextDateProperty, ref _contextDate, value);
set => SetAndRaise(ContextDateProperty, ref _contextDate, value);
}
public bool IsTodayHighlighted
@@ -535,7 +535,7 @@ public class CalendarView : TemplatedControl
IsEnabledProperty.SetValue(canNext, _nextButton, _fastNextButton);
}
public void MarkDates(DateTime? startDate = null, DateTime? endDate = null, DateTime? previewStartDate = null, DateTime? previewEndDate = null)
internal void MarkDates(DateTime? startDate = null, DateTime? endDate = null, DateTime? previewStartDate = null, DateTime? previewEndDate = null)
{
_start = startDate;
_end = endDate;

View File

@@ -25,7 +25,7 @@ public class CalendarYearButton : ContentControl
internal CalendarViewMode Mode { get; private set; }
public event EventHandler<CalendarDayButtonEventArgs> ItemSelected
public event EventHandler<CalendarYearButtonEventArgs> ItemSelected
{
add => AddHandler(ItemSelectedEvent, value);
remove => RemoveHandler(ItemSelectedEvent, value);

View File

@@ -7,8 +7,6 @@ using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Irihi.Avalonia.Shared.Common;
using Irihi.Avalonia.Shared.Contracts;
using Irihi.Avalonia.Shared.Helpers;
@@ -49,6 +47,7 @@ public class DatePicker: DatePickerBase, IClearControl
static DatePicker()
{
FocusableProperty.OverrideDefaultValue<DatePicker>(true);
SelectedDateProperty.Changed.AddClassHandler<DatePicker, DateTime?>((picker, args) =>
picker.OnSelectionChanged(args));
}
@@ -64,7 +63,6 @@ public class DatePicker: DatePickerBase, IClearControl
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _textBox);
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _textBox);
Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
CalendarView.DateSelectedEvent.RemoveHandler(OnDateSelected, _calendar);
@@ -73,10 +71,9 @@ public class DatePicker: DatePickerBase, IClearControl
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
_calendar = e.NameScope.Find<CalendarView>(PART_Calendar);
Button.ClickEvent.AddHandler(OnButtonClick, RoutingStrategies.Bubble, true, _button);
Button.ClickEvent.AddHandler(OnButtonClick, RoutingStrategies.Bubble, false, _button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
CalendarView.DateSelectedEvent.AddHandler(OnDateSelected, RoutingStrategies.Bubble, true, _calendar);
SyncSelectedDateToText(SelectedDate);
}
@@ -89,19 +86,10 @@ public class DatePicker: DatePickerBase, IClearControl
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)
if(IsFocused)
{
var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = new CalendarContext(date.Year, date.Month);
_calendar.UpdateDayButtons();
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
@@ -186,6 +174,7 @@ public class DatePicker: DatePickerBase, IClearControl
e.Handled = true;
}
}
}
protected override void OnKeyDown(KeyEventArgs e)
@@ -213,4 +202,40 @@ public class DatePicker: DatePickerBase, IClearControl
{
SetCurrentValue(SelectedDateProperty, null);
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
FocusChanged(IsKeyboardFocusWithin);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
FocusChanged(IsKeyboardFocusWithin);
var top = TopLevel.GetTopLevel(this);
var element = top?.FocusManager?.GetFocusedElement();
if (element is Visual v && _popup?.IsInsidePopup(v)==true)
{
return;
}
SetCurrentValue(IsDropdownOpenProperty, false);
}
private bool _isFocused;
private void FocusChanged(bool hasFocus)
{
bool wasFocused = _isFocused;
_isFocused = hasFocus;
if (hasFocus)
{
if (!wasFocused && _textBox != null)
{
_textBox.Focus();
_textBox.SelectAll();
}
}
}
}

View File

@@ -46,7 +46,7 @@ internal static class DateTimeHelper
public static (int start, int end) GetDecadeViewRangeByYear(int year)
{
int start = year / 10 * 10;
return new ValueTuple<int, int>(start, start + 10);
return new ValueTuple<int, int>(start, start + 10 - 1);
}
public static (int start, int end) GetCenturyViewRangeByYear(int year)

View File

@@ -15,7 +15,7 @@ namespace Ursa.Controls;
[TemplatePart(PART_Popup, typeof(Popup))]
[TemplatePart(PART_TextBox, typeof(TextBox))]
[TemplatePart(PART_Calendar, typeof(CalendarView))]
[TemplatePart(PART_TimePicker, typeof(TimePicker))]
[TemplatePart(PART_TimePicker, typeof(TimePickerPresenter))]
public class DateTimePicker : DatePickerBase
{
public const string PART_Button = "PART_Button";
@@ -76,6 +76,7 @@ public class DateTimePicker : DatePickerBase
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<DateTime?> args)
{
if (_fromText) return;
SyncSelectedDateToText(args.NewValue.Value);
}
@@ -85,14 +86,14 @@ public class DateTimePicker : DatePickerBase
{
_textBox?.SetValue(TextBox.TextProperty, null);
_calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
_timePickerPresenter?.SyncTime(null);
}
else
{
_textBox?.SetValue(TextBox.TextProperty,
date.Value.ToString(DisplayFormat ?? CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern));
_calendar?.MarkDates(date.Value.Date, date.Value.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.Value.TimeOfDay);
_timePickerPresenter?.SyncTime(date.Value.TimeOfDay);
}
}
@@ -175,12 +176,14 @@ public class DateTimePicker : DatePickerBase
var date = SelectedDate ?? DateTime.Now;
_calendar.ContextDate = new CalendarContext(date.Year, date.Month);
_calendar.UpdateDayButtons();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, SelectedDate?.TimeOfDay);
_timePickerPresenter?.SyncTime(SelectedDate?.TimeOfDay);
}
SetCurrentValue(IsDropdownOpenProperty, true);
}
private bool _fromText = false;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private void OnTextChanged(object? sender, TextChangedEventArgs e)
{
@@ -189,11 +192,13 @@ public class DateTimePicker : DatePickerBase
private void SetSelectedDate(bool fromText = false)
{
var temp = _fromText;
_fromText = fromText;
if (string.IsNullOrEmpty(_textBox?.Text))
{
SetCurrentValue(SelectedDateProperty, null);
_calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
_timePickerPresenter?.SyncTime(null);
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
@@ -201,7 +206,7 @@ public class DateTimePicker : DatePickerBase
{
SetCurrentValue(SelectedDateProperty, defaultTime);
_calendar?.MarkDates(defaultTime.Date, defaultTime.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, defaultTime.TimeOfDay);
_timePickerPresenter?.SyncTime(defaultTime.TimeOfDay);
}
}
else
@@ -217,16 +222,17 @@ public class DateTimePicker : DatePickerBase
}
_calendar?.MarkDates(date.Date, date.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.TimeOfDay);
_timePickerPresenter?.SyncTime(date.TimeOfDay);
}
else
{
SetCurrentValue(SelectedDateProperty, null);
if (!fromText) _textBox?.SetValue(TextBox.TextProperty, null);
_calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
_timePickerPresenter?.SyncTime(null);
}
}
_fromText = temp;
}
private void OnTextBoxGetFocus(object? sender, GotFocusEventArgs e)
@@ -236,7 +242,7 @@ public class DateTimePicker : DatePickerBase
var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = _calendar.ContextDate.With(date.Year, date.Month);
_calendar.UpdateDayButtons();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.TimeOfDay);
_timePickerPresenter?.SyncTime(date.TimeOfDay);
}
SetCurrentValue(IsDropdownOpenProperty, true);

View File

@@ -29,24 +29,22 @@ public class TimePicker : TimePickerBase, IClearControl
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<TimePicker, string?>(
nameof(Watermark));
private bool _suppressTextPresenterEvent;
private Button? _button;
private TimePickerPresenter? _presenter;
private TextBox? _textBox;
private bool _isFocused;
private Popup? _popup;
private TimePickerPresenter? _presenter;
private bool _suppressTextPresenterEvent;
private TextBox? _textBox;
static TimePicker()
{
FocusableProperty.OverrideDefaultValue<TimePicker>(true);
SelectedTimeProperty.Changed.AddClassHandler<TimePicker, TimeSpan?>((picker, args) =>
picker.OnSelectionChanged(args));
DisplayFormatProperty.Changed.AddClassHandler<TimePicker, string?>((picker, args) => picker.OnDisplayFormatChanged(args));
}
private void OnDisplayFormatChanged(AvaloniaPropertyChangedEventArgs<string?> _)
{
if (_textBox is null) return;
SyncTimeToText(SelectedTime);
DisplayFormatProperty.Changed.AddClassHandler<TimePicker, string?>((picker, args) =>
picker.OnDisplayFormatChanged(args));
}
public string? Watermark
@@ -63,58 +61,66 @@ public class TimePicker : TimePickerBase, IClearControl
public void Clear()
{
SetCurrentValue(SelectedTimeProperty, null);
Focus(NavigationMethod.Pointer);
_presenter?.SetValue(TimePickerPresenter.TimeProperty, null);
}
private void OnDisplayFormatChanged(AvaloniaPropertyChangedEventArgs<string?> _)
{
if (_textBox is null) return;
SyncTimeToText(SelectedTime);
}
/// <inheritdoc />
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
if (!e.Handled && e.Source is Visual source)
if (_popup?.IsInsidePopup(source) == true)
e.Handled = true;
}
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);
TimePickerPresenter.SelectedTimeChangedEvent.RemoveHandler(OnPresenterTimeChanged, _presenter);
_textBox = e.NameScope.Find<TextBox>(PART_TextBox);
e.NameScope.Find<Popup>(PartNames.PART_Popup);
_popup = e.NameScope.Find<Popup>(PartNames.PART_Popup);
_presenter = e.NameScope.Find<TimePickerPresenter>(PART_Presenter);
_button = e.NameScope.Find<Button>(PART_Button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
Button.ClickEvent.AddHandler(OnButtonClick, _button);
TimePickerPresenter.SelectedTimeChangedEvent.AddHandler(OnPresenterTimeChanged, _presenter);
// SetCurrentValue(SelectedTimeProperty, DateTime.Now.TimeOfDay);
_presenter?.SetValue(TimePickerPresenter.TimeProperty, SelectedTime);
_presenter?.SyncTime(SelectedTime);
SyncTimeToText(SelectedTime);
}
private void OnPresenterTimeChanged(object? sender, TimeChangedEventArgs e)
{
if (!IsInitialized) return;
if (_suppressTextPresenterEvent) return;
SetCurrentValue(SelectedTimeProperty, e.NewTime);
}
private void OnButtonClick(object? sender, RoutedEventArgs e)
{
Focus(NavigationMethod.Pointer);
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
private void OnTextBoxPointerPressed(object? sender, PointerPressedEventArgs e)
{
SetCurrentValue(IsDropdownOpenProperty, true);
if (IsFocused) SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
private void OnTextBoxGetFocus(object? sender, GotFocusEventArgs e)
{
// SetCurrentValue(IsDropdownOpenProperty, true);
SetCurrentValue(IsDropdownOpenProperty, true);
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (e.Key == Key.Escape)
@@ -123,20 +129,17 @@ public class TimePicker : TimePickerBase, IClearControl
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);
}
@@ -145,17 +148,17 @@ public class TimePicker : TimePickerBase, IClearControl
{
if (string.IsNullOrEmpty(_textBox?.Text))
{
TimePickerPresenter.TimeProperty.SetValue(null, _presenter);
_presenter?.SyncTime(null);
}
else if (DisplayFormat is null || DisplayFormat.Length == 0)
{
if (TimeSpan.TryParse(_textBox?.Text, out var defaultTime))
TimePickerPresenter.TimeProperty.SetValue(defaultTime, _presenter);
if (TimeSpan.TryParse(_textBox?.Text, out var defaultTime)) _presenter?.SyncTime(defaultTime);
}
else
{
if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None,
out var time)) TimePickerPresenter.TimeProperty.SetValue(time.TimeOfDay, _presenter);
out var time))
_presenter?.SyncTime(time.TimeOfDay);
}
}
@@ -163,7 +166,7 @@ public class TimePicker : TimePickerBase, IClearControl
{
if (_textBox is null) return;
_suppressTextPresenterEvent = true;
_presenter?.SetValue(TimePickerPresenter.TimeProperty, args.NewValue.Value);
_presenter?.SyncTime(args.NewValue.Value);
SyncTimeToText(args.NewValue.Value);
_suppressTextPresenterEvent = false;
}
@@ -176,6 +179,7 @@ public class TimePicker : TimePickerBase, IClearControl
_textBox.Text = null;
return;
}
var date = new DateTime(1, 1, 1, time.Value.Hours, time.Value.Minutes, time.Value.Seconds);
var text = date.ToString(DisplayFormat);
_textBox.Text = text;
@@ -199,4 +203,31 @@ public class TimePicker : TimePickerBase, IClearControl
base.UpdateDataValidation(property, state, error);
if (property == SelectedTimeProperty) DataValidationErrors.SetError(this, error);
}
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
// SetCurrentValue(IsDropdownOpenProperty, true);
FocusChanged(IsKeyboardFocusWithin);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
FocusChanged(IsKeyboardFocusWithin);
var top = TopLevel.GetTopLevel(this);
var element = top?.FocusManager?.GetFocusedElement();
if (element is Visual v && _popup?.IsInsidePopup(v) == true) return;
if (element == _textBox) return;
SetCurrentValue(IsDropdownOpenProperty, false);
}
private void FocusChanged(bool hasFocus)
{
var wasFocused = _isFocused;
_isFocused = hasFocus;
if (hasFocus)
if (!wasFocused && _textBox != null)
_textBox.Focus();
}
}

View File

@@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Helpers;
@@ -43,20 +44,20 @@ public class TimePickerPresenter : TemplatedControl
public static readonly StyledProperty<int> MinuteIncrementProperty =
AvaloniaProperty.Register<TimePickerPresenter, int>(
nameof(MinuteIncrement));
nameof(MinuteIncrement), 1);
public static readonly StyledProperty<int> SecondIncrementProperty =
AvaloniaProperty.Register<TimePickerPresenter, int>(
nameof(SecondIncrement));
public static readonly StyledProperty<TimeSpan?> TimeProperty =
AvaloniaProperty.Register<TimePickerPresenter, TimeSpan?>(
nameof(Time));
nameof(SecondIncrement), 1);
public static readonly StyledProperty<string> PanelFormatProperty =
AvaloniaProperty.Register<TimePickerPresenter, string>(
nameof(PanelFormat), "HH mm ss t");
public static readonly RoutedEvent<TimeChangedEventArgs> SelectedTimeChangedEvent =
RoutedEvent.Register<TimePickerPresenter, TimeChangedEventArgs>(
nameof(SelectedTimeChanged), RoutingStrategies.Bubble);
private Control? _ampmScrollPanel;
private DateTimePickerPanel? _ampmSelector;
private Control? _firstSeparator;
@@ -65,26 +66,21 @@ public class TimePickerPresenter : TemplatedControl
private DateTimePickerPanel? _hourSelector;
private Control? _minuteScrollPanel;
private DateTimePickerPanel? _minuteSelector;
private Grid? _pickerContainer;
private Control? _secondScrollPanel;
private DateTimePickerPanel? _secondSelector;
private Control? _secondSeparator;
private bool _surpressTimeEvent = true;
private Control? _thirdSeparator;
internal TimeSpan TimeHolder;
private bool _updateFromTimeChange;
private bool _use12Clock;
internal TimeSpan? TimeHolder;
static TimePickerPresenter()
{
PanelFormatProperty.Changed.AddClassHandler<TimePickerPresenter, string>((presenter, args) =>
presenter.OnPanelFormatChanged(args));
TimeProperty.Changed.AddClassHandler<TimePickerPresenter, TimeSpan?>((presenter, args) =>
presenter.OnTimeChanged(args));
}
public TimePickerPresenter()
{
// SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay);
}
public bool NeedsConfirmation
@@ -105,40 +101,18 @@ public class TimePickerPresenter : TemplatedControl
set => SetValue(SecondIncrementProperty, value);
}
public TimeSpan? Time
{
get => GetValue(TimeProperty);
set => SetValue(TimeProperty, value);
}
public string PanelFormat
{
get => GetValue(PanelFormatProperty);
set => SetValue(PanelFormatProperty, value);
}
public static readonly RoutedEvent<TimeChangedEventArgs> SelectedTimeChangedEvent =
RoutedEvent.Register<TimePickerPresenter, TimeChangedEventArgs>(
nameof(SelectedTimeChanged), RoutingStrategies.Bubble);
public event EventHandler<TimeChangedEventArgs> SelectedTimeChanged
{
add => AddHandler(SelectedTimeChangedEvent, value);
remove => RemoveHandler(SelectedTimeChangedEvent, value);
}
private void OnTimeChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args)
{
_updateFromTimeChange = true;
UpdatePanelsFromSelectedTime(args.NewValue.Value);
_updateFromTimeChange = false;
if (args.OldValue.Value != args.NewValue.Value)
{
RaiseEvent(new TimeChangedEventArgs(args.OldValue.Value, args.NewValue.Value)
{ RoutedEvent = SelectedTimeChangedEvent, Source = this });
}
}
private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs<string> args)
{
var format = args.NewValue.Value;
@@ -180,6 +154,7 @@ public class TimePickerPresenter : TemplatedControl
{
panels.Add(_ampmScrollPanel);
_ampmSelector?.SetValue(DateTimePickerPanel.ItemFormatProperty, part);
if (_ampmSelector is not null) _ampmSelector.IsEnabled = _use12Clock;
}
}
catch
@@ -191,6 +166,7 @@ public class TimePickerPresenter : TemplatedControl
if (panels.Count < 1) return;
IsVisibleProperty.SetValue(false, _hourScrollPanel, _minuteScrollPanel, _secondScrollPanel, _ampmScrollPanel,
_firstSeparator, _secondSeparator, _thirdSeparator);
Grid.ColumnProperty.SetValue(0, _hourScrollPanel, _minuteScrollPanel, _secondScrollPanel, _ampmScrollPanel);
for (var i = 0; i < panels.Count; i++)
{
var panel = panels[i];
@@ -211,47 +187,63 @@ public class TimePickerPresenter : TemplatedControl
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
if (_hourSelector is not null) _hourSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_minuteSelector is not null) _minuteSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_secondSelector is not null) _secondSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_ampmSelector is not null) _ampmSelector.SelectionChanged -= OnPanelSelectionChanged;
_hourSelector = e.NameScope.Find<DateTimePickerPanel>(PART_HourSelector);
_minuteSelector = e.NameScope.Find<DateTimePickerPanel>(PART_MinuteSelector);
_secondSelector = e.NameScope.Find<DateTimePickerPanel>(PART_SecondSelector);
_ampmSelector = e.NameScope.Find<DateTimePickerPanel>(PART_AmPmSelector);
if (_hourSelector is not null) _hourSelector.SelectionChanged += OnPanelSelectionChanged;
if (_minuteSelector is not null) _minuteSelector.SelectionChanged += OnPanelSelectionChanged;
if (_secondSelector is not null) _secondSelector.SelectionChanged += OnPanelSelectionChanged;
if (_ampmSelector is not null) _ampmSelector.SelectionChanged += OnPanelSelectionChanged;
_pickerContainer = e.NameScope.Find<Grid>(PART_PickerContainer);
_hourScrollPanel = e.NameScope.Find<Control>(PART_HourScrollPanel);
_minuteScrollPanel = e.NameScope.Find<Control>(PART_MinuteScrollPanel);
_secondScrollPanel = e.NameScope.Find<Control>(PART_SecondScrollPanel);
_ampmScrollPanel = e.NameScope.Find<Control>(PART_AmPmScrollPanel);
_firstSeparator = e.NameScope.Find<Control>(PART_FirstSeparator);
_secondSeparator = e.NameScope.Find<Control>(PART_SecondSeparator);
_thirdSeparator = e.NameScope.Find<Control>(PART_ThirdSeparator);
Initialize();
UpdatePanelLayout(PanelFormat);
UpdatePanelsFromSelectedTime(Time);
_surpressTimeEvent = false;
}
protected override void OnLoaded(RoutedEventArgs e)
{
base.OnLoaded(e);
UpdatePanelsFromSelectedTime(TimeHolder);
if (_hourSelector is not null) _hourSelector.SelectionChanged += OnPanelSelectionChanged;
if (_minuteSelector is not null) _minuteSelector.SelectionChanged += OnPanelSelectionChanged;
if (_secondSelector is not null) _secondSelector.SelectionChanged += OnPanelSelectionChanged;
if (_ampmSelector is not null) _ampmSelector.SelectionChanged += OnPanelSelectionChanged;
}
protected override void OnUnloaded(RoutedEventArgs e)
{
base.OnUnloaded(e);
if (_hourSelector is not null) _hourSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_minuteSelector is not null) _minuteSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_secondSelector is not null) _secondSelector.SelectionChanged -= OnPanelSelectionChanged;
if (_ampmSelector is not null) _ampmSelector.SelectionChanged -= OnPanelSelectionChanged;
}
private void OnPanelSelectionChanged(object? sender, System.EventArgs e)
{
if (_updateFromTimeChange) return;
if (_surpressTimeEvent) return;
if (!_use12Clock && Equals(sender, _ampmSelector)) return;
var time = NeedsConfirmation ? TimeHolder : Time ?? DateTime.Now.TimeOfDay;
var time = TimeHolder ?? DateTime.Now.TimeOfDay;
var hour = _hourSelector?.SelectedValue ?? time.Hours;
var minute = _minuteSelector?.SelectedValue ?? time.Minutes;
var second = _secondSelector?.SelectedValue ?? time.Seconds;
var ampm = _ampmSelector?.SelectedValue ?? (time.Hours >= 12 ? 1 : 0);
if (_use12Clock)
{
hour = ampm switch
{
0 when hour == 12 => 0,
1 when hour < 12 => hour + 12,
_ => hour
};
}
else
{
ampm = hour switch
@@ -261,11 +253,23 @@ public class TimePickerPresenter : TemplatedControl
};
SetIfChanged(_ampmSelector, ampm);
}
var newTime = new TimeSpan(hour, minute, second);
if (NeedsConfirmation)
{
TimeHolder = newTime;
}
else
SetCurrentValue(TimeProperty, newTime);
{
if (_surpressTimeEvent) return;
RaiseEvent(new TimeChangedEventArgs(null, newTime) { RoutedEvent = SelectedTimeChangedEvent });
}
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
OnPanelSelectionChanged(null, System.EventArgs.Empty);
}
private void UpdatePanelsFromSelectedTime(TimeSpan? time)
@@ -275,21 +279,19 @@ public class TimePickerPresenter : TemplatedControl
{
var index = _use12Clock ? time.Value.Hours % 12 : time.Value.Hours;
if (_use12Clock && index == 0) index = 12;
SetIfChanged(_hourSelector, index);
SetIfChanged(_hourSelector, index, true);
}
SetIfChanged(_minuteSelector, time.Value.Minutes);
SetIfChanged(_secondSelector, time.Value.Seconds);
SetIfChanged(_minuteSelector, time.Value.Minutes, true);
SetIfChanged(_secondSelector, time.Value.Seconds, true);
var ampm = time.Value.Hours switch
{
>= 12 => 1,
_ => 0
};
SetIfChanged(_ampmSelector, ampm);
if (_ampmSelector is not null)
{
_ampmSelector.IsEnabled = _use12Clock;
}
SetIfChanged(_ampmSelector, ampm, true);
if (_ampmSelector is not null) _ampmSelector.IsEnabled = _use12Clock;
}
private void Initialize()
@@ -326,12 +328,23 @@ public class TimePickerPresenter : TemplatedControl
public void Confirm()
{
if (NeedsConfirmation) SetCurrentValue(TimeProperty, TimeHolder);
if (NeedsConfirmation)
RaiseEvent(new TimeChangedEventArgs(null, TimeHolder) { RoutedEvent = SelectedTimeChangedEvent });
}
private void SetIfChanged(DateTimePickerPanel? panel, int index)
private void SetIfChanged(DateTimePickerPanel? panel, int index, bool surpress = false)
{
if (panel is null) return;
panel.SelectionChanged -= OnPanelSelectionChanged;
if (panel.SelectedValue != index) panel.SelectedValue = index;
panel.SelectionChanged += OnPanelSelectionChanged;
}
internal void SyncTime(TimeSpan? time)
{
_surpressTimeEvent = true;
TimeHolder = time;
UpdatePanelsFromSelectedTime(time);
_surpressTimeEvent = false;
}
}

View File

@@ -100,15 +100,16 @@ public class TimeRangePicker : TimePickerBase, IClearControl
public void Clear()
{
Focus(NavigationMethod.Pointer);
_startPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
_endPresenter?.SetValue(TimePickerPresenter.TimeProperty, null);
_startPresenter?.SyncTime(null);
_endPresenter?.SyncTime(null);
}
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args, bool start = true)
{
SyncTimeToText(args.NewValue.Value, start);
_suppressTextPresenterEvent = true;
TimePickerPresenter.TimeProperty.SetValue(args.NewValue.Value, start ? _startPresenter : _endPresenter);
var presenter = start ? _startPresenter : _endPresenter;
presenter?.SyncTime(args.NewValue.Value);
_suppressTextPresenterEvent = false;
}
@@ -150,8 +151,8 @@ public class TimeRangePicker : TimePickerBase, IClearControl
Button.ClickEvent.AddHandler(OnButtonClick, _button);
TimePickerPresenter.SelectedTimeChangedEvent.AddHandler(OnPresenterTimeChanged, _startPresenter, _endPresenter);
_startPresenter?.SetValue(TimePickerPresenter.TimeProperty, StartTime);
_endPresenter?.SetValue(TimePickerPresenter.TimeProperty, EndTime);
_startPresenter?.SyncTime(StartTime);
_endPresenter?.SyncTime(EndTime);
SyncTimeToText(StartTime);
SyncTimeToText(EndTime, false);
}

View File

@@ -0,0 +1,73 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Avalonia.Threading;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.DateTimePicker;
public class CalendarDayButtonTests
{
[AvaloniaFact]
public void OnPointerReleased_RaisesDateSelectedEvent()
{
Window window = new Window();
var button = new CalendarDayButton();
var date = new DateTime(2023, 5, 15);
button.DataContext = date;
window.Content = button;
window.Show();
Dispatcher.UIThread.RunJobs();
int eventRaised = 0;
DateTime? eventContext = null;
void OnMouseClick(object? sender, CalendarDayButtonEventArgs args)
{
eventRaised++;
eventContext = args.Date;
}
button.DateSelected += OnMouseClick;
window.MouseUp(new Point(10, 10), MouseButton.Left);
Assert.Equal(1, eventRaised);
Assert.Equal(date, eventContext);
button.DateSelected -= OnMouseClick;
eventContext = null;
window.MouseUp(new Point(10, 10), MouseButton.Left);
Assert.Null(eventContext);
Assert.Equal(1, eventRaised);
}
[AvaloniaFact]
public void OnPointerEntered_RaisesDatePreviewedEvent()
{
Window window = new Window();
var button = new CalendarDayButton();
var date = new DateTime(2023, 5, 15);
button.DataContext = date;
window.Content = button;
window.Show();
Dispatcher.UIThread.RunJobs();
int eventRaised = 0;
DateTime? eventContext = null;
void OnMouseEnter(object? sender, CalendarDayButtonEventArgs args)
{
eventRaised++;
eventContext = args.Date;
}
button.DatePreviewed += OnMouseEnter;
window.MouseMove(new Point(10, 10));
Assert.Equal(1, eventRaised);
Assert.Equal(date, eventContext);
window.MouseMove(new Point(100, 100));
button.DatePreviewed -= OnMouseEnter;
eventContext = null;
window.MouseMove(new Point(10, 10));
Assert.Null(eventContext);
Assert.Equal(1, eventRaised);
}
}

View File

@@ -0,0 +1,54 @@
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Interactivity;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.DateTimePicker;
internal static class CalendarViewHelper
{
internal static void ClickPrevious(this CalendarView calendarView)
{
var previousButton = calendarView.GetTemplateChildren()
.FirstOrDefault(a => a.Name == CalendarView.PART_PreviousButton);
Assert.IsType<Button>(previousButton);
var button = (Button)previousButton;
button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
internal static void ClickNext(this CalendarView calendarView)
{
var nextButton = calendarView.GetTemplateChildren()
.FirstOrDefault(a => a.Name == CalendarView.PART_NextButton);
Assert.IsType<Button>(nextButton);
var button = (Button)nextButton;
button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
internal static void ClickFastNext(this CalendarView calendarView)
{
var nextButton = calendarView.GetTemplateChildren()
.FirstOrDefault(a => a.Name == CalendarView.PART_FastNextButton);
Assert.IsType<Button>(nextButton);
var button = (Button)nextButton;
button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
internal static void ClickFastPrevious(this CalendarView calendarView)
{
var previousButton = calendarView.GetTemplateChildren()
.FirstOrDefault(a => a.Name == CalendarView.PART_FastPreviousButton);
Assert.IsType<Button>(previousButton);
var button = (Button)previousButton;
button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
internal static void ClickHeaderButton(this CalendarView calendarView)
{
var headerButton = calendarView.GetTemplateChildren()
.FirstOrDefault(a => a.Name == CalendarView.PART_HeaderButton);
Assert.IsType<Button>(headerButton);
var button = (Button)headerButton;
button.RaiseEvent(new RoutedEventArgs(Button.ClickEvent));
}
}

View File

@@ -0,0 +1,278 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.DateTimePicker;
public class CalendarViewTests
{
[AvaloniaFact]
public void OnNext_MonthMode_UpdatesToNextMonth()
{
var window = new Window();
var calendarView = new CalendarView
{ Mode = CalendarViewMode.Month };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
calendarView.ClickNext();
Assert.Equal(6, calendarView.ContextDate.Month);
Assert.Equal(2023, calendarView.ContextDate.Year);
}
[AvaloniaFact]
public void OnNext_MonthMode_FastForward_UpdatesToNextYear()
{
var window = new Window();
var calendarView = new CalendarView
{ Mode = CalendarViewMode.Month };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
calendarView.ClickFastNext();
Assert.Equal(5, calendarView.ContextDate.Month);
Assert.Equal(2024, calendarView.ContextDate.Year);
}
[AvaloniaFact]
public void OnNext_YearMode_UpdatesToNextYear()
{
var window = new Window();
var calendarView = new CalendarView
{ Mode = CalendarViewMode.Year };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
calendarView.ClickNext();
Assert.Equal(5, calendarView.ContextDate.Month);
Assert.Equal(2024, calendarView.ContextDate.Year);
}
[AvaloniaFact]
public void OnNext_DecadeMode_UpdatesToNextDecade()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Decade };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(null, null, 2000, 2009));
Dispatcher.UIThread.RunJobs();
calendarView.ClickNext();
Assert.Equal(2010, calendarView.ContextDate.StartYear);
Assert.Equal(2019, calendarView.ContextDate.EndYear);
}
[AvaloniaFact]
public void OnNext_CenturyMode_UpdatesToNextCentury()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Century };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(null, null, 1900, 1999));
Dispatcher.UIThread.RunJobs();
calendarView.ClickNext();
Assert.Equal(2000, calendarView.ContextDate.StartYear);
Assert.Equal(2099, calendarView.ContextDate.EndYear);
}
[AvaloniaFact]
public void OnPrevious_MonthMode_UpdatesToPreviousMonth()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Month };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
calendarView.ClickPrevious();
Assert.Equal(4, calendarView.ContextDate.Month);
Assert.Equal(2023, calendarView.ContextDate.Year);
}
[AvaloniaFact]
public void OnPrevious_YearMode_UpdatesToPreviousYear()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Year };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
calendarView.ClickPrevious();
Assert.Equal(2022, calendarView.ContextDate.Year);
}
[AvaloniaFact]
public void OnPrevious_DecadeMode_UpdatesToPreviousDecade()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Decade };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(null, null, 2000, 2009));
Dispatcher.UIThread.RunJobs();
calendarView.ClickPrevious();
Assert.Equal(1990, calendarView.ContextDate.StartYear);
Assert.Equal(1999, calendarView.ContextDate.EndYear);
}
[AvaloniaFact]
public void OnPrevious_CenturyMode_UpdatesToPreviousCentury()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Century };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(null, null, 1900, 1999));
Dispatcher.UIThread.RunJobs();
calendarView.ClickPrevious();
Assert.Equal(1800, calendarView.ContextDate.StartYear);
Assert.Equal(1899, calendarView.ContextDate.EndYear);
}
[AvaloniaFact]
public void OnFastPrevious_MonthMode_UpdatesToPreviousYear()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Month };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
calendarView.ClickFastPrevious();
Assert.Equal(2022, calendarView.ContextDate.Year);
}
[AvaloniaFact]
public void OnHeaderButtonClick_YearMode_SwitchesToDecadeMode()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Year };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
calendarView.ClickHeaderButton();
Assert.Equal(CalendarViewMode.Decade, calendarView.Mode);
Assert.Equal(2020, calendarView.ContextDate.StartYear);
Assert.Equal(2029, calendarView.ContextDate.EndYear);
}
[AvaloniaFact]
public void OnHeaderButtonClick_DecadeMode_SwitchesToCenturyMode()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Decade };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(null, null, 2000, 2009));
Dispatcher.UIThread.RunJobs();
calendarView.ClickHeaderButton();
Assert.Equal(CalendarViewMode.Century, calendarView.Mode);
}
[AvaloniaFact]
public void OnYearItemSelected_CenturyMode_SwitchesToDecadeMode()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Century };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(null, null, 1900, 1999));
Dispatcher.UIThread.RunJobs();
var yearButton = calendarView.FindDescendantOfType<CalendarYearButton>();
var position = yearButton?.TranslatePoint(new Point(6, 6), window);
Assert.NotNull(position);
window.MouseUp(position.Value, MouseButton.Left);
Assert.Equal(CalendarViewMode.Decade, calendarView.Mode);
}
[AvaloniaFact]
public void OnYearItemSelected_DecadeMode_SwitchesToYearMode()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Decade };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(null, null, 2000, 2009));
Dispatcher.UIThread.RunJobs();
var yearButton = calendarView.FindDescendantOfType<CalendarYearButton>();
var position = yearButton?.TranslatePoint(new Point(6, 6), window);
Assert.NotNull(position);
window.MouseUp(position.Value, MouseButton.Left);
Assert.Equal(CalendarViewMode.Year, calendarView.Mode);
}
[AvaloniaFact]
public void OnYearItemSelected_YearMode_SwitchesToMonthMode()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Year };
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
var yearButton = calendarView.FindDescendantOfType<CalendarYearButton>();
var position = yearButton?.TranslatePoint(new Point(6, 6), window);
Assert.NotNull(position);
window.MouseUp(position.Value, MouseButton.Left);
Assert.Equal(CalendarViewMode.Month, calendarView.Mode);
}
[AvaloniaFact]
public void Update_FirstDayOfWeek_UpdatesCalendar()
{
var window = new Window();
var calendarView = new CalendarView { Mode = CalendarViewMode.Month, FirstDayOfWeek = DayOfWeek.Sunday};
window.Content = calendarView;
window.Show();
Dispatcher.UIThread.RunJobs();
calendarView.SyncContextDate(new CalendarContext(2023, 5));
Dispatcher.UIThread.RunJobs();
var monthGrid = calendarView.GetTemplateChildren()
.FirstOrDefault(a => a is Grid && a.Name == CalendarView.PART_MonthGrid) as Grid;
Assert.NotNull(monthGrid);
var dayHeader = monthGrid.Children.OfType<TextBlock>().FirstOrDefault();
var dayButton = monthGrid.Children.OfType<CalendarDayButton>().FirstOrDefault();
Assert.NotNull(dayHeader);
Assert.NotNull(dayButton);
Assert.Equal('S', dayHeader.Text?[0]);
Assert.Equal(30, (dayButton.DataContext as DateTime?)?.Day);
calendarView.FirstDayOfWeek = DayOfWeek.Tuesday;
dayHeader = monthGrid.Children.OfType<TextBlock>().FirstOrDefault();
dayButton = monthGrid.Children.OfType<CalendarDayButton>().FirstOrDefault();
Assert.NotNull(dayHeader);
Assert.NotNull(dayButton);
Assert.Equal('T', dayHeader.Text?[0]);
Assert.Equal(25, (dayButton.DataContext as DateTime?)?.Day);
}
}

View File

@@ -0,0 +1,96 @@
using System.Globalization;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Ursa.Controls;
namespace HeadlessTest.Ursa.Controls.DateTimePicker;
public class CalendarYearButtonTests
{
[AvaloniaFact]
public void SetContext_YearMode_SetsContentToAbbreviatedMonthName()
{
var button = new CalendarYearButton();
var context = new CalendarContext(null, 5);
button.SetContext(CalendarViewMode.Year, context);
Assert.Equal("May", button.Content);
}
[AvaloniaFact]
public void SetContext_YearMode_SetsContentToAbbreviatedMonthName_Chinese()
{
CultureInfo.CurrentCulture = new CultureInfo("zh-CN");
var button = new CalendarYearButton();
var context = new CalendarContext(null, 5);
button.SetContext(CalendarViewMode.Year, context);
Assert.Equal("5月", button.Content);
}
[AvaloniaFact]
public void SetContext_DecadeMode_SetsContentToYear()
{
var button = new CalendarYearButton();
var context = new CalendarContext(2023);
button.SetContext(CalendarViewMode.Decade, context);
Assert.Equal("2023", button.Content);
}
[AvaloniaFact]
public void SetContext_CenturyMode_SetsContentToYearRange()
{
var button = new CalendarYearButton();
var context = new CalendarContext(null, null, 1900, 1999);
button.SetContext(CalendarViewMode.Century, context);
Assert.Equal("1900-1999", button.Content);
}
[AvaloniaFact]
public void SetContext_InvalidYear_DisablesButton()
{
var button = new CalendarYearButton();
var context = new CalendarContext(null, null, -1);
button.SetContext(CalendarViewMode.Decade, context);
Assert.False(button.IsEnabled);
}
[AvaloniaFact]
public void SetContext_Month_Mode_DisablesButton()
{
var button = new CalendarYearButton();
var context = new CalendarContext(1, 1);
button.SetContext(CalendarViewMode.Month, context);
Assert.False(button.IsEnabled);
}
[AvaloniaFact]
public void OnPointerReleased_RaisesItemSelectedEvent()
{
var button = new CalendarYearButton();
var context = new CalendarContext(2023, 5);
button.SetContext(CalendarViewMode.Year, context);
int eventRaised = 0;
CalendarContext? eventContext = null;
void OnItemSelected( object? sender, CalendarYearButtonEventArgs args)
{
eventRaised++;
eventContext = args.Context;
}
button.ItemSelected += OnItemSelected;
Window window = new Window();
window.Content = button;
window.Show();
window.MouseUp(new Point(10, 10), MouseButton.Left);
Assert.Equal(1, eventRaised);
Assert.Equal(context, eventContext);
button.ItemSelected -= OnItemSelected;
eventContext = null;
window.MouseUp(new Point(10, 10), MouseButton.Left);
Assert.Null(eventContext);
Assert.Equal(1, eventRaised);
}
}

View File

@@ -0,0 +1,355 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Threading;
using Avalonia.VisualTree;
using HeadlessTest.Ursa.TestHelpers;
using Ursa.Controls;
using DatePicker = Ursa.Controls.DatePicker;
namespace HeadlessTest.Ursa.Controls.DateTimePicker;
public class DatePickerTests
{
[AvaloniaFact]
public void Click_Opens_Popup()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Assert.False(picker.IsDropdownOpen);
Dispatcher.UIThread.RunJobs();
window.MouseDown(new Point(10, 10), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
window.MouseDown(new Point(10, 10), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
}
[AvaloniaFact]
public void Click_Button_Toggles_Popup()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
var button = picker.GetTemplateChildOfType<Button>(DatePicker.PART_Button);
var position = button?.TranslatePoint(new Point(5, 5), window);
Assert.NotNull(position);
Assert.False(picker.IsDropdownOpen);
Dispatcher.UIThread.RunJobs();
window.MouseDown(position.Value, MouseButton.Left);
window.MouseUp(position.Value, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
window.MouseDown(position.Value, MouseButton.Left);
window.MouseUp(position.Value, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.False(picker.IsDropdownOpen);
}
[AvaloniaFact]
public void Clear_Set_SelectedDate_To_Null()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
picker.SelectedDate = DateTime.Now;
Dispatcher.UIThread.RunJobs();
Assert.NotNull(picker.SelectedDate);
picker.Clear();
Dispatcher.UIThread.RunJobs();
Assert.Null(picker.SelectedDate);
}
[AvaloniaFact]
public void Press_Escape_Closes_Popup()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Assert.False(picker.IsDropdownOpen);
Dispatcher.UIThread.RunJobs();
window.MouseDown(new Point(10, 10), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
window.KeyPressQwerty(PhysicalKey.Escape, RawInputModifiers.None);
Assert.False(picker.IsDropdownOpen);
}
[AvaloniaFact]
public void Press_Down_Opens_Popup()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Assert.False(picker.IsDropdownOpen);
Dispatcher.UIThread.RunJobs();
window.MouseDown(new Point(10, 10), MouseButton.Left);
window.KeyPressQwerty(PhysicalKey.ArrowDown, RawInputModifiers.None);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
}
[AvaloniaFact]
public void Press_Tab_Closes_Popup()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = new StackPanel()
{
Children =
{
picker,
new TextBox(),
}
};
window.Show();
Assert.False(picker.IsDropdownOpen);
Dispatcher.UIThread.RunJobs();
window.MouseDown(new Point(10, 10), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
window.KeyPressQwerty(PhysicalKey.Tab, RawInputModifiers.None);
Assert.False(picker.IsDropdownOpen);
}
[AvaloniaFact]
public void SelectedDate_Set_TextBox_Text()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
DisplayFormat = "yyyy-MM-dd",
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Dispatcher.UIThread.RunJobs();
window.MouseDown(new Point(10, 10), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
var popup = picker.GetTemplateChildOfType<Popup>(DatePicker.PART_Popup);
var calendar = popup.GetLogicalDescendants().OfType<CalendarView>().FirstOrDefault();
calendar?.RaiseEvent(new CalendarDayButtonEventArgs(new DateTime(2025, 2, 17))
{ RoutedEvent = CalendarView.DateSelectedEvent });
Dispatcher.UIThread.RunJobs();
var textBox = picker.GetTemplateChildOfType<TextBox>(DatePicker.PART_TextBox);
Assert.NotNull(textBox);
Assert.Equal("2025-02-17", textBox.Text);
Assert.False(picker.IsDropdownOpen);
}
[AvaloniaFact]
public void Set_SelectedDate_To_Null_Clears_TextBox()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
DisplayFormat = "yyyy-MM-dd",
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Dispatcher.UIThread.RunJobs();
window.MouseDown(new Point(10, 10), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
var popup = picker.GetTemplateChildOfType<Popup>(DatePicker.PART_Popup);
var calendar = popup.GetLogicalDescendants().OfType<CalendarView>().FirstOrDefault();
calendar?.RaiseEvent(new CalendarDayButtonEventArgs(new DateTime(2025, 2, 17))
{ RoutedEvent = CalendarView.DateSelectedEvent });
Dispatcher.UIThread.RunJobs();
var textBox = picker.GetTemplateChildOfType<TextBox>(DatePicker.PART_TextBox);
Assert.NotNull(textBox);
Assert.Equal("2025-02-17", textBox.Text);
Assert.False(picker.IsDropdownOpen);
picker.SelectedDate = null;
Dispatcher.UIThread.RunJobs();
Assert.Null(textBox.Text);
}
[AvaloniaFact]
public void Set_SelectedDate_Updates_TextBox()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
DisplayFormat = "yyyy-MM-dd",
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Dispatcher.UIThread.RunJobs();
var textBox = picker.GetTemplateChildOfType<TextBox>(DatePicker.PART_TextBox);
picker.SelectedDate = new DateTime(2025, 2, 18);
Dispatcher.UIThread.RunJobs();
Assert.Equal("2025-02-18", textBox?.Text);
}
[AvaloniaFact]
public void Set_Valid_TextBox_Text_Updates_SelectedDate()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
DisplayFormat = "yyyy-MM-dd",
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Dispatcher.UIThread.RunJobs();
var textBox = picker.GetTemplateChildOfType<TextBox>(DatePicker.PART_TextBox);
textBox?.RaiseEvent(new TextChangedEventArgs(TextBox.TextChangedEvent));
Dispatcher.UIThread.RunJobs();
Assert.Null(picker.SelectedDate);
textBox?.SetValue(TextBox.TextProperty, "2025-02-18");
Dispatcher.UIThread.RunJobs();
Assert.Equal(new DateTime(2025, 2, 18), picker.SelectedDate);
}
[AvaloniaFact]
public void Set_Invalid_TextBox_Text_Clears_SelectedDate()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
DisplayFormat = "yyyy-MM-dd",
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Dispatcher.UIThread.RunJobs();
var textBox = picker.GetTemplateChildOfType<TextBox>(DatePicker.PART_TextBox);
Dispatcher.UIThread.RunJobs();
Assert.Null(picker.SelectedDate);
textBox?.SetValue(TextBox.TextProperty, "2025-02-18");
Dispatcher.UIThread.RunJobs();
Assert.Equal(new DateTime(2025, 2, 18), picker.SelectedDate);
textBox?.SetValue(TextBox.TextProperty, "2025-02-18-");
Dispatcher.UIThread.RunJobs();
Assert.Null(picker.SelectedDate);
}
[AvaloniaFact]
public void Empty_DisplayFormat_Works()
{
var window = new Window();
var picker = new DatePicker()
{
Width = 300,
DisplayFormat = null,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Dispatcher.UIThread.RunJobs();
var textBox = picker.GetTemplateChildOfType<TextBox>(DatePicker.PART_TextBox);
textBox?.RaiseEvent(new TextChangedEventArgs(TextBox.TextChangedEvent));
Dispatcher.UIThread.RunJobs();
Assert.Null(picker.SelectedDate);
textBox?.SetValue(TextBox.TextProperty, "2025-02-18");
Dispatcher.UIThread.RunJobs();
Assert.Equal(new DateTime(2025, 2, 18), picker.SelectedDate);
textBox?.SetValue(TextBox.TextProperty, "2025/02/19");
Dispatcher.UIThread.RunJobs();
Assert.Equal(new DateTime(2025, 2, 19), picker.SelectedDate);
}
[AvaloniaFact]
public void Ensure_Focusable()
{
var picker = new DatePicker();
Assert.True(picker.Focusable);
}
[AvaloniaFact]
public void Click_On_Popup_Will_Not_Close_Popup()
{
var window = new Window()
{
Width = 800, Height = 800
};
var picker = new DatePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
Assert.False(picker.IsDropdownOpen);
Dispatcher.UIThread.RunJobs();
window.MouseDown(new Point(10, 10), MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
var popup = picker.GetTemplateChildOfType<Popup>(DatePicker.PART_Popup);
var calendar = popup?.GetLogicalDescendants().OfType<CalendarView>().FirstOrDefault();
Assert.NotNull(calendar);
var nextButton = calendar.GetTemplateChildOfType<Button>(CalendarView.PART_NextButton);
Assert.NotNull(nextButton);
var position = nextButton.TranslatePoint(new Point(5, 5), window);
Assert.NotNull(position);
window.MouseDown(new Point(10, 10), MouseButton.Left);
var renderRoot = popup.GetVisualRoot();
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
}
}

View File

@@ -0,0 +1,195 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Headless.XUnit;
using Avalonia.Threading;
using TimePickerPresenter = Ursa.Controls.TimePickerPresenter;
namespace HeadlessTest.Ursa.Controls.DateTimePicker;
public class TimePickerPresenterTests
{
[AvaloniaFact]
public void TimePickerPresenter_DefaultValues_ShouldBeCorrect()
{
var presenter = new TimePickerPresenter();
Assert.False(presenter.NeedsConfirmation);
Assert.Equal(1, presenter.MinuteIncrement);
Assert.Equal(1, presenter.SecondIncrement);
Assert.Null(presenter.TimeHolder);
Assert.Equal("HH mm ss t", presenter.PanelFormat);
}
[AvaloniaFact]
public void TimePickerPresenter_SetTime_ShouldUpdateTimeProperty()
{
var presenter = new TimePickerPresenter();
var time = new TimeSpan(10, 30, 45);
presenter.TimeHolder = time;
Assert.Equal(time, presenter.TimeHolder);
}
[AvaloniaTheory]
[InlineData("hh mm ss", 0, 2, 4, 0, true, true, true, false)]
[InlineData("hh mm ss t", 0, 2, 4, 6, true, true, true, true)]
[InlineData("hh mm", 0, 2, 0, 0, true, true, false, false)]
[InlineData("mm ss", 0, 0, 2, 0, false, true, true, false)]
[InlineData("ss", 0, 0, 0, 0, false, false, true, false)]
[InlineData("t", 0, 0, 0, 0, false, false, false, true)]
public void TimePickerPresenter_SetPanelFormat_ShouldUpdatePanelLayout(
string format, int hourColumn, int minuteColumn, int secondColumn, int amColumn,
bool isHourPanelVisible, bool isMinutePanelVisible, bool isSecondPanelVisible, bool isAmPanelVisible
)
{
var window = new Window();
var presenter = new TimePickerPresenter();
window.Content = presenter;
window.Show();
Dispatcher.UIThread.RunJobs();
presenter.PanelFormat = format;
Dispatcher.UIThread.RunJobs();
Assert.Equal(format, presenter.PanelFormat);
var hourPanel = presenter.GetTemplateChildren().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_HourScrollPanel);
var minutePanel = presenter.GetTemplateChildren().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_MinuteScrollPanel);
var secondPanel = presenter.GetTemplateChildren().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_SecondScrollPanel);
var amPanel = presenter.GetTemplateChildren().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_AmPmScrollPanel);
Assert.NotNull(hourPanel);
Assert.NotNull(minutePanel);
Assert.NotNull(secondPanel);
Assert.NotNull(amPanel);
Assert.Equal(hourColumn, Grid.GetColumn(hourPanel));
Assert.Equal(minuteColumn, Grid.GetColumn(minutePanel));
Assert.Equal(secondColumn, Grid.GetColumn(secondPanel));
Assert.Equal(amColumn, Grid.GetColumn(amPanel));
Assert.Equal(isHourPanelVisible, hourPanel.IsVisible);
Assert.Equal(isMinutePanelVisible, minutePanel.IsVisible);
Assert.Equal(isSecondPanelVisible, secondPanel.IsVisible);
Assert.Equal(isAmPanelVisible, amPanel.IsVisible);
}
[AvaloniaFact]
public void TimePickerPresenter_Confirm_ShouldSetTimeProperty()
{
var presenter = new TimePickerPresenter { NeedsConfirmation = true };
var time = new TimeSpan(10, 30, 45);
var eventRaised = 0;
TimeSpan? eventResult = null;
presenter.SelectedTimeChanged += (o, e) =>
{
eventRaised++;
eventResult = e.NewTime;
};
presenter.TimeHolder = time;
Assert.Null(eventResult);
Assert.Equal(0, eventRaised);
presenter.Confirm();
Assert.Equal(time, presenter.TimeHolder);
Assert.Equal(time, eventResult);
Assert.Equal(1, eventRaised);
}
[AvaloniaFact]
public void TimePickerPresenter_SyncTime_Should_Not_RaiseEvent()
{
var presenter = new TimePickerPresenter();
var oldTime = new TimeSpan(10, 30, 45);
var newTime = new TimeSpan(11, 45, 30);
presenter.SyncTime(oldTime);
var eventRaised = false;
presenter.SelectedTimeChanged += (sender, args) =>
{
eventRaised = true;
};
presenter.SyncTime(newTime);
Assert.False(eventRaised);
}
[AvaloniaTheory]
[MemberData(nameof(GetSelectionMemberData))]
public void TimePickerPresenter_Time_Updated_When_Panel_Selection_Changed(string format, int hourSelection, int minuteSelection, int secondSelection, int amSelection, TimeSpan expectedTime)
{
var window = new Window();
var presenter = new TimePickerPresenter();
window.Content = presenter;
window.Show();
Dispatcher.UIThread.RunJobs();
presenter.PanelFormat = format;
TimeSpan? eventResult = null;
presenter.SelectedTimeChanged += (o, e) =>
{
eventResult = e.NewTime;
};
Dispatcher.UIThread.RunJobs();
var hourPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_HourSelector);
var minutePanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_MinuteSelector);
var secondPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_SecondSelector);
var amPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_AmPmSelector);
Assert.NotNull(hourPanel);
Assert.NotNull(minutePanel);
Assert.NotNull(secondPanel);
Assert.NotNull(amPanel);
hourPanel.SelectedValue = hourSelection;
minutePanel.SelectedValue = minuteSelection;
secondPanel.SelectedValue = secondSelection;
amPanel.SelectedValue = amSelection;
Assert.Equal(expectedTime, eventResult);
}
[AvaloniaTheory]
[MemberData(nameof(GetSelectionMemberData))]
public void TimePickerPresenter_TimeHolder_Updated_When_Panel_Selection_Changed(string format, int hourSelection, int minuteSelection, int secondSelection, int amSelection, TimeSpan expectedTime)
{
var window = new Window();
var presenter = new TimePickerPresenter(){NeedsConfirmation = true};
window.Content = presenter;
window.Show();
Dispatcher.UIThread.RunJobs();
presenter.PanelFormat = format;
Dispatcher.UIThread.RunJobs();
var hourPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_HourSelector);
var minutePanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_MinuteSelector);
var secondPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_SecondSelector);
var amPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_AmPmSelector);
Assert.NotNull(hourPanel);
Assert.NotNull(minutePanel);
Assert.NotNull(secondPanel);
Assert.NotNull(amPanel);
hourPanel.SelectedValue = hourSelection;
minutePanel.SelectedValue = minuteSelection;
secondPanel.SelectedValue = secondSelection;
amPanel.SelectedValue = amSelection;
Assert.Equal(expectedTime, presenter.TimeHolder);
}
public static IEnumerable<object[]> GetSelectionMemberData()
{
yield return new object[] { "hh mm ss t", 1, 1, 1, 1, new TimeSpan(0, 13, 1, 1) };
yield return new object[] { "HH mm ss t", 12, 30, 45, 0, new TimeSpan(0, 12,30,45)};
yield return new object[] { "HH mm ss t", 12, 0, 0, 1, new TimeSpan(0, 12,0,0)};
yield return new object[] { "HH mm ss t", 13, 0, 0, 1, new TimeSpan(0, 13,0,0)};
yield return new object[] { "hh mm ss t", 9, 0, 0, 0, new TimeSpan(0, 9,0,0)};
yield return new object[] { "hh mm ss t", 9, 0, 0, 1, new TimeSpan(0, 21,0,0)};
yield return new object[] { "HH mm ss t", 0, 0, 0, 0, new TimeSpan(0, 0,0,0)};
}
}

View File

@@ -0,0 +1,62 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Headless;
using Avalonia.Headless.XUnit;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.Threading;
using HeadlessTest.Ursa.TestHelpers;
using TimePicker = Ursa.Controls.TimePicker;
namespace HeadlessTest.Ursa.Controls.DateTimePicker;
public class TimePickerTests
{
[AvaloniaFact]
public void Click_Opens_Popup()
{
var window = new Window();
var timePicker = new TimePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top,
};
window.Content = timePicker;
window.Show();
Dispatcher.UIThread.RunJobs();
Assert.False(timePicker.IsDropdownOpen);
window.MouseDown(new Point(10, 10), MouseButton.Left);
Assert.True(timePicker.IsDropdownOpen);
}
[AvaloniaFact]
public void Click_Button_Toggles_Popup()
{
var window = new Window();
var picker = new TimePicker()
{
Width = 300,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
window.Content = picker;
window.Show();
var button = picker.GetTemplateChildOfType<Button>(TimePicker.PART_Button);
var position = button?.TranslatePoint(new Point(5, 5), window);
Assert.NotNull(position);
Assert.False(picker.IsDropdownOpen);
Dispatcher.UIThread.RunJobs();
window.MouseDown(position.Value, MouseButton.Left);
window.MouseUp(position.Value, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.True(picker.IsDropdownOpen);
window.MouseDown(position.Value, MouseButton.Left);
window.MouseUp(position.Value, MouseButton.Left);
Dispatcher.UIThread.RunJobs();
Assert.False(picker.IsDropdownOpen);
}
}

View File

@@ -8,8 +8,14 @@ public class DrawerCloseTestPopupControlVM : IDialogContext
{
RequestClose?.Invoke(this, Result);
}
#if NET8_0
public int Result { get; } = Random.Shared.Next();
#endif
#if NETSTANDARD2_0
private static Random r = new Random();
public int Result { get; } = r.Next();
#endif
public event EventHandler<object?>? RequestClose;
}

View File

@@ -3,6 +3,7 @@
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>

View File

@@ -0,0 +1,17 @@
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
namespace HeadlessTest.Ursa.TestHelpers;
public static class ControlExtensions
{
public static IEnumerable<T> GetTemplateChildrenOfType<T>(this TemplatedControl control) where T: Control
{
return control.GetTemplateChildren().OfType<T>();
}
public static T? GetTemplateChildOfType<T>(this TemplatedControl control, string name) where T : Control
{
return control.GetTemplateChildren().OfType<T>().FirstOrDefault(a => a.Name == name);
}
}

View File

@@ -0,0 +1,122 @@
using Ursa.Controls;
namespace Test.Ursa.DateTimePicker;
public class CalendarContextTests
{
[Fact]
public void Clone_ReturnsExactCopy()
{
var context = new CalendarContext(2023, 5, 2000, 2030);
var clone = context.Clone();
Assert.Equal(context.Year, clone.Year);
Assert.Equal(context.Month, clone.Month);
Assert.Equal(context.StartYear, clone.StartYear);
Assert.Equal(context.EndYear, clone.EndYear);
}
[Fact]
public void Today_ReturnsCurrentDate()
{
var today = CalendarContext.Today();
Assert.Equal(DateTime.Today.Year, today.Year);
Assert.Equal(DateTime.Today.Month, today.Month);
}
[Fact]
public void With_UpdatesSpecifiedFields()
{
var context = new CalendarContext(2023, 5, 2000, 2030);
var updated = context.With(month: 6);
Assert.Equal(2023, updated.Year);
Assert.Equal(6, updated.Month);
Assert.Equal(2000, updated.StartYear);
Assert.Equal(2030, updated.EndYear);
}
[Fact]
public void NextMonth_UpdatesToNextMonth()
{
var context = new CalendarContext(2023, 5);
var nextMonth = context.NextMonth();
Assert.Equal(2023, nextMonth.Year);
Assert.Equal(6, nextMonth.Month);
}
[Fact]
public void NextMonth_UpdatesToNextYear()
{
var context = new CalendarContext(2023, 12);
var nextMonth = context.NextMonth();
Assert.Equal(2024, nextMonth.Year);
Assert.Equal(1, nextMonth.Month);
}
[Fact]
public void PreviousMonth_UpdatesToPreviousMonth()
{
var context = new CalendarContext(2023, 5);
var previousMonth = context.PreviousMonth();
Assert.Equal(2023, previousMonth.Year);
Assert.Equal(4, previousMonth.Month);
}
[Fact]
public void PreviousMonth_UpdatesToPreviousYear()
{
var context = new CalendarContext(2023, 1);
var previousMonth = context.PreviousMonth();
Assert.Equal(2022, previousMonth.Year);
Assert.Equal(12, previousMonth.Month);
}
[Fact]
public void NextYear_UpdatesToNextYear()
{
var context = new CalendarContext(2023, 5);
var nextYear = context.NextYear();
Assert.Equal(2024, nextYear.Year);
Assert.Equal(5, nextYear.Month);
}
[Fact]
public void PreviousYear_UpdatesToPreviousYear()
{
var context = new CalendarContext(2023, 5);
var previousYear = context.PreviousYear();
Assert.Equal(2022, previousYear.Year);
Assert.Equal(5, previousYear.Month);
}
[Fact]
public void CompareTo_SameContext_ReturnsZero()
{
var context1 = new CalendarContext(2023, 5);
var context2 = new CalendarContext(2023, 5);
Assert.Equal(0, context1.CompareTo(context2));
}
[Fact]
public void CompareTo_DifferentYear_ReturnsNonZero()
{
var context1 = new CalendarContext(2023, 5);
var context2 = new CalendarContext(2024, 5);
Assert.NotEqual(0, context1.CompareTo(context2));
}
[Fact]
public void CompareTo_DifferentMonth_ReturnsNonZero()
{
var context1 = new CalendarContext(2023, 5);
var context2 = new CalendarContext(2023, 6);
Assert.NotEqual(0, context1.CompareTo(context2));
}
[Fact]
public void ToString_ReturnsCorrectFormat()
{
var context = new CalendarContext(2023, 5, 2000, 2030);
var expected = "Start: 2000, End: 2030, Year: 2023, Month: 5";
Assert.Equal(expected, context.ToString());
}
}

View File

@@ -0,0 +1,112 @@
using Avalonia.Input;
using Irihi.Avalonia.Shared.Common;
using CalendarDayButton = Ursa.Controls.CalendarDayButton;
namespace Test.Ursa.DateTimePicker;
public class CalendarDayButtonTests
{
[Fact]
public void IsToday_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsToday = true;
Assert.Contains(CalendarDayButton.PC_Today, button.Classes);
Assert.True(button.IsToday);
}
[Fact]
public void IsStartDate_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsStartDate = true;
Assert.Contains(CalendarDayButton.PC_StartDate, button.Classes);
Assert.True(button.IsStartDate);
}
[Fact]
public void IsEndDate_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsEndDate = true;
Assert.Contains(CalendarDayButton.PC_EndDate, button.Classes);
Assert.True(button.IsEndDate);
}
[Fact]
public void IsPreviewStartDate_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsPreviewStartDate = true;
Assert.Contains(CalendarDayButton.PC_PreviewStartDate, button.Classes);
Assert.True(button.IsPreviewStartDate);
}
[Fact]
public void IsPreviewEndDate_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsPreviewEndDate = true;
Assert.Contains(CalendarDayButton.PC_PreviewEndDate, button.Classes);
Assert.True(button.IsPreviewEndDate);
}
[Fact]
public void IsInRange_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsInRange = true;
Assert.Contains(CalendarDayButton.PC_InRange, button.Classes);
Assert.True(button.IsInRange);
}
[Fact]
public void IsSelected_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsSelected = true;
Assert.Contains(PseudoClassName.PC_Selected, button.Classes);
Assert.True(button.IsSelected);
}
[Fact]
public void IsBlackout_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsBlackout = true;
Assert.Contains(CalendarDayButton.PC_Blackout, button.Classes);
Assert.True(button.IsBlackout);
}
[Fact]
public void IsNotCurrentMonth_SetsPseudoClass()
{
var button = new CalendarDayButton();
button.IsNotCurrentMonth = true;
Assert.Contains(CalendarDayButton.PC_NotCurrentMonth, button.Classes);
Assert.True(button.IsNotCurrentMonth);
}
[Fact]
public void ResetSelection_ClearsPseudoClasses()
{
var button = new CalendarDayButton();
button.IsSelected = true;
button.IsStartDate = true;
button.ResetSelection();
Assert.DoesNotContain(PseudoClassName.PC_Selected, button.Classes);
Assert.DoesNotContain(CalendarDayButton.PC_StartDate, button.Classes);
}
[Fact]
public void IsToday_ClearsOtherPseudoClasses()
{
var button = new CalendarDayButton();
button.IsStartDate = true;
button.IsEndDate = true;
button.IsToday = true;
Assert.Contains(CalendarDayButton.PC_Today, button.Classes);
Assert.Contains(CalendarDayButton.PC_EndDate, button.Classes);
Assert.DoesNotContain(CalendarDayButton.PC_StartDate, button.Classes);
}
}

View File

@@ -0,0 +1,53 @@
using Ursa.Controls;
namespace Test.Ursa.DateTimePicker;
public class DateRangeExtensionTests
{
[Fact]
public void Contains_DateInAnyRange_ShouldReturnTrue()
{
var ranges = new List<DateRange>
{
new(new DateTime(2023, 10, 1), new DateTime(2023, 10, 5)),
new(new DateTime(2023, 10, 10), new DateTime(2023, 10, 15))
};
var date = new DateTime(2023, 10, 3);
Assert.True(ranges.Contains(date));
}
[Fact]
public void Contains_DateNotInAnyRange_ShouldReturnFalse()
{
var ranges = new List<DateRange>
{
new(new DateTime(2023, 10, 1), new DateTime(2023, 10, 5)),
new(new DateTime(2023, 10, 10), new DateTime(2023, 10, 15))
};
var date = new DateTime(2023, 10, 6);
Assert.False(ranges.Contains(date));
}
[Fact]
public void Contains_NullDate_ShouldReturnFalse()
{
var ranges = new List<DateRange>
{
new(new DateTime(2023, 10, 1), new DateTime(2023, 10, 5)),
new(new DateTime(2023, 10, 10), new DateTime(2023, 10, 15))
};
Assert.False(ranges.Contains((DateTime?)null));
}
[Fact]
public void Contains_NullRanges_ShouldReturnFalse()
{
IEnumerable<DateRange>? ranges = null;
var date = new DateTime(2023, 10, 3);
Assert.False(ranges.Contains(date));
}
}

View File

@@ -0,0 +1,64 @@
using Ursa.Controls;
namespace Test.Ursa.DateTimePicker;
public class DateRangeTests
{
[Fact]
public void DateRange_SingleDayRange_ShouldHaveSameStartAndEnd()
{
var date = new DateTime(2023, 10, 1);
var range = new DateRange(date);
Assert.Equal(date.Date, range.Start);
Assert.Equal(date.Date, range.End);
}
[Fact]
public void DateRange_ValidRange_ShouldSetStartAndEndCorrectly()
{
var start = new DateTime(2023, 10, 1);
var end = new DateTime(2023, 10, 5);
var range = new DateRange(start, end);
Assert.Equal(start.Date, range.Start);
Assert.Equal(end.Date, range.End);
}
[Fact]
public void DateRange_EndBeforeStart_ShouldSetEndToStart()
{
var start = new DateTime(2023, 10, 5);
var end = new DateTime(2023, 10, 1);
var range = new DateRange(start, end);
Assert.Equal(start.Date, range.Start);
Assert.Equal(start.Date, range.End);
}
[Fact]
public void Contains_DateWithinRange_ShouldReturnTrue()
{
var range = new DateRange(new DateTime(2023, 10, 1), new DateTime(2023, 10, 5));
var date = new DateTime(2023, 10, 3);
Assert.True(range.Contains(date));
}
[Fact]
public void Contains_DateOutsideRange_ShouldReturnFalse()
{
var range = new DateRange(new DateTime(2023, 10, 1), new DateTime(2023, 10, 5));
var date = new DateTime(2023, 10, 6);
Assert.False(range.Contains(date));
}
[Fact]
public void Contains_NullDate_ShouldReturnFalse()
{
var range = new DateRange(new DateTime(2023, 10, 1), new DateTime(2023, 10, 5));
Assert.False(range.Contains(null));
}
}

View File

@@ -0,0 +1,74 @@
using Ursa.Controls;
namespace Test.Ursa.DateTimePicker;
public class DateTimeHelperTests
{
[Fact]
public void GetFirstDayOfMonth_ReturnsFirstDay()
{
var date = new DateTime(2023, 5, 15);
var firstDay = date.GetFirstDayOfMonth();
Assert.Equal(new DateTime(2023, 5, 1), firstDay);
}
[Fact]
public void GetLastDayOfMonth_ReturnsLastDay()
{
var date = new DateTime(2023, 5, 15);
var lastDay = date.GetLastDayOfMonth();
Assert.Equal(new DateTime(2023, 5, 31), lastDay);
}
[Fact]
public void CompareYearMonth_SameYearMonth_ReturnsZero()
{
var date1 = new DateTime(2023, 5, 15);
var date2 = new DateTime(2023, 5, 20);
var result = DateTimeHelper.CompareYearMonth(date1, date2);
Assert.Equal(0, result);
}
[Fact]
public void CompareYearMonth_DifferentYearMonth_ReturnsDifference()
{
var date1 = new DateTime(2023, 5, 15);
var date2 = new DateTime(2024, 6, 20);
var result = DateTimeHelper.CompareYearMonth(date1, date2);
Assert.Equal(-13, result);
}
[Fact]
public void Min_ReturnsMinimumDate()
{
var date1 = new DateTime(2023, 5, 15);
var date2 = new DateTime(2024, 6, 20);
var result = DateTimeHelper.Min(date1, date2);
Assert.Equal(date1, result);
}
[Fact]
public void Max_ReturnsMaximumDate()
{
var date1 = new DateTime(2023, 5, 15);
var date2 = new DateTime(2024, 6, 20);
var result = DateTimeHelper.Max(date1, date2);
Assert.Equal(date2, result);
}
[Fact]
public void GetDecadeViewRangeByYear_ReturnsCorrectRange()
{
var year = 2023;
var result = DateTimeHelper.GetDecadeViewRangeByYear(year);
Assert.Equal((2020, 2029), result);
}
[Fact]
public void GetCenturyViewRangeByYear_ReturnsCorrectRange()
{
var year = 2023;
var result = DateTimeHelper.GetCenturyViewRangeByYear(year);
Assert.Equal((2000, 2100), result);
}
}