Browse Source

Removed all nullability overrides from TimePickerPresenter and DatePickerPresenter (#19241)

Fixed obscure cases where NullReferenceException could be thrown if a template hasn't been applied yet, or where it provides only some optional items
Replaced repeated string literals with shared const values
Relaxed template part requirements: RepeatButton to Button, Rectancle to Control
pull/19439/head
Tom Edwards 6 months ago
committed by GitHub
parent
commit
995f8eda5d
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 357
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  2. 382
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

357
src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs

@ -1,6 +1,5 @@
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using System; using System;
@ -13,23 +12,23 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a date for a /// Defines the presenter used for selecting a date for a
/// <see cref="DatePicker"/> /// <see cref="DatePicker"/>
/// </summary> /// </summary>
[TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)] [TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)]
[TemplatePart("PART_DayDownButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.DayDownButtonName, typeof(Button))]
[TemplatePart("PART_DayHost", typeof(Panel), IsRequired = true)] [TemplatePart(TemplateItems.DayHostName, typeof(Panel), IsRequired = true)]
[TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart(TemplateItems.DaySelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_DayUpButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.DayUpButtonName, typeof(Button))]
[TemplatePart("PART_DismissButton", typeof(Button))] [TemplatePart(TemplateItems.DismissButtonName, typeof(Button))]
[TemplatePart("PART_FirstSpacer", typeof(Rectangle))] [TemplatePart(TemplateItems.FirstSpacerName, typeof(Control))]
[TemplatePart("PART_MonthDownButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.MonthDownButtonName, typeof(Button))]
[TemplatePart("PART_MonthHost", typeof(Panel), IsRequired = true)] [TemplatePart(TemplateItems.MonthHostName, typeof(Panel), IsRequired = true)]
[TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart(TemplateItems.MonthSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_MonthUpButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.MonthUpButtonName, typeof(Button))]
[TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] [TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle))] [TemplatePart(TemplateItems.SecondSpacerName, typeof(Control))]
[TemplatePart("PART_YearDownButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.YearDownButtonName, typeof(Button))]
[TemplatePart("PART_YearHost", typeof(Panel), IsRequired = true)] [TemplatePart(TemplateItems.YearHostName, typeof(Panel), IsRequired = true)]
[TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart(TemplateItems.YearSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_YearUpButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.YearUpButtonName, typeof(Button))]
public class DatePickerPresenter : PickerPresenterBase public class DatePickerPresenter : PickerPresenterBase
{ {
/// <summary> /// <summary>
@ -102,24 +101,61 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> YearVisibleProperty = public static readonly StyledProperty<bool> YearVisibleProperty =
DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>(); DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>();
// Template Items private struct TemplateItems
private Grid? _pickerContainer; {
private Button? _acceptButton; public Grid _pickerContainer;
private Button? _dismissButton; public const string PickerContainerName = "PART_PickerContainer";
private Rectangle? _spacer1;
private Rectangle? _spacer2; public Button _acceptButton;
private Panel? _monthHost; public const string AcceptButtonName = "PART_AcceptButton";
private Panel? _yearHost;
private Panel? _dayHost; public Button? _dismissButton;
private DateTimePickerPanel? _monthSelector; public const string DismissButtonName = "PART_DismissButton";
private DateTimePickerPanel? _yearSelector;
private DateTimePickerPanel? _daySelector; public Control? _firstSpacer;
private Button? _monthUpButton; public const string FirstSpacerName = "PART_FirstSpacer";
private Button? _dayUpButton;
private Button? _yearUpButton; public Control? _secondSpacer;
private Button? _monthDownButton; public const string SecondSpacerName = "PART_SecondSpacer";
private Button? _dayDownButton;
private Button? _yearDownButton; public Panel _monthHost;
public const string MonthHostName = "PART_MonthHost";
public Panel _yearHost;
public const string YearHostName = "PART_YearHost";
public Panel _dayHost;
public const string DayHostName = "PART_DayHost";
public DateTimePickerPanel _monthSelector;
public const string MonthSelectorName = "PART_MonthSelector";
public DateTimePickerPanel _yearSelector;
public const string YearSelectorName = "PART_YearSelector";
public DateTimePickerPanel _daySelector;
public const string DaySelectorName = "PART_DaySelector";
public Button? _monthUpButton;
public const string MonthUpButtonName = "PART_MonthUpButton";
public Button? _dayUpButton;
public const string DayUpButtonName = "PART_DayUpButton";
public Button? _yearUpButton;
public const string YearUpButtonName = "PART_YearUpButton";
public Button? _monthDownButton;
public const string MonthDownButtonName = "PART_MonthDownButton";
public Button? _dayDownButton;
public const string DayDownButtonName = "PART_DayDownButton";
public Button? _yearDownButton;
public const string YearDownButtonName = "PART_YearDownButton";
}
private TemplateItems? _templateItems;
private DateTimeOffset _syncDate; private DateTimeOffset _syncDate;
@ -235,69 +271,54 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs e) protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
// These are requirements, so throw if not found
_pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
_monthHost = e.NameScope.Get<Panel>("PART_MonthHost");
_dayHost = e.NameScope.Get<Panel>("PART_DayHost");
_yearHost = e.NameScope.Get<Panel>("PART_YearHost");
_monthSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MonthSelector"); _templateItems = new()
_monthSelector.SelectionChanged += OnMonthChanged;
_daySelector = e.NameScope.Get<DateTimePickerPanel>("PART_DaySelector");
_daySelector.SelectionChanged += OnDayChanged;
_yearSelector = e.NameScope.Get<DateTimePickerPanel>("PART_YearSelector");
_yearSelector.SelectionChanged += OnYearChanged;
_acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
_monthUpButton = e.NameScope.Find<RepeatButton>("PART_MonthUpButton");
if (_monthUpButton != null)
{
_monthUpButton.Click += OnSelectorButtonClick;
}
_monthDownButton = e.NameScope.Find<RepeatButton>("PART_MonthDownButton");
if (_monthDownButton != null)
{ {
_monthDownButton.Click += OnSelectorButtonClick; // These are requirements, so throw if not found
} _pickerContainer = e.NameScope.Get<Grid>(TemplateItems.PickerContainerName),
_monthHost = e.NameScope.Get<Panel>(TemplateItems.MonthHostName),
_dayHost = e.NameScope.Get<Panel>(TemplateItems.DayHostName),
_yearHost = e.NameScope.Get<Panel>(TemplateItems.YearHostName),
_monthSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.MonthSelectorName),
_daySelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.DaySelectorName),
_yearSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.YearSelectorName),
_acceptButton = e.NameScope.Get<Button>(TemplateItems.AcceptButtonName),
_monthUpButton = SelectorButton(TemplateItems.MonthUpButtonName, DateTimePickerPanelType.Month, SpinDirection.Decrease),
_monthDownButton = SelectorButton(TemplateItems.MonthDownButtonName, DateTimePickerPanelType.Month, SpinDirection.Increase),
_dayUpButton = SelectorButton(TemplateItems.DayUpButtonName, DateTimePickerPanelType.Day, SpinDirection.Decrease),
_dayDownButton = SelectorButton(TemplateItems.DayDownButtonName, DateTimePickerPanelType.Day, SpinDirection.Increase),
_yearUpButton = SelectorButton(TemplateItems.YearUpButtonName, DateTimePickerPanelType.Year, SpinDirection.Decrease),
_yearDownButton = SelectorButton(TemplateItems.YearDownButtonName, DateTimePickerPanelType.Year, SpinDirection.Increase),
_dismissButton = e.NameScope.Find<Button>(TemplateItems.DismissButtonName),
_firstSpacer = e.NameScope.Find<Control>(TemplateItems.FirstSpacerName),
_secondSpacer = e.NameScope.Find<Control>(TemplateItems.SecondSpacerName),
};
_dayUpButton = e.NameScope.Find<RepeatButton>("PART_DayUpButton"); _templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
if (_dayUpButton != null) _templateItems.Value._monthSelector.SelectionChanged += OnMonthChanged;
{ _templateItems.Value._daySelector.SelectionChanged += OnDayChanged;
_dayUpButton.Click += OnSelectorButtonClick; _templateItems.Value._yearSelector.SelectionChanged += OnYearChanged;
}
_dayDownButton = e.NameScope.Find<RepeatButton>("PART_DayDownButton");
if (_dayDownButton != null)
{
_dayDownButton.Click += OnSelectorButtonClick;
}
_yearUpButton = e.NameScope.Find<RepeatButton>("PART_YearUpButton"); if (_templateItems.Value._dismissButton is { } dismissButton)
if (_yearUpButton != null)
{ {
_yearUpButton.Click += OnSelectorButtonClick; dismissButton.Click += OnDismissButtonClicked;
}
_yearDownButton = e.NameScope.Find<RepeatButton>("PART_YearDownButton");
if (_yearDownButton != null)
{
_yearDownButton.Click += OnSelectorButtonClick;
} }
_dismissButton = e.NameScope.Find<Button>("PART_DismissButton"); InitPicker();
_spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
if (_acceptButton != null) Button? SelectorButton(string name, DateTimePickerPanelType type, SpinDirection direction)
{
_acceptButton.Click += OnAcceptButtonClicked;
}
if (_dismissButton != null)
{ {
_dismissButton.Click += OnDismissButtonClicked; if (e.NameScope.Find<Button>(name) is { } button)
{
button.Click += (s, e) => OnSelectorButtonClick(type, direction);
return button;
}
return null;
} }
InitPicker();
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -350,63 +371,63 @@ namespace Avalonia.Controls
private void InitPicker() private void InitPicker()
{ {
// OnApplyTemplate must've been called before we can init here... // OnApplyTemplate must've been called before we can init here...
if (_pickerContainer == null) if (_templateItems is not { } items)
return; return;
_suppressUpdateSelection = true; _suppressUpdateSelection = true;
_monthSelector!.MaximumValue = 12; items._monthSelector.MaximumValue = 12;
_monthSelector.MinimumValue = 1; items._monthSelector.MinimumValue = 1;
_monthSelector.ItemFormat = MonthFormat; items._monthSelector.ItemFormat = MonthFormat;
_daySelector!.ItemFormat = DayFormat; items._daySelector.ItemFormat = DayFormat;
_yearSelector!.MaximumValue = MaxYear.Year; items._yearSelector.MaximumValue = MaxYear.Year;
_yearSelector.MinimumValue = MinYear.Year; items._yearSelector.MinimumValue = MinYear.Year;
_yearSelector.ItemFormat = YearFormat; items._yearSelector.ItemFormat = YearFormat;
SetGrid(); SetGrid(items);
// Date should've been set when we reach this point // Date should've been set when we reach this point
var dt = Date; var dt = Date;
if (DayVisible) if (DayVisible)
{ {
_daySelector.FormatDate = dt.Date; items._daySelector.FormatDate = dt.Date;
var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month); var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month);
_daySelector.MaximumValue = maxDays; items._daySelector.MaximumValue = maxDays;
_daySelector.MinimumValue = 1; items._daySelector.MinimumValue = 1;
_daySelector.SelectedValue = dt.Day; items._daySelector.SelectedValue = dt.Day;
} }
if (MonthVisible) if (MonthVisible)
{ {
_monthSelector.SelectedValue = dt.Month; items._monthSelector.SelectedValue = dt.Month;
_monthSelector.FormatDate = dt.Date; items._monthSelector.FormatDate = dt.Date;
} }
if (YearVisible) if (YearVisible)
{ {
_yearSelector.SelectedValue = dt.Year; items._yearSelector.SelectedValue = dt.Year;
_yearSelector.FormatDate = dt.Date; items._yearSelector.FormatDate = dt.Date;
} }
_suppressUpdateSelection = false; _suppressUpdateSelection = false;
SetInitialFocus(); SetInitialFocus(items);
} }
private void SetGrid() private void SetGrid(TemplateItems items)
{ {
var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern; var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var columns = new List<(Panel?, int)> var columns = new List<(Panel?, int)>
{ {
(_monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1), (items._monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
(_yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1), (items._yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
(_dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1), (items._dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
}; };
columns.Sort((x, y) => x.Item2 - y.Item2); columns.Sort((x, y) => x.Item2 - y.Item2);
_pickerContainer!.ColumnDefinitions.Clear(); items._pickerContainer.ColumnDefinitions.Clear();
var columnIndex = 0; var columnIndex = 0;
@ -421,48 +442,53 @@ namespace Avalonia.Controls
{ {
if (columnIndex > 0) if (columnIndex > 0)
{ {
_pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto)); items._pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
} }
_pickerContainer.ColumnDefinitions.Add( items._pickerContainer.ColumnDefinitions.Add(
new ColumnDefinition(column.Item1 == _monthHost ? 138 : 78, GridUnitType.Star)); new ColumnDefinition(column.Item1 == items._monthHost ? 138 : 78, GridUnitType.Star));
if (column.Item1.Parent is null) if (column.Item1.Parent is null)
{ {
_pickerContainer.Children.Add(column.Item1); items._pickerContainer.Children.Add(column.Item1);
} }
Grid.SetColumn(column.Item1, (columnIndex++ * 2)); Grid.SetColumn(column.Item1, (columnIndex++ * 2));
} }
} }
var isSpacer1Visible = columnIndex > 1; ConfigureSpacer(items._firstSpacer, columnIndex > 1);
var isSpacer2Visible = columnIndex > 2; ConfigureSpacer(items._secondSpacer, columnIndex > 2);
// ternary conditional operator is used to make sure grid cells will be validated
Grid.SetColumn(_spacer1!, isSpacer1Visible ? 1 : 0); static void ConfigureSpacer(Control? spacer, bool visible)
Grid.SetColumn(_spacer2!, isSpacer2Visible ? 3 : 0); {
if (spacer == null)
return;
// ternary conditional operator is used to make sure grid cells will be validated
Grid.SetColumn(spacer, visible ? 1 : 0);
spacer.IsVisible = visible;
_spacer1!.IsVisible = isSpacer1Visible; }
_spacer2!.IsVisible = isSpacer2Visible;
} }
private void SetInitialFocus() private void SetInitialFocus(TemplateItems items)
{ {
int monthCol = MonthVisible ? Grid.GetColumn(_monthHost!) : int.MaxValue; int monthCol = MonthVisible ? Grid.GetColumn(items._monthHost) : int.MaxValue;
int dayCol = DayVisible ? Grid.GetColumn(_dayHost!) : int.MaxValue; int dayCol = DayVisible ? Grid.GetColumn(items._dayHost) : int.MaxValue;
int yearCol = YearVisible ? Grid.GetColumn(_yearHost!) : int.MaxValue; int yearCol = YearVisible ? Grid.GetColumn(items._yearHost) : int.MaxValue;
if (monthCol < dayCol && monthCol < yearCol) if (monthCol < dayCol && monthCol < yearCol)
{ {
_monthSelector?.Focus(NavigationMethod.Pointer); items._monthSelector.Focus(NavigationMethod.Pointer);
} }
else if (dayCol < monthCol && dayCol < yearCol) else if (dayCol < monthCol && dayCol < yearCol)
{ {
_monthSelector?.Focus(NavigationMethod.Pointer); items._monthSelector.Focus(NavigationMethod.Pointer);
} }
else if (yearCol < monthCol && yearCol < dayCol) else if (yearCol < monthCol && yearCol < dayCol)
{ {
_yearSelector?.Focus(NavigationMethod.Pointer); items._yearSelector.Focus(NavigationMethod.Pointer);
} }
} }
@ -477,29 +503,36 @@ namespace Avalonia.Controls
OnConfirmed(); OnConfirmed();
} }
private void OnSelectorButtonClick(object? sender, RoutedEventArgs e) private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
{ {
if (sender == _monthUpButton) var target = type switch
_monthSelector!.ScrollUp(); {
else if (sender == _monthDownButton) DateTimePickerPanelType.Month => _templateItems?._monthSelector,
_monthSelector!.ScrollDown(); DateTimePickerPanelType.Day => _templateItems?._daySelector,
else if (sender == _yearUpButton) DateTimePickerPanelType.Year=> _templateItems?._yearSelector,
_yearSelector!.ScrollUp(); _ => throw new NotImplementedException(),
else if (sender == _yearDownButton) };
_yearSelector!.ScrollDown();
else if (sender == _dayUpButton) switch (direction)
_daySelector!.ScrollUp(); {
else if (sender == _dayDownButton) case SpinDirection.Increase:
_daySelector!.ScrollDown(); target?.ScrollDown();
break;
case SpinDirection.Decrease:
target?.ScrollUp();
break;
default:
throw new NotImplementedException();
}
} }
private void OnYearChanged(object? sender, EventArgs e) private void OnYearChanged(object? sender, EventArgs e)
{ {
if (_suppressUpdateSelection) if (_suppressUpdateSelection || _templateItems is not { } items)
return; return;
int maxDays = _calendar.GetDaysInMonth(_yearSelector!.SelectedValue, _syncDate.Month); int maxDays = _calendar.GetDaysInMonth(items._yearSelector.SelectedValue, _syncDate.Month);
var newDate = new DateTimeOffset(_yearSelector.SelectedValue, _syncDate.Month, var newDate = new DateTimeOffset(items._yearSelector.SelectedValue, _syncDate.Month,
_syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset); _syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
_syncDate = newDate; _syncDate = newDate;
@ -510,30 +543,30 @@ namespace Avalonia.Controls
_suppressUpdateSelection = true; _suppressUpdateSelection = true;
_daySelector!.FormatDate = newDate.Date; items._daySelector.FormatDate = newDate.Date;
if (_daySelector.MaximumValue != maxDays) if (items._daySelector.MaximumValue != maxDays)
_daySelector.MaximumValue = maxDays; items._daySelector.MaximumValue = maxDays;
else else
_daySelector.RefreshItems(); items._daySelector.RefreshItems();
_suppressUpdateSelection = false; _suppressUpdateSelection = false;
} }
private void OnDayChanged(object? sender, EventArgs e) private void OnDayChanged(object? sender, EventArgs e)
{ {
if (_suppressUpdateSelection) if (_suppressUpdateSelection || _templateItems is not { } items)
return; return;
_syncDate = new DateTimeOffset(_syncDate.Year, _syncDate.Month, _daySelector!.SelectedValue, 0, 0, 0, _syncDate.Offset); _syncDate = new DateTimeOffset(_syncDate.Year, _syncDate.Month, items._daySelector.SelectedValue, 0, 0, 0, _syncDate.Offset);
} }
private void OnMonthChanged(object? sender, EventArgs e) private void OnMonthChanged(object? sender, EventArgs e)
{ {
if (_suppressUpdateSelection) if (_suppressUpdateSelection || _templateItems is not { } items)
return; return;
int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, _monthSelector!.SelectedValue); int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, items._monthSelector.SelectedValue);
var newDate = new DateTimeOffset(_syncDate.Year, _monthSelector.SelectedValue, var newDate = new DateTimeOffset(_syncDate.Year, items._monthSelector.SelectedValue,
_syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset); _syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
if (!DayVisible) if (!DayVisible)
@ -544,24 +577,24 @@ namespace Avalonia.Controls
_suppressUpdateSelection = true; _suppressUpdateSelection = true;
_daySelector!.FormatDate = newDate.Date; items._daySelector.FormatDate = newDate.Date;
_syncDate = newDate; _syncDate = newDate;
if (_daySelector.MaximumValue != maxDays) if (items._daySelector.MaximumValue != maxDays)
_daySelector.MaximumValue = maxDays; items._daySelector.MaximumValue = maxDays;
else else
_daySelector.RefreshItems(); items._daySelector.RefreshItems();
_suppressUpdateSelection = false; _suppressUpdateSelection = false;
} }
internal double GetOffsetForPopup() internal double GetOffsetForPopup()
{ {
if (_monthSelector is null) if (_templateItems is not { } items)
return 0; return 0;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41; var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_monthSelector.ItemHeight / 2); return -(MaxHeight - acceptDismissButtonHeight) / 2 - (items._monthSelector.ItemHeight / 2);
} }
} }
} }

382
src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

@ -1,9 +1,8 @@
using Avalonia.Controls.Metadata; using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using System;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -11,25 +10,25 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a time. Intended for use with /// Defines the presenter used for selecting a time. Intended for use with
/// <see cref="TimePicker"/> but can be used independently /// <see cref="TimePicker"/> but can be used independently
/// </summary> /// </summary>
[TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)] [TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)]
[TemplatePart("PART_DismissButton", typeof(Button))] [TemplatePart(TemplateItems.DismissButtonName, typeof(Button))]
[TemplatePart("PART_HourDownButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.HourDownButtonName, typeof(Button))]
[TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart(TemplateItems.HourSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_HourUpButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.HourUpButtonName, typeof(Button))]
[TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.MinuteDownButtonName, typeof(Button))]
[TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart(TemplateItems.MinuteSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.MinuteUpButtonName, typeof(Button))]
[TemplatePart("PART_SecondDownButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.SecondDownButtonName, typeof(Button))]
[TemplatePart("PART_SecondHost", typeof(Panel), IsRequired = true)] [TemplatePart(TemplateItems.SecondHostName, typeof(Panel))]
[TemplatePart("PART_SecondSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart(TemplateItems.SecondSelectorName, typeof(DateTimePickerPanel))]
[TemplatePart("PART_SecondUpButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.SecondUpButtonName, typeof(Button))]
[TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.PeriodDownButtonName, typeof(Button))]
[TemplatePart("PART_PeriodHost", typeof(Panel), IsRequired = true)] [TemplatePart(TemplateItems.PeriodHostName, typeof(Panel), IsRequired = true)]
[TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart(TemplateItems.PeriodSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))] [TemplatePart(TemplateItems.PeriodUpButtonName, typeof(Button))]
[TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] [TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle), IsRequired = true)] [TemplatePart(TemplateItems.SecondSpacerName, typeof(Control), IsRequired = true)]
[TemplatePart("PART_ThirdSpacer", typeof(Rectangle), IsRequired = true)] [TemplatePart(TemplateItems.ThirdSpacerName, typeof(Control))]
public class TimePickerPresenter : PickerPresenterBase public class TimePickerPresenter : PickerPresenterBase
{ {
/// <summary> /// <summary>
@ -37,7 +36,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public static readonly StyledProperty<int> MinuteIncrementProperty = public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>(); TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>();
/// <summary> /// <summary>
/// Defines the <see cref="SecondIncrement"/> property /// Defines the <see cref="SecondIncrement"/> property
/// </summary> /// </summary>
@ -49,7 +48,7 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public static readonly StyledProperty<string> ClockIdentifierProperty = public static readonly StyledProperty<string> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>(); TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>();
/// <summary> /// <summary>
/// Defines the <see cref="UseSeconds"/> property /// Defines the <see cref="UseSeconds"/> property
/// </summary> /// </summary>
@ -72,26 +71,54 @@ namespace Avalonia.Controls
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TimePickerPresenter>(KeyboardNavigationMode.Cycle); KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TimePickerPresenter>(KeyboardNavigationMode.Cycle);
} }
// TemplateItems private struct TemplateItems
private Grid? _pickerContainer; {
private Button? _acceptButton; public Grid _pickerContainer;
private Button? _dismissButton; public const string PickerContainerName = "PART_PickerContainer";
private Rectangle? _spacer2;
private Rectangle? _spacer3; public Button _acceptButton;
private Panel? _secondHost; public const string AcceptButtonName = "PART_AcceptButton";
private Panel? _periodHost; public Button? _dismissButton;
private DateTimePickerPanel? _hourSelector; public const string DismissButtonName = "PART_DismissButton";
private DateTimePickerPanel? _minuteSelector;
private DateTimePickerPanel? _secondSelector; public Control _secondSpacer; // the 2nd spacer, not seconds of time
private DateTimePickerPanel? _periodSelector; public const string SecondSpacerName = "PART_SecondSpacer";
private Button? _hourUpButton; public Control? _thirdSpacer;
private Button? _minuteUpButton; public const string ThirdSpacerName = "PART_ThirdSpacer";
private Button? _secondUpButton;
private Button? _periodUpButton; public Panel? _secondHost;
private Button? _hourDownButton; public const string SecondHostName = "PART_SecondHost";
private Button? _minuteDownButton; public Panel _periodHost;
private Button? _secondDownButton; public const string PeriodHostName = "PART_PeriodHost";
private Button? _periodDownButton;
public DateTimePickerPanel _hourSelector;
public const string HourSelectorName = "PART_HourSelector";
public DateTimePickerPanel _minuteSelector;
public const string MinuteSelectorName = "PART_MinuteSelector";
public DateTimePickerPanel? _secondSelector;
public const string SecondSelectorName = "PART_SecondSelector";
public DateTimePickerPanel _periodSelector;
public const string PeriodSelectorName = "PART_PeriodSelector";
public Button? _hourUpButton;
public const string HourUpButtonName = "PART_HourUpButton";
public Button? _minuteUpButton;
public const string MinuteUpButtonName = "PART_MinuteUpButton";
public Button? _secondUpButton;
public const string SecondUpButtonName = "PART_SecondUpButton";
public Button? _periodUpButton;
public const string PeriodUpButtonName = "PART_PeriodUpButton";
public Button? _hourDownButton;
public const string HourDownButtonName = "PART_HourDownButton";
public Button? _minuteDownButton;
public const string MinuteDownButtonName = "PART_MinuteDownButton";
public Button? _secondDownButton;
public const string SecondDownButtonName = "PART_SecondDownButton";
public Button? _periodDownButton;
public const string PeriodDownButtonName = "PART_PeriodDownButton";
}
private TemplateItems? _templateItems;
/// <summary> /// <summary>
/// Gets or sets the minute increment in the selector /// Gets or sets the minute increment in the selector
@ -101,7 +128,7 @@ namespace Avalonia.Controls
get => GetValue(MinuteIncrementProperty); get => GetValue(MinuteIncrementProperty);
set => SetValue(MinuteIncrementProperty, value); set => SetValue(MinuteIncrementProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the second increment in the selector /// Gets or sets the second increment in the selector
/// </summary> /// </summary>
@ -119,7 +146,7 @@ namespace Avalonia.Controls
get => GetValue(ClockIdentifierProperty); get => GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value); set => SetValue(ClockIdentifierProperty, value);
} }
/// <summary> /// <summary>
/// Gets or sets the current clock identifier, either 12HourClock or 24HourClock /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
/// </summary> /// </summary>
@ -142,54 +169,54 @@ namespace Avalonia.Controls
{ {
base.OnApplyTemplate(e); base.OnApplyTemplate(e);
_pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer"); _templateItems = new()
_periodHost = e.NameScope.Get<Panel>("PART_PeriodHost"); {
_secondHost = e.NameScope.Find<Panel>("PART_SecondHost"); _pickerContainer = e.NameScope.Get<Grid>(TemplateItems.PickerContainerName),
_periodHost = e.NameScope.Get<Panel>(TemplateItems.PeriodHostName),
_hourSelector = e.NameScope.Get<DateTimePickerPanel>("PART_HourSelector"); _secondHost = e.NameScope.Find<Panel>(TemplateItems.SecondHostName),
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MinuteSelector");
_secondSelector = e.NameScope.Find<DateTimePickerPanel>("PART_SecondSelector"); _hourSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.HourSelectorName),
_periodSelector = e.NameScope.Get<DateTimePickerPanel>("PART_PeriodSelector"); _minuteSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.MinuteSelectorName),
_secondSelector = e.NameScope.Find<DateTimePickerPanel>(TemplateItems.SecondSelectorName),
_spacer2 = e.NameScope.Get<Rectangle>("PART_SecondSpacer"); _periodSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.PeriodSelectorName),
_spacer3 = e.NameScope.Find<Rectangle>("PART_ThirdSpacer");
_secondSpacer = e.NameScope.Get<Control>(TemplateItems.SecondSpacerName),
_acceptButton = e.NameScope.Get<Button>("PART_AcceptButton"); _thirdSpacer = e.NameScope.Find<Control>(TemplateItems.ThirdSpacerName),
_acceptButton.Click += OnAcceptButtonClicked;
_acceptButton = e.NameScope.Get<Button>(TemplateItems.AcceptButtonName),
_hourUpButton = e.NameScope.Find<RepeatButton>("PART_HourUpButton");
if (_hourUpButton != null) _hourUpButton = SelectorButton(TemplateItems.HourUpButtonName, DateTimePickerPanelType.Hour, SpinDirection.Decrease),
_hourUpButton.Click += OnSelectorButtonClick; _hourDownButton = SelectorButton(TemplateItems.HourDownButtonName, DateTimePickerPanelType.Hour, SpinDirection.Increase),
_hourDownButton = e.NameScope.Find<RepeatButton>("PART_HourDownButton");
if (_hourDownButton != null) _minuteUpButton = SelectorButton(TemplateItems.MinuteUpButtonName, DateTimePickerPanelType.Minute, SpinDirection.Decrease),
_hourDownButton.Click += OnSelectorButtonClick; _minuteDownButton = SelectorButton(TemplateItems.MinuteDownButtonName, DateTimePickerPanelType.Minute, SpinDirection.Increase),
_minuteUpButton = e.NameScope.Find<RepeatButton>("PART_MinuteUpButton"); _secondUpButton = SelectorButton(TemplateItems.SecondUpButtonName, DateTimePickerPanelType.Second, SpinDirection.Decrease),
if (_minuteUpButton != null) _secondDownButton = SelectorButton(TemplateItems.SecondDownButtonName, DateTimePickerPanelType.Second, SpinDirection.Increase),
_minuteUpButton.Click += OnSelectorButtonClick;
_minuteDownButton = e.NameScope.Find<RepeatButton>("PART_MinuteDownButton"); _periodUpButton = SelectorButton(TemplateItems.PeriodUpButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Decrease),
if (_minuteDownButton != null) _periodDownButton = SelectorButton(TemplateItems.PeriodDownButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Increase),
_minuteDownButton.Click += OnSelectorButtonClick;
_dismissButton = e.NameScope.Find<Button>(TemplateItems.DismissButtonName),
_secondUpButton = e.NameScope.Find<RepeatButton>("PART_SecondUpButton"); };
if (_secondUpButton != null)
_secondUpButton.Click += OnSelectorButtonClick; _templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
_secondDownButton = e.NameScope.Find<RepeatButton>("PART_SecondDownButton"); if (_templateItems.Value._dismissButton is { } dismissButton)
if (_secondDownButton != null) {
_secondDownButton.Click += OnSelectorButtonClick; dismissButton.Click += OnDismissButtonClicked;
}
_periodUpButton = e.NameScope.Find<RepeatButton>("PART_PeriodUpButton");
if (_periodUpButton != null)
_periodUpButton.Click += OnSelectorButtonClick;
_periodDownButton = e.NameScope.Find<RepeatButton>("PART_PeriodDownButton");
if (_periodDownButton != null)
_periodDownButton.Click += OnSelectorButtonClick;
_dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
if (_dismissButton != null)
_dismissButton.Click += OnDismissButtonClicked;
InitPicker(); InitPicker();
Button? SelectorButton(string name, DateTimePickerPanelType type, SpinDirection direction)
{
if (e.NameScope.Find<Button>(name) is { } button)
{
button.Click += (s, e) => OnSelectorButtonClick(type, direction);
return button;
}
return null;
}
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -232,100 +259,105 @@ namespace Avalonia.Controls
protected override void OnConfirmed() protected override void OnConfirmed()
{ {
var hr = _hourSelector!.SelectedValue; if (_templateItems is { } items)
var min = _minuteSelector!.SelectedValue;
var sec = _secondSelector?.SelectedValue ?? 0;
var per = _periodSelector!.SelectedValue;
if (ClockIdentifier == "12HourClock")
{ {
hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr; var hr = items._hourSelector.SelectedValue;
} var min = items._minuteSelector.SelectedValue;
var sec = items._secondSelector?.SelectedValue ?? 0;
var per = items._periodSelector.SelectedValue;
SetCurrentValue(TimeProperty, new TimeSpan(hr, min, UseSeconds ? sec : 0)); if (ClockIdentifier == "12HourClock")
{
hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr;
}
SetCurrentValue(TimeProperty, new TimeSpan(hr, min, UseSeconds ? sec : 0));
}
base.OnConfirmed(); base.OnConfirmed();
} }
private void InitPicker() private void InitPicker()
{ {
if (_pickerContainer == null) if (_templateItems is not { } items)
return; return;
bool clock12 = ClockIdentifier == "12HourClock"; bool clock12 = ClockIdentifier == "12HourClock";
_hourSelector!.MaximumValue = clock12 ? 12 : 23; items._hourSelector.MaximumValue = clock12 ? 12 : 23;
_hourSelector.MinimumValue = clock12 ? 1 : 0; items._hourSelector.MinimumValue = clock12 ? 1 : 0;
_hourSelector.ItemFormat = "%h"; items._hourSelector.ItemFormat = "%h";
var hr = Time.Hours; var hr = Time.Hours;
_hourSelector.SelectedValue = !clock12 ? hr : items._hourSelector.SelectedValue = !clock12 ? hr :
hr > 12 ? hr - 12 : hr == 0 ? 12 : hr; hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
_minuteSelector!.MaximumValue = 59; items._minuteSelector.MaximumValue = 59;
_minuteSelector.MinimumValue = 0; items._minuteSelector.MinimumValue = 0;
_minuteSelector.Increment = MinuteIncrement; items._minuteSelector.Increment = MinuteIncrement;
_minuteSelector.ItemFormat = "mm"; items._minuteSelector.ItemFormat = "mm";
_minuteSelector.SelectedValue = Time.Minutes; items._minuteSelector.SelectedValue = Time.Minutes;
if (_secondSelector is not null) if (items._secondSelector is { } secondSelector)
{ {
_secondSelector.MaximumValue = 59; secondSelector.MaximumValue = 59;
_secondSelector.MinimumValue = 0; secondSelector.MinimumValue = 0;
_secondSelector.Increment = SecondIncrement; secondSelector.Increment = SecondIncrement;
_secondSelector.ItemFormat = "ss"; secondSelector.ItemFormat = "ss";
_secondSelector.SelectedValue = Time.Seconds; secondSelector.SelectedValue = Time.Seconds;
} }
_periodSelector!.MaximumValue = 1; items._periodSelector.MaximumValue = 1;
_periodSelector.MinimumValue = 0; items._periodSelector.MinimumValue = 0;
_periodSelector.SelectedValue = hr >= 12 ? 1 : 0; items._periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
SetGrid(); SetGrid(items);
_hourSelector?.Focus(NavigationMethod.Pointer); items._hourSelector.Focus(NavigationMethod.Pointer);
} }
private void SetGrid() private void SetGrid(TemplateItems items)
{ {
var use24HourClock = ClockIdentifier == "24HourClock"; var use24HourClock = ClockIdentifier == "24HourClock";
var canUseSeconds = _secondHost is not null && _spacer3 is not null;
var columnsD = new ColumnDefinitions();
columnsD.Add(new ColumnDefinition(GridLength.Star));
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
if (canUseSeconds && UseSeconds)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
}
if (!use24HourClock)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
}
_pickerContainer!.ColumnDefinitions = columnsD; var columnsD = new ColumnDefinitions
{
new(GridLength.Star),
new(GridLength.Auto),
new(GridLength.Star)
};
if (canUseSeconds) if (items._secondHost is not null && items._thirdSpacer is not null)
{ {
_spacer2!.IsVisible = UseSeconds; if (UseSeconds)
_secondHost!.IsVisible = UseSeconds; {
_spacer3!.IsVisible = !use24HourClock; columnsD.Add(new ColumnDefinition(GridLength.Auto));
_periodHost!.IsVisible = !use24HourClock; columnsD.Add(new ColumnDefinition(GridLength.Star));
}
items._secondSpacer.IsVisible = UseSeconds;
items._secondHost.IsVisible = UseSeconds;
items._thirdSpacer.IsVisible = !use24HourClock;
items._periodHost.IsVisible = !use24HourClock;
var amPmColumn = (UseSeconds) ? 6 : 4; var amPmColumn = (UseSeconds) ? 6 : 4;
Grid.SetColumn(_spacer2, UseSeconds ? 3 : 0); Grid.SetColumn(items._secondSpacer, UseSeconds ? 3 : 0);
Grid.SetColumn(_secondHost, UseSeconds ? 4 : 0); Grid.SetColumn(items._secondHost, UseSeconds ? 4 : 0);
Grid.SetColumn(_spacer3, use24HourClock ? 0 : amPmColumn-1); Grid.SetColumn(items._thirdSpacer, use24HourClock ? 0 : amPmColumn - 1);
Grid.SetColumn(_periodHost, use24HourClock ? 0 : amPmColumn); Grid.SetColumn(items._periodHost, use24HourClock ? 0 : amPmColumn);
} }
else else
{ {
_spacer2!.IsVisible = !use24HourClock; items._secondSpacer.IsVisible = !use24HourClock;
_periodHost!.IsVisible = !use24HourClock; items._periodHost.IsVisible = !use24HourClock;
Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3); Grid.SetColumn(items._secondSpacer, use24HourClock ? 0 : 3);
Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4); Grid.SetColumn(items._periodHost, use24HourClock ? 0 : 4);
} }
if (!use24HourClock)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
columnsD.Add(new ColumnDefinition(GridLength.Star));
}
items._pickerContainer.ColumnDefinitions = columnsD;
} }
private void OnDismissButtonClicked(object? sender, RoutedEventArgs e) private void OnDismissButtonClicked(object? sender, RoutedEventArgs e)
@ -338,33 +370,37 @@ namespace Avalonia.Controls
OnConfirmed(); OnConfirmed();
} }
private void OnSelectorButtonClick(object? sender, RoutedEventArgs e) private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
{ {
if (sender == _hourUpButton) var target = type switch
_hourSelector!.ScrollUp(); {
else if (sender == _hourDownButton) DateTimePickerPanelType.Hour => _templateItems?._hourSelector,
_hourSelector!.ScrollDown(); DateTimePickerPanelType.Minute => _templateItems?._minuteSelector,
else if (sender == _minuteUpButton) DateTimePickerPanelType.Second => _templateItems?._secondSelector,
_minuteSelector!.ScrollUp(); DateTimePickerPanelType.TimePeriod => _templateItems?._periodSelector,
else if (sender == _minuteDownButton) _ => throw new NotImplementedException(),
_minuteSelector!.ScrollDown(); };
else if (sender == _secondUpButton)
_secondSelector!.ScrollUp(); switch (direction)
else if (sender == _secondDownButton) {
_secondSelector!.ScrollDown(); case SpinDirection.Increase:
else if (sender == _periodUpButton) target?.ScrollDown();
_periodSelector!.ScrollUp(); break;
else if (sender == _periodDownButton) case SpinDirection.Decrease:
_periodSelector!.ScrollDown(); target?.ScrollUp();
break;
default:
throw new NotImplementedException();
}
} }
internal double GetOffsetForPopup() internal double GetOffsetForPopup()
{ {
if (_hourSelector is null) if (_templateItems is not { } items)
return 0; return 0;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41; var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector.ItemHeight / 2); return -(MaxHeight - acceptDismissButtonHeight) / 2 - (items._hourSelector.ItemHeight / 2);
} }
} }
} }

Loading…
Cancel
Save