fix: completely fix timepicker racing issue.

This commit is contained in:
Dong Bin
2025-02-18 18:02:37 +08:00
parent e062efead0
commit 27375f722e
7 changed files with 237 additions and 131 deletions

View File

@@ -57,22 +57,6 @@ public class DatePicker: DatePickerBase, IClearControl
SyncSelectedDateToText(args.NewValue.Value); SyncSelectedDateToText(args.NewValue.Value);
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == IsDropdownOpenProperty)
{
if (change.GetNewValue<bool>() == false)
{
}
else
{
}
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);

View File

@@ -15,7 +15,7 @@ namespace Ursa.Controls;
[TemplatePart(PART_Popup, typeof(Popup))] [TemplatePart(PART_Popup, typeof(Popup))]
[TemplatePart(PART_TextBox, typeof(TextBox))] [TemplatePart(PART_TextBox, typeof(TextBox))]
[TemplatePart(PART_Calendar, typeof(CalendarView))] [TemplatePart(PART_Calendar, typeof(CalendarView))]
[TemplatePart(PART_TimePicker, typeof(TimePicker))] [TemplatePart(PART_TimePicker, typeof(TimePickerPresenter))]
public class DateTimePicker : DatePickerBase public class DateTimePicker : DatePickerBase
{ {
public const string PART_Button = "PART_Button"; public const string PART_Button = "PART_Button";
@@ -85,14 +85,14 @@ public class DateTimePicker : DatePickerBase
{ {
_textBox?.SetValue(TextBox.TextProperty, null); _textBox?.SetValue(TextBox.TextProperty, null);
_calendar?.ClearSelection(); _calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null); _timePickerPresenter?.SyncTime(null);
} }
else else
{ {
_textBox?.SetValue(TextBox.TextProperty, _textBox?.SetValue(TextBox.TextProperty,
date.Value.ToString(DisplayFormat ?? CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern)); date.Value.ToString(DisplayFormat ?? CultureInfo.InvariantCulture.DateTimeFormat.FullDateTimePattern));
_calendar?.MarkDates(date.Value.Date, date.Value.Date); _calendar?.MarkDates(date.Value.Date, date.Value.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.Value.TimeOfDay); _timePickerPresenter?.SyncTime(date.Value.TimeOfDay);
} }
} }
@@ -175,7 +175,7 @@ public class DateTimePicker : DatePickerBase
var date = SelectedDate ?? DateTime.Now; var date = SelectedDate ?? DateTime.Now;
_calendar.ContextDate = new CalendarContext(date.Year, date.Month); _calendar.ContextDate = new CalendarContext(date.Year, date.Month);
_calendar.UpdateDayButtons(); _calendar.UpdateDayButtons();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, SelectedDate?.TimeOfDay); _timePickerPresenter?.SyncTime(SelectedDate?.TimeOfDay);
} }
SetCurrentValue(IsDropdownOpenProperty, true); SetCurrentValue(IsDropdownOpenProperty, true);
@@ -193,7 +193,7 @@ public class DateTimePicker : DatePickerBase
{ {
SetCurrentValue(SelectedDateProperty, null); SetCurrentValue(SelectedDateProperty, null);
_calendar?.ClearSelection(); _calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null); _timePickerPresenter?.SyncTime(null);
} }
else if (DisplayFormat is null || DisplayFormat.Length == 0) else if (DisplayFormat is null || DisplayFormat.Length == 0)
{ {
@@ -201,7 +201,7 @@ public class DateTimePicker : DatePickerBase
{ {
SetCurrentValue(SelectedDateProperty, defaultTime); SetCurrentValue(SelectedDateProperty, defaultTime);
_calendar?.MarkDates(defaultTime.Date, defaultTime.Date); _calendar?.MarkDates(defaultTime.Date, defaultTime.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, defaultTime.TimeOfDay); _timePickerPresenter?.SyncTime(defaultTime.TimeOfDay);
} }
} }
else else
@@ -217,14 +217,14 @@ public class DateTimePicker : DatePickerBase
} }
_calendar?.MarkDates(date.Date, date.Date); _calendar?.MarkDates(date.Date, date.Date);
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.TimeOfDay); _timePickerPresenter?.SyncTime(date.TimeOfDay);
} }
else else
{ {
SetCurrentValue(SelectedDateProperty, null); SetCurrentValue(SelectedDateProperty, null);
if (!fromText) _textBox?.SetValue(TextBox.TextProperty, null); if (!fromText) _textBox?.SetValue(TextBox.TextProperty, null);
_calendar?.ClearSelection(); _calendar?.ClearSelection();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, null); _timePickerPresenter?.SyncTime(null);
} }
} }
} }
@@ -236,7 +236,7 @@ public class DateTimePicker : DatePickerBase
var date = SelectedDate ?? DateTime.Today; var date = SelectedDate ?? DateTime.Today;
_calendar.ContextDate = _calendar.ContextDate.With(date.Year, date.Month); _calendar.ContextDate = _calendar.ContextDate.With(date.Year, date.Month);
_calendar.UpdateDayButtons(); _calendar.UpdateDayButtons();
_timePickerPresenter?.SetValue(TimePickerPresenter.TimeProperty, date.TimeOfDay); _timePickerPresenter?.SyncTime(date.TimeOfDay);
} }
SetCurrentValue(IsDropdownOpenProperty, true); SetCurrentValue(IsDropdownOpenProperty, true);

View File

@@ -29,24 +29,22 @@ public class TimePicker : TimePickerBase, IClearControl
public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<TimePicker, string?>( public static readonly StyledProperty<string?> WatermarkProperty = AvaloniaProperty.Register<TimePicker, string?>(
nameof(Watermark)); nameof(Watermark));
private bool _suppressTextPresenterEvent;
private Button? _button; 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() static TimePicker()
{ {
FocusableProperty.OverrideDefaultValue<TimePicker>(true);
SelectedTimeProperty.Changed.AddClassHandler<TimePicker, TimeSpan?>((picker, args) => SelectedTimeProperty.Changed.AddClassHandler<TimePicker, TimeSpan?>((picker, args) =>
picker.OnSelectionChanged(args)); picker.OnSelectionChanged(args));
DisplayFormatProperty.Changed.AddClassHandler<TimePicker, string?>((picker, args) => picker.OnDisplayFormatChanged(args)); DisplayFormatProperty.Changed.AddClassHandler<TimePicker, string?>((picker, args) =>
} picker.OnDisplayFormatChanged(args));
private void OnDisplayFormatChanged(AvaloniaPropertyChangedEventArgs<string?> _)
{
if (_textBox is null) return;
SyncTimeToText(SelectedTime);
} }
public string? Watermark public string? Watermark
@@ -64,7 +62,13 @@ public class TimePicker : TimePickerBase, IClearControl
public void Clear() public void Clear()
{ {
Focus(NavigationMethod.Pointer); Focus(NavigationMethod.Pointer);
_presenter?.SetValue(TimePickerPresenter.TimeProperty, null); _presenter?.SyncTime(null);
}
private void OnDisplayFormatChanged(AvaloniaPropertyChangedEventArgs<string?> _)
{
if (_textBox is null) return;
SyncTimeToText(SelectedTime);
} }
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
@@ -73,46 +77,43 @@ public class TimePicker : TimePickerBase, IClearControl
GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _textBox); GotFocusEvent.RemoveHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _textBox); TextBox.TextChangedEvent.RemoveHandler(OnTextChanged, _textBox);
PointerPressedEvent.RemoveHandler(OnTextBoxPointerPressed, _textBox);
Button.ClickEvent.RemoveHandler(OnButtonClick, _button); Button.ClickEvent.RemoveHandler(OnButtonClick, _button);
TimePickerPresenter.SelectedTimeChangedEvent.RemoveHandler(OnPresenterTimeChanged, _presenter); TimePickerPresenter.SelectedTimeChangedEvent.RemoveHandler(OnPresenterTimeChanged, _presenter);
_textBox = e.NameScope.Find<TextBox>(PART_TextBox); _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); _presenter = e.NameScope.Find<TimePickerPresenter>(PART_Presenter);
_button = e.NameScope.Find<Button>(PART_Button); _button = e.NameScope.Find<Button>(PART_Button);
GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox); GotFocusEvent.AddHandler(OnTextBoxGetFocus, _textBox);
TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox); TextBox.TextChangedEvent.AddHandler(OnTextChanged, _textBox);
PointerPressedEvent.AddHandler(OnTextBoxPointerPressed, RoutingStrategies.Tunnel, false, _textBox);
Button.ClickEvent.AddHandler(OnButtonClick, _button); Button.ClickEvent.AddHandler(OnButtonClick, _button);
TimePickerPresenter.SelectedTimeChangedEvent.AddHandler(OnPresenterTimeChanged, _presenter); TimePickerPresenter.SelectedTimeChangedEvent.AddHandler(OnPresenterTimeChanged, _presenter);
// SetCurrentValue(SelectedTimeProperty, DateTime.Now.TimeOfDay); // SetCurrentValue(SelectedTimeProperty, DateTime.Now.TimeOfDay);
_presenter?.SetValue(TimePickerPresenter.TimeProperty, SelectedTime); // _presenter?.SetValue(TimePickerPresenter.TimeProperty, SelectedTime);
_presenter?.SyncTime(SelectedTime);
SyncTimeToText(SelectedTime); SyncTimeToText(SelectedTime);
} }
private void OnPresenterTimeChanged(object? sender, TimeChangedEventArgs e) private void OnPresenterTimeChanged(object? sender, TimeChangedEventArgs e)
{ {
if (!IsInitialized) return;
if (_suppressTextPresenterEvent) return; if (_suppressTextPresenterEvent) return;
SetCurrentValue(SelectedTimeProperty, e.NewTime); SetCurrentValue(SelectedTimeProperty, e.NewTime);
} }
private void OnButtonClick(object? sender, RoutedEventArgs e) private void OnButtonClick(object? sender, RoutedEventArgs e)
{ {
Focus(NavigationMethod.Pointer); if(IsFocused)
SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen); {
} SetCurrentValue(IsDropdownOpenProperty, !IsDropdownOpen);
}
private void OnTextBoxPointerPressed(object? sender, PointerPressedEventArgs e)
{
SetCurrentValue(IsDropdownOpenProperty, true);
} }
private void OnTextBoxGetFocus(object? sender, GotFocusEventArgs e) private void OnTextBoxGetFocus(object? sender, GotFocusEventArgs e)
{ {
// SetCurrentValue(IsDropdownOpenProperty, true); SetCurrentValue(IsDropdownOpenProperty, true);
} }
protected override void OnKeyDown(KeyEventArgs e) protected override void OnKeyDown(KeyEventArgs e)
@@ -145,17 +146,22 @@ public class TimePicker : TimePickerBase, IClearControl
{ {
if (string.IsNullOrEmpty(_textBox?.Text)) if (string.IsNullOrEmpty(_textBox?.Text))
{ {
TimePickerPresenter.TimeProperty.SetValue(null, _presenter); _presenter?.SyncTime(null);
} }
else if (DisplayFormat is null || DisplayFormat.Length == 0) else if (DisplayFormat is null || DisplayFormat.Length == 0)
{ {
if (TimeSpan.TryParse(_textBox?.Text, out var defaultTime)) if (TimeSpan.TryParse(_textBox?.Text, out var defaultTime))
TimePickerPresenter.TimeProperty.SetValue(defaultTime, _presenter); {
_presenter?.SyncTime(defaultTime);
}
} }
else else
{ {
if (DateTime.TryParseExact(_textBox?.Text, DisplayFormat, CultureInfo.CurrentUICulture, DateTimeStyles.None, 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 +169,7 @@ public class TimePicker : TimePickerBase, IClearControl
{ {
if (_textBox is null) return; if (_textBox is null) return;
_suppressTextPresenterEvent = true; _suppressTextPresenterEvent = true;
_presenter?.SetValue(TimePickerPresenter.TimeProperty, args.NewValue.Value); _presenter?.SyncTime(args.NewValue.Value);
SyncTimeToText(args.NewValue.Value); SyncTimeToText(args.NewValue.Value);
_suppressTextPresenterEvent = false; _suppressTextPresenterEvent = false;
} }
@@ -176,6 +182,7 @@ public class TimePicker : TimePickerBase, IClearControl
_textBox.Text = null; _textBox.Text = null;
return; return;
} }
var date = new DateTime(1, 1, 1, time.Value.Hours, time.Value.Minutes, time.Value.Seconds); var date = new DateTime(1, 1, 1, time.Value.Hours, time.Value.Minutes, time.Value.Seconds);
var text = date.ToString(DisplayFormat); var text = date.ToString(DisplayFormat);
_textBox.Text = text; _textBox.Text = text;
@@ -199,4 +206,33 @@ public class TimePicker : TimePickerBase, IClearControl
base.UpdateDataValidation(property, state, error); base.UpdateDataValidation(property, state, error);
if (property == SelectedTimeProperty) DataValidationErrors.SetError(this, error); if (property == SelectedTimeProperty) DataValidationErrors.SetError(this, error);
} }
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 void FocusChanged(bool hasFocus)
{
var wasFocused = _isFocused;
_isFocused = hasFocus;
if (hasFocus)
if (!wasFocused && _textBox != null)
{
_textBox.Focus();
_textBox.SelectAll();
}
}
} }

View File

@@ -2,6 +2,7 @@
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Irihi.Avalonia.Shared.Helpers; using Irihi.Avalonia.Shared.Helpers;
@@ -49,14 +50,14 @@ public class TimePickerPresenter : TemplatedControl
AvaloniaProperty.Register<TimePickerPresenter, int>( AvaloniaProperty.Register<TimePickerPresenter, int>(
nameof(SecondIncrement), 1); nameof(SecondIncrement), 1);
public static readonly StyledProperty<TimeSpan?> TimeProperty =
AvaloniaProperty.Register<TimePickerPresenter, TimeSpan?>(
nameof(Time));
public static readonly StyledProperty<string> PanelFormatProperty = public static readonly StyledProperty<string> PanelFormatProperty =
AvaloniaProperty.Register<TimePickerPresenter, string>( AvaloniaProperty.Register<TimePickerPresenter, string>(
nameof(PanelFormat), "HH mm ss t"); nameof(PanelFormat), "HH mm ss t");
public static readonly RoutedEvent<TimeChangedEventArgs> SelectedTimeChangedEvent =
RoutedEvent.Register<TimePickerPresenter, TimeChangedEventArgs>(
nameof(SelectedTimeChanged), RoutingStrategies.Bubble);
private Control? _ampmScrollPanel; private Control? _ampmScrollPanel;
private DateTimePickerPanel? _ampmSelector; private DateTimePickerPanel? _ampmSelector;
private Control? _firstSeparator; private Control? _firstSeparator;
@@ -65,26 +66,21 @@ public class TimePickerPresenter : TemplatedControl
private DateTimePickerPanel? _hourSelector; private DateTimePickerPanel? _hourSelector;
private Control? _minuteScrollPanel; private Control? _minuteScrollPanel;
private DateTimePickerPanel? _minuteSelector; private DateTimePickerPanel? _minuteSelector;
private Grid? _pickerContainer; private Grid? _pickerContainer;
private Control? _secondScrollPanel; private Control? _secondScrollPanel;
private DateTimePickerPanel? _secondSelector; private DateTimePickerPanel? _secondSelector;
private Control? _secondSeparator; private Control? _secondSeparator;
private bool _surpressTimeEvent = true;
private Control? _thirdSeparator; private Control? _thirdSeparator;
internal TimeSpan TimeHolder;
private bool _updateFromTimeChange;
private bool _use12Clock; private bool _use12Clock;
internal TimeSpan? TimeHolder;
static TimePickerPresenter() static TimePickerPresenter()
{ {
PanelFormatProperty.Changed.AddClassHandler<TimePickerPresenter, string>((presenter, args) => PanelFormatProperty.Changed.AddClassHandler<TimePickerPresenter, string>((presenter, args) =>
presenter.OnPanelFormatChanged(args)); presenter.OnPanelFormatChanged(args));
TimeProperty.Changed.AddClassHandler<TimePickerPresenter, TimeSpan?>((presenter, args) =>
presenter.OnTimeChanged(args));
}
public TimePickerPresenter()
{
// SetCurrentValue(TimeProperty, DateTime.Now.TimeOfDay);
} }
public bool NeedsConfirmation public bool NeedsConfirmation
@@ -105,40 +101,18 @@ public class TimePickerPresenter : TemplatedControl
set => SetValue(SecondIncrementProperty, value); set => SetValue(SecondIncrementProperty, value);
} }
public TimeSpan? Time
{
get => GetValue(TimeProperty);
set => SetValue(TimeProperty, value);
}
public string PanelFormat public string PanelFormat
{ {
get => GetValue(PanelFormatProperty); get => GetValue(PanelFormatProperty);
set => SetValue(PanelFormatProperty, value); set => SetValue(PanelFormatProperty, value);
} }
public static readonly RoutedEvent<TimeChangedEventArgs> SelectedTimeChangedEvent =
RoutedEvent.Register<TimePickerPresenter, TimeChangedEventArgs>(
nameof(SelectedTimeChanged), RoutingStrategies.Bubble);
public event EventHandler<TimeChangedEventArgs> SelectedTimeChanged public event EventHandler<TimeChangedEventArgs> SelectedTimeChanged
{ {
add => AddHandler(SelectedTimeChangedEvent, value); add => AddHandler(SelectedTimeChangedEvent, value);
remove => RemoveHandler(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) private void OnPanelFormatChanged(AvaloniaPropertyChangedEventArgs<string> args)
{ {
var format = args.NewValue.Value; var format = args.NewValue.Value;
@@ -212,47 +186,63 @@ public class TimePickerPresenter : TemplatedControl
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(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); _hourSelector = e.NameScope.Find<DateTimePickerPanel>(PART_HourSelector);
_minuteSelector = e.NameScope.Find<DateTimePickerPanel>(PART_MinuteSelector); _minuteSelector = e.NameScope.Find<DateTimePickerPanel>(PART_MinuteSelector);
_secondSelector = e.NameScope.Find<DateTimePickerPanel>(PART_SecondSelector); _secondSelector = e.NameScope.Find<DateTimePickerPanel>(PART_SecondSelector);
_ampmSelector = e.NameScope.Find<DateTimePickerPanel>(PART_AmPmSelector); _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); _pickerContainer = e.NameScope.Find<Grid>(PART_PickerContainer);
_hourScrollPanel = e.NameScope.Find<Control>(PART_HourScrollPanel); _hourScrollPanel = e.NameScope.Find<Control>(PART_HourScrollPanel);
_minuteScrollPanel = e.NameScope.Find<Control>(PART_MinuteScrollPanel); _minuteScrollPanel = e.NameScope.Find<Control>(PART_MinuteScrollPanel);
_secondScrollPanel = e.NameScope.Find<Control>(PART_SecondScrollPanel); _secondScrollPanel = e.NameScope.Find<Control>(PART_SecondScrollPanel);
_ampmScrollPanel = e.NameScope.Find<Control>(PART_AmPmScrollPanel); _ampmScrollPanel = e.NameScope.Find<Control>(PART_AmPmScrollPanel);
_firstSeparator = e.NameScope.Find<Control>(PART_FirstSeparator); _firstSeparator = e.NameScope.Find<Control>(PART_FirstSeparator);
_secondSeparator = e.NameScope.Find<Control>(PART_SecondSeparator); _secondSeparator = e.NameScope.Find<Control>(PART_SecondSeparator);
_thirdSeparator = e.NameScope.Find<Control>(PART_ThirdSeparator); _thirdSeparator = e.NameScope.Find<Control>(PART_ThirdSeparator);
Initialize(); Initialize();
UpdatePanelLayout(PanelFormat); 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) private void OnPanelSelectionChanged(object? sender, System.EventArgs e)
{ {
if (_updateFromTimeChange) return; if (_surpressTimeEvent) return;
if (!_use12Clock && Equals(sender, _ampmSelector)) 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 hour = _hourSelector?.SelectedValue ?? time.Hours;
var minute = _minuteSelector?.SelectedValue ?? time.Minutes; var minute = _minuteSelector?.SelectedValue ?? time.Minutes;
var second = _secondSelector?.SelectedValue ?? time.Seconds; var second = _secondSelector?.SelectedValue ?? time.Seconds;
var ampm = _ampmSelector?.SelectedValue ?? (time.Hours >= 12 ? 1 : 0); var ampm = _ampmSelector?.SelectedValue ?? (time.Hours >= 12 ? 1 : 0);
if (_use12Clock) if (_use12Clock)
{
hour = ampm switch hour = ampm switch
{ {
0 when hour == 12 => 0, 0 when hour == 12 => 0,
1 when hour < 12 => hour + 12, 1 when hour < 12 => hour + 12,
_ => hour _ => hour
}; };
}
else else
{ {
ampm = hour switch ampm = hour switch
@@ -262,11 +252,23 @@ public class TimePickerPresenter : TemplatedControl
}; };
SetIfChanged(_ampmSelector, ampm); SetIfChanged(_ampmSelector, ampm);
} }
var newTime = new TimeSpan(hour, minute, second); var newTime = new TimeSpan(hour, minute, second);
if (NeedsConfirmation) if (NeedsConfirmation)
{
TimeHolder = newTime; TimeHolder = newTime;
}
else 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) private void UpdatePanelsFromSelectedTime(TimeSpan? time)
@@ -276,21 +278,19 @@ public class TimePickerPresenter : TemplatedControl
{ {
var index = _use12Clock ? time.Value.Hours % 12 : time.Value.Hours; var index = _use12Clock ? time.Value.Hours % 12 : time.Value.Hours;
if (_use12Clock && index == 0) index = 12; 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 var ampm = time.Value.Hours switch
{ {
>= 12 => 1, >= 12 => 1,
_ => 0 _ => 0
}; };
SetIfChanged(_ampmSelector, ampm); SetIfChanged(_ampmSelector, ampm, true);
if (_ampmSelector is not null) if (_ampmSelector is not null) _ampmSelector.IsEnabled = _use12Clock;
{
_ampmSelector.IsEnabled = _use12Clock;
}
} }
private void Initialize() private void Initialize()
@@ -327,12 +327,23 @@ public class TimePickerPresenter : TemplatedControl
public void Confirm() 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; if (panel is null) return;
panel.SelectionChanged -= OnPanelSelectionChanged;
if (panel.SelectedValue != index) panel.SelectedValue = index; 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() public void Clear()
{ {
Focus(NavigationMethod.Pointer); Focus(NavigationMethod.Pointer);
_startPresenter?.SetValue(TimePickerPresenter.TimeProperty, null); _startPresenter?.SyncTime(null);
_endPresenter?.SetValue(TimePickerPresenter.TimeProperty, null); _endPresenter?.SyncTime(null);
} }
private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args, bool start = true) private void OnSelectionChanged(AvaloniaPropertyChangedEventArgs<TimeSpan?> args, bool start = true)
{ {
SyncTimeToText(args.NewValue.Value, start); SyncTimeToText(args.NewValue.Value, start);
_suppressTextPresenterEvent = true; _suppressTextPresenterEvent = true;
TimePickerPresenter.TimeProperty.SetValue(args.NewValue.Value, start ? _startPresenter : _endPresenter); var presenter = start ? _startPresenter : _endPresenter;
presenter?.SyncTime(args.NewValue.Value);
_suppressTextPresenterEvent = false; _suppressTextPresenterEvent = false;
} }
@@ -150,8 +151,8 @@ public class TimeRangePicker : TimePickerBase, IClearControl
Button.ClickEvent.AddHandler(OnButtonClick, _button); Button.ClickEvent.AddHandler(OnButtonClick, _button);
TimePickerPresenter.SelectedTimeChangedEvent.AddHandler(OnPresenterTimeChanged, _startPresenter, _endPresenter); TimePickerPresenter.SelectedTimeChangedEvent.AddHandler(OnPresenterTimeChanged, _startPresenter, _endPresenter);
_startPresenter?.SetValue(TimePickerPresenter.TimeProperty, StartTime); _startPresenter?.SyncTime(StartTime);
_endPresenter?.SetValue(TimePickerPresenter.TimeProperty, EndTime); _endPresenter?.SyncTime(EndTime);
SyncTimeToText(StartTime); SyncTimeToText(StartTime);
SyncTimeToText(EndTime, false); SyncTimeToText(EndTime, false);
} }

View File

@@ -17,7 +17,7 @@ public class TimePickerPresenterTests
Assert.False(presenter.NeedsConfirmation); Assert.False(presenter.NeedsConfirmation);
Assert.Equal(1, presenter.MinuteIncrement); Assert.Equal(1, presenter.MinuteIncrement);
Assert.Equal(1, presenter.SecondIncrement); Assert.Equal(1, presenter.SecondIncrement);
Assert.Null(presenter.Time); Assert.Null(presenter.TimeHolder);
Assert.Equal("HH mm ss t", presenter.PanelFormat); Assert.Equal("HH mm ss t", presenter.PanelFormat);
} }
@@ -27,9 +27,9 @@ public class TimePickerPresenterTests
var presenter = new TimePickerPresenter(); var presenter = new TimePickerPresenter();
var time = new TimeSpan(10, 30, 45); var time = new TimeSpan(10, 30, 45);
presenter.Time = time; presenter.TimeHolder = time;
Assert.Equal(time, presenter.Time); Assert.Equal(time, presenter.TimeHolder);
} }
[AvaloniaTheory] [AvaloniaTheory]
@@ -77,37 +77,44 @@ public class TimePickerPresenterTests
[Fact] [AvaloniaFact]
public void TimePickerPresenter_Confirm_ShouldSetTimeProperty() public void TimePickerPresenter_Confirm_ShouldSetTimeProperty()
{ {
var presenter = new TimePickerPresenter { NeedsConfirmation = true }; var presenter = new TimePickerPresenter { NeedsConfirmation = true };
var time = new TimeSpan(10, 30, 45); var time = new TimeSpan(10, 30, 45);
var eventRaised = 0;
TimeSpan? eventResult = null;
presenter.SelectedTimeChanged += (o, e) =>
{
eventRaised++;
eventResult = e.NewTime;
};
presenter.TimeHolder = time; presenter.TimeHolder = time;
Assert.NotEqual(time, presenter.Time); Assert.Null(eventResult);
Assert.Equal(0, eventRaised);
presenter.Confirm(); presenter.Confirm();
Assert.Equal(time, presenter.Time); Assert.Equal(time, presenter.TimeHolder);
Assert.Equal(time, eventResult);
Assert.Equal(1, eventRaised);
} }
[AvaloniaFact] [AvaloniaFact]
public void TimePickerPresenter_OnTimeChanged_ShouldRaiseEvent() public void TimePickerPresenter_SyncTime_Should_Not_RaiseEvent()
{ {
var presenter = new TimePickerPresenter(); var presenter = new TimePickerPresenter();
var oldTime = new TimeSpan(10, 30, 45); var oldTime = new TimeSpan(10, 30, 45);
var newTime = new TimeSpan(11, 45, 30); var newTime = new TimeSpan(11, 45, 30);
presenter.Time = oldTime; presenter.SyncTime(oldTime);
var eventRaised = false; var eventRaised = false;
presenter.SelectedTimeChanged += (sender, args) => presenter.SelectedTimeChanged += (sender, args) =>
{ {
eventRaised = true; eventRaised = true;
Assert.Equal(oldTime, args.OldTime);
Assert.Equal(newTime, args.NewTime);
}; };
presenter.Time = newTime; presenter.SyncTime(newTime);
Assert.False(eventRaised);
Assert.True(eventRaised);
} }
[AvaloniaTheory] [AvaloniaTheory]
@@ -120,6 +127,11 @@ public class TimePickerPresenterTests
window.Show(); window.Show();
Dispatcher.UIThread.RunJobs(); Dispatcher.UIThread.RunJobs();
presenter.PanelFormat = format; presenter.PanelFormat = format;
TimeSpan? eventResult = null;
presenter.SelectedTimeChanged += (o, e) =>
{
eventResult = e.NewTime;
};
Dispatcher.UIThread.RunJobs(); Dispatcher.UIThread.RunJobs();
var hourPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_HourSelector); var hourPanel = presenter.GetTemplateChildren().OfType<DateTimePickerPanel>().FirstOrDefault(a => a.Name == TimePickerPresenter.PART_HourSelector);
@@ -137,7 +149,7 @@ public class TimePickerPresenterTests
secondPanel.SelectedValue = secondSelection; secondPanel.SelectedValue = secondSelection;
amPanel.SelectedValue = amSelection; amPanel.SelectedValue = amSelection;
Assert.Equal(expectedTime, presenter.Time); Assert.Equal(expectedTime, eventResult);
} }
[AvaloniaTheory] [AvaloniaTheory]

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);
}
}