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
release/11.3.4
Tom Edwards 6 months ago
committed by Julien Lebosquain
parent
commit
862aee2b02
  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.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using System;
@ -13,23 +12,23 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a date for a
/// <see cref="DatePicker"/>
/// </summary>
[TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)]
[TemplatePart("PART_DayDownButton", typeof(RepeatButton))]
[TemplatePart("PART_DayHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_DaySelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_DayUpButton", typeof(RepeatButton))]
[TemplatePart("PART_DismissButton", typeof(Button))]
[TemplatePart("PART_FirstSpacer", typeof(Rectangle))]
[TemplatePart("PART_MonthDownButton", typeof(RepeatButton))]
[TemplatePart("PART_MonthHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_MonthSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_MonthUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle))]
[TemplatePart("PART_YearDownButton", typeof(RepeatButton))]
[TemplatePart("PART_YearHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_YearSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_YearUpButton", typeof(RepeatButton))]
[TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)]
[TemplatePart(TemplateItems.DayDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.DayHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.DaySelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.DayUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.DismissButtonName, typeof(Button))]
[TemplatePart(TemplateItems.FirstSpacerName, typeof(Control))]
[TemplatePart(TemplateItems.MonthDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.MonthHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.MonthSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.MonthUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)]
[TemplatePart(TemplateItems.SecondSpacerName, typeof(Control))]
[TemplatePart(TemplateItems.YearDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.YearHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.YearSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.YearUpButtonName, typeof(Button))]
public class DatePickerPresenter : PickerPresenterBase
{
/// <summary>
@ -102,24 +101,61 @@ namespace Avalonia.Controls
public static readonly StyledProperty<bool> YearVisibleProperty =
DatePicker.YearVisibleProperty.AddOwner<DatePickerPresenter>();
// Template Items
private Grid? _pickerContainer;
private Button? _acceptButton;
private Button? _dismissButton;
private Rectangle? _spacer1;
private Rectangle? _spacer2;
private Panel? _monthHost;
private Panel? _yearHost;
private Panel? _dayHost;
private DateTimePickerPanel? _monthSelector;
private DateTimePickerPanel? _yearSelector;
private DateTimePickerPanel? _daySelector;
private Button? _monthUpButton;
private Button? _dayUpButton;
private Button? _yearUpButton;
private Button? _monthDownButton;
private Button? _dayDownButton;
private Button? _yearDownButton;
private struct TemplateItems
{
public Grid _pickerContainer;
public const string PickerContainerName = "PART_PickerContainer";
public Button _acceptButton;
public const string AcceptButtonName = "PART_AcceptButton";
public Button? _dismissButton;
public const string DismissButtonName = "PART_DismissButton";
public Control? _firstSpacer;
public const string FirstSpacerName = "PART_FirstSpacer";
public Control? _secondSpacer;
public const string SecondSpacerName = "PART_SecondSpacer";
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;
@ -235,69 +271,54 @@ namespace Avalonia.Controls
protected override void OnApplyTemplate(TemplateAppliedEventArgs 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");
_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)
_templateItems = new()
{
_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");
if (_dayUpButton != null)
{
_dayUpButton.Click += OnSelectorButtonClick;
}
_dayDownButton = e.NameScope.Find<RepeatButton>("PART_DayDownButton");
if (_dayDownButton != null)
{
_dayDownButton.Click += OnSelectorButtonClick;
}
_templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
_templateItems.Value._monthSelector.SelectionChanged += OnMonthChanged;
_templateItems.Value._daySelector.SelectionChanged += OnDayChanged;
_templateItems.Value._yearSelector.SelectionChanged += OnYearChanged;
_yearUpButton = e.NameScope.Find<RepeatButton>("PART_YearUpButton");
if (_yearUpButton != null)
if (_templateItems.Value._dismissButton is { } dismissButton)
{
_yearUpButton.Click += OnSelectorButtonClick;
}
_yearDownButton = e.NameScope.Find<RepeatButton>("PART_YearDownButton");
if (_yearDownButton != null)
{
_yearDownButton.Click += OnSelectorButtonClick;
dismissButton.Click += OnDismissButtonClicked;
}
_dismissButton = e.NameScope.Find<Button>("PART_DismissButton");
_spacer1 = e.NameScope.Find<Rectangle>("PART_FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("PART_SecondSpacer");
InitPicker();
if (_acceptButton != null)
{
_acceptButton.Click += OnAcceptButtonClicked;
}
if (_dismissButton != null)
Button? SelectorButton(string name, DateTimePickerPanelType type, SpinDirection direction)
{
_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)
@ -350,63 +371,63 @@ namespace Avalonia.Controls
private void InitPicker()
{
// OnApplyTemplate must've been called before we can init here...
if (_pickerContainer == null)
if (_templateItems is not { } items)
return;
_suppressUpdateSelection = true;
_monthSelector!.MaximumValue = 12;
_monthSelector.MinimumValue = 1;
_monthSelector.ItemFormat = MonthFormat;
items._monthSelector.MaximumValue = 12;
items._monthSelector.MinimumValue = 1;
items._monthSelector.ItemFormat = MonthFormat;
_daySelector!.ItemFormat = DayFormat;
items._daySelector.ItemFormat = DayFormat;
_yearSelector!.MaximumValue = MaxYear.Year;
_yearSelector.MinimumValue = MinYear.Year;
_yearSelector.ItemFormat = YearFormat;
items._yearSelector.MaximumValue = MaxYear.Year;
items._yearSelector.MinimumValue = MinYear.Year;
items._yearSelector.ItemFormat = YearFormat;
SetGrid();
SetGrid(items);
// Date should've been set when we reach this point
var dt = Date;
if (DayVisible)
{
_daySelector.FormatDate = dt.Date;
items._daySelector.FormatDate = dt.Date;
var maxDays = _calendar.GetDaysInMonth(dt.Year, dt.Month);
_daySelector.MaximumValue = maxDays;
_daySelector.MinimumValue = 1;
_daySelector.SelectedValue = dt.Day;
items._daySelector.MaximumValue = maxDays;
items._daySelector.MinimumValue = 1;
items._daySelector.SelectedValue = dt.Day;
}
if (MonthVisible)
{
_monthSelector.SelectedValue = dt.Month;
_monthSelector.FormatDate = dt.Date;
items._monthSelector.SelectedValue = dt.Month;
items._monthSelector.FormatDate = dt.Date;
}
if (YearVisible)
{
_yearSelector.SelectedValue = dt.Year;
_yearSelector.FormatDate = dt.Date;
items._yearSelector.SelectedValue = dt.Year;
items._yearSelector.FormatDate = dt.Date;
}
_suppressUpdateSelection = false;
SetInitialFocus();
SetInitialFocus(items);
}
private void SetGrid()
private void SetGrid(TemplateItems items)
{
var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var columns = new List<(Panel?, int)>
{
(_monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
(_yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
(_dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
(items._monthHost, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
(items._yearHost, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
(items._dayHost, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
};
columns.Sort((x, y) => x.Item2 - y.Item2);
_pickerContainer!.ColumnDefinitions.Clear();
items._pickerContainer.ColumnDefinitions.Clear();
var columnIndex = 0;
@ -421,48 +442,53 @@ namespace Avalonia.Controls
{
if (columnIndex > 0)
{
_pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
items._pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
}
_pickerContainer.ColumnDefinitions.Add(
new ColumnDefinition(column.Item1 == _monthHost ? 138 : 78, GridUnitType.Star));
items._pickerContainer.ColumnDefinitions.Add(
new ColumnDefinition(column.Item1 == items._monthHost ? 138 : 78, GridUnitType.Star));
if (column.Item1.Parent is null)
{
_pickerContainer.Children.Add(column.Item1);
items._pickerContainer.Children.Add(column.Item1);
}
Grid.SetColumn(column.Item1, (columnIndex++ * 2));
}
}
var isSpacer1Visible = columnIndex > 1;
var isSpacer2Visible = columnIndex > 2;
// ternary conditional operator is used to make sure grid cells will be validated
Grid.SetColumn(_spacer1!, isSpacer1Visible ? 1 : 0);
Grid.SetColumn(_spacer2!, isSpacer2Visible ? 3 : 0);
ConfigureSpacer(items._firstSpacer, columnIndex > 1);
ConfigureSpacer(items._secondSpacer, columnIndex > 2);
static void ConfigureSpacer(Control? spacer, bool visible)
{
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 dayCol = DayVisible ? Grid.GetColumn(_dayHost!) : int.MaxValue;
int yearCol = YearVisible ? Grid.GetColumn(_yearHost!) : int.MaxValue;
int monthCol = MonthVisible ? Grid.GetColumn(items._monthHost) : int.MaxValue;
int dayCol = DayVisible ? Grid.GetColumn(items._dayHost) : int.MaxValue;
int yearCol = YearVisible ? Grid.GetColumn(items._yearHost) : int.MaxValue;
if (monthCol < dayCol && monthCol < yearCol)
{
_monthSelector?.Focus(NavigationMethod.Pointer);
items._monthSelector.Focus(NavigationMethod.Pointer);
}
else if (dayCol < monthCol && dayCol < yearCol)
{
_monthSelector?.Focus(NavigationMethod.Pointer);
items._monthSelector.Focus(NavigationMethod.Pointer);
}
else if (yearCol < monthCol && yearCol < dayCol)
{
_yearSelector?.Focus(NavigationMethod.Pointer);
items._yearSelector.Focus(NavigationMethod.Pointer);
}
}
@ -477,29 +503,36 @@ namespace Avalonia.Controls
OnConfirmed();
}
private void OnSelectorButtonClick(object? sender, RoutedEventArgs e)
private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
{
if (sender == _monthUpButton)
_monthSelector!.ScrollUp();
else if (sender == _monthDownButton)
_monthSelector!.ScrollDown();
else if (sender == _yearUpButton)
_yearSelector!.ScrollUp();
else if (sender == _yearDownButton)
_yearSelector!.ScrollDown();
else if (sender == _dayUpButton)
_daySelector!.ScrollUp();
else if (sender == _dayDownButton)
_daySelector!.ScrollDown();
var target = type switch
{
DateTimePickerPanelType.Month => _templateItems?._monthSelector,
DateTimePickerPanelType.Day => _templateItems?._daySelector,
DateTimePickerPanelType.Year=> _templateItems?._yearSelector,
_ => throw new NotImplementedException(),
};
switch (direction)
{
case SpinDirection.Increase:
target?.ScrollDown();
break;
case SpinDirection.Decrease:
target?.ScrollUp();
break;
default:
throw new NotImplementedException();
}
}
private void OnYearChanged(object? sender, EventArgs e)
{
if (_suppressUpdateSelection)
if (_suppressUpdateSelection || _templateItems is not { } items)
return;
int maxDays = _calendar.GetDaysInMonth(_yearSelector!.SelectedValue, _syncDate.Month);
var newDate = new DateTimeOffset(_yearSelector.SelectedValue, _syncDate.Month,
int maxDays = _calendar.GetDaysInMonth(items._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 = newDate;
@ -510,30 +543,30 @@ namespace Avalonia.Controls
_suppressUpdateSelection = true;
_daySelector!.FormatDate = newDate.Date;
items._daySelector.FormatDate = newDate.Date;
if (_daySelector.MaximumValue != maxDays)
_daySelector.MaximumValue = maxDays;
if (items._daySelector.MaximumValue != maxDays)
items._daySelector.MaximumValue = maxDays;
else
_daySelector.RefreshItems();
items._daySelector.RefreshItems();
_suppressUpdateSelection = false;
}
private void OnDayChanged(object? sender, EventArgs e)
{
if (_suppressUpdateSelection)
if (_suppressUpdateSelection || _templateItems is not { } items)
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)
{
if (_suppressUpdateSelection)
if (_suppressUpdateSelection || _templateItems is not { } items)
return;
int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, _monthSelector!.SelectedValue);
var newDate = new DateTimeOffset(_syncDate.Year, _monthSelector.SelectedValue,
int maxDays = _calendar.GetDaysInMonth(_syncDate.Year, items._monthSelector.SelectedValue);
var newDate = new DateTimeOffset(_syncDate.Year, items._monthSelector.SelectedValue,
_syncDate.Day > maxDays ? maxDays : _syncDate.Day, 0, 0, 0, _syncDate.Offset);
if (!DayVisible)
@ -544,24 +577,24 @@ namespace Avalonia.Controls
_suppressUpdateSelection = true;
_daySelector!.FormatDate = newDate.Date;
items._daySelector.FormatDate = newDate.Date;
_syncDate = newDate;
if (_daySelector.MaximumValue != maxDays)
_daySelector.MaximumValue = maxDays;
if (items._daySelector.MaximumValue != maxDays)
items._daySelector.MaximumValue = maxDays;
else
_daySelector.RefreshItems();
items._daySelector.RefreshItems();
_suppressUpdateSelection = false;
}
internal double GetOffsetForPopup()
{
if (_monthSelector is null)
if (_templateItems is not { } items)
return 0;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_monthSelector.ItemHeight / 2);
var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
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.Shapes;
using Avalonia.Input;
using Avalonia.Interactivity;
using System;
namespace Avalonia.Controls
{
@ -11,25 +10,25 @@ namespace Avalonia.Controls
/// Defines the presenter used for selecting a time. Intended for use with
/// <see cref="TimePicker"/> but can be used independently
/// </summary>
[TemplatePart("PART_AcceptButton", typeof(Button), IsRequired = true)]
[TemplatePart("PART_DismissButton", typeof(Button))]
[TemplatePart("PART_HourDownButton", typeof(RepeatButton))]
[TemplatePart("PART_HourSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_HourUpButton", typeof(RepeatButton))]
[TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))]
[TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))]
[TemplatePart("PART_SecondDownButton", typeof(RepeatButton))]
[TemplatePart("PART_SecondHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_SecondSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_SecondUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))]
[TemplatePart("PART_PeriodHost", typeof(Panel), IsRequired = true)]
[TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))]
[TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)]
[TemplatePart("PART_SecondSpacer", typeof(Rectangle), IsRequired = true)]
[TemplatePart("PART_ThirdSpacer", typeof(Rectangle), IsRequired = true)]
[TemplatePart(TemplateItems.AcceptButtonName, typeof(Button), IsRequired = true)]
[TemplatePart(TemplateItems.DismissButtonName, typeof(Button))]
[TemplatePart(TemplateItems.HourDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.HourSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.HourUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.MinuteDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.MinuteSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.MinuteUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.SecondDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.SecondHostName, typeof(Panel))]
[TemplatePart(TemplateItems.SecondSelectorName, typeof(DateTimePickerPanel))]
[TemplatePart(TemplateItems.SecondUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PeriodDownButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PeriodHostName, typeof(Panel), IsRequired = true)]
[TemplatePart(TemplateItems.PeriodSelectorName, typeof(DateTimePickerPanel), IsRequired = true)]
[TemplatePart(TemplateItems.PeriodUpButtonName, typeof(Button))]
[TemplatePart(TemplateItems.PickerContainerName, typeof(Grid), IsRequired = true)]
[TemplatePart(TemplateItems.SecondSpacerName, typeof(Control), IsRequired = true)]
[TemplatePart(TemplateItems.ThirdSpacerName, typeof(Control))]
public class TimePickerPresenter : PickerPresenterBase
{
/// <summary>
@ -37,7 +36,7 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="SecondIncrement"/> property
/// </summary>
@ -49,7 +48,7 @@ namespace Avalonia.Controls
/// </summary>
public static readonly StyledProperty<string> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>();
/// <summary>
/// Defines the <see cref="UseSeconds"/> property
/// </summary>
@ -72,26 +71,54 @@ namespace Avalonia.Controls
KeyboardNavigation.TabNavigationProperty.OverrideDefaultValue<TimePickerPresenter>(KeyboardNavigationMode.Cycle);
}
// TemplateItems
private Grid? _pickerContainer;
private Button? _acceptButton;
private Button? _dismissButton;
private Rectangle? _spacer2;
private Rectangle? _spacer3;
private Panel? _secondHost;
private Panel? _periodHost;
private DateTimePickerPanel? _hourSelector;
private DateTimePickerPanel? _minuteSelector;
private DateTimePickerPanel? _secondSelector;
private DateTimePickerPanel? _periodSelector;
private Button? _hourUpButton;
private Button? _minuteUpButton;
private Button? _secondUpButton;
private Button? _periodUpButton;
private Button? _hourDownButton;
private Button? _minuteDownButton;
private Button? _secondDownButton;
private Button? _periodDownButton;
private struct TemplateItems
{
public Grid _pickerContainer;
public const string PickerContainerName = "PART_PickerContainer";
public Button _acceptButton;
public const string AcceptButtonName = "PART_AcceptButton";
public Button? _dismissButton;
public const string DismissButtonName = "PART_DismissButton";
public Control _secondSpacer; // the 2nd spacer, not seconds of time
public const string SecondSpacerName = "PART_SecondSpacer";
public Control? _thirdSpacer;
public const string ThirdSpacerName = "PART_ThirdSpacer";
public Panel? _secondHost;
public const string SecondHostName = "PART_SecondHost";
public Panel _periodHost;
public const string PeriodHostName = "PART_PeriodHost";
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>
/// Gets or sets the minute increment in the selector
@ -101,7 +128,7 @@ namespace Avalonia.Controls
get => GetValue(MinuteIncrementProperty);
set => SetValue(MinuteIncrementProperty, value);
}
/// <summary>
/// Gets or sets the second increment in the selector
/// </summary>
@ -119,7 +146,7 @@ namespace Avalonia.Controls
get => GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value);
}
/// <summary>
/// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
/// </summary>
@ -142,54 +169,54 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
_periodHost = e.NameScope.Get<Panel>("PART_PeriodHost");
_secondHost = e.NameScope.Find<Panel>("PART_SecondHost");
_hourSelector = e.NameScope.Get<DateTimePickerPanel>("PART_HourSelector");
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MinuteSelector");
_secondSelector = e.NameScope.Find<DateTimePickerPanel>("PART_SecondSelector");
_periodSelector = e.NameScope.Get<DateTimePickerPanel>("PART_PeriodSelector");
_spacer2 = e.NameScope.Get<Rectangle>("PART_SecondSpacer");
_spacer3 = e.NameScope.Find<Rectangle>("PART_ThirdSpacer");
_acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
_acceptButton.Click += OnAcceptButtonClicked;
_hourUpButton = e.NameScope.Find<RepeatButton>("PART_HourUpButton");
if (_hourUpButton != null)
_hourUpButton.Click += OnSelectorButtonClick;
_hourDownButton = e.NameScope.Find<RepeatButton>("PART_HourDownButton");
if (_hourDownButton != null)
_hourDownButton.Click += OnSelectorButtonClick;
_minuteUpButton = e.NameScope.Find<RepeatButton>("PART_MinuteUpButton");
if (_minuteUpButton != null)
_minuteUpButton.Click += OnSelectorButtonClick;
_minuteDownButton = e.NameScope.Find<RepeatButton>("PART_MinuteDownButton");
if (_minuteDownButton != null)
_minuteDownButton.Click += OnSelectorButtonClick;
_secondUpButton = e.NameScope.Find<RepeatButton>("PART_SecondUpButton");
if (_secondUpButton != null)
_secondUpButton.Click += OnSelectorButtonClick;
_secondDownButton = e.NameScope.Find<RepeatButton>("PART_SecondDownButton");
if (_secondDownButton != null)
_secondDownButton.Click += OnSelectorButtonClick;
_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;
_templateItems = new()
{
_pickerContainer = e.NameScope.Get<Grid>(TemplateItems.PickerContainerName),
_periodHost = e.NameScope.Get<Panel>(TemplateItems.PeriodHostName),
_secondHost = e.NameScope.Find<Panel>(TemplateItems.SecondHostName),
_hourSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.HourSelectorName),
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.MinuteSelectorName),
_secondSelector = e.NameScope.Find<DateTimePickerPanel>(TemplateItems.SecondSelectorName),
_periodSelector = e.NameScope.Get<DateTimePickerPanel>(TemplateItems.PeriodSelectorName),
_secondSpacer = e.NameScope.Get<Control>(TemplateItems.SecondSpacerName),
_thirdSpacer = e.NameScope.Find<Control>(TemplateItems.ThirdSpacerName),
_acceptButton = e.NameScope.Get<Button>(TemplateItems.AcceptButtonName),
_hourUpButton = SelectorButton(TemplateItems.HourUpButtonName, DateTimePickerPanelType.Hour, SpinDirection.Decrease),
_hourDownButton = SelectorButton(TemplateItems.HourDownButtonName, DateTimePickerPanelType.Hour, SpinDirection.Increase),
_minuteUpButton = SelectorButton(TemplateItems.MinuteUpButtonName, DateTimePickerPanelType.Minute, SpinDirection.Decrease),
_minuteDownButton = SelectorButton(TemplateItems.MinuteDownButtonName, DateTimePickerPanelType.Minute, SpinDirection.Increase),
_secondUpButton = SelectorButton(TemplateItems.SecondUpButtonName, DateTimePickerPanelType.Second, SpinDirection.Decrease),
_secondDownButton = SelectorButton(TemplateItems.SecondDownButtonName, DateTimePickerPanelType.Second, SpinDirection.Increase),
_periodUpButton = SelectorButton(TemplateItems.PeriodUpButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Decrease),
_periodDownButton = SelectorButton(TemplateItems.PeriodDownButtonName, DateTimePickerPanelType.TimePeriod, SpinDirection.Increase),
_dismissButton = e.NameScope.Find<Button>(TemplateItems.DismissButtonName),
};
_templateItems.Value._acceptButton.Click += OnAcceptButtonClicked;
if (_templateItems.Value._dismissButton is { } dismissButton)
{
dismissButton.Click += OnDismissButtonClicked;
}
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)
@ -232,100 +259,105 @@ namespace Avalonia.Controls
protected override void OnConfirmed()
{
var hr = _hourSelector!.SelectedValue;
var min = _minuteSelector!.SelectedValue;
var sec = _secondSelector?.SelectedValue ?? 0;
var per = _periodSelector!.SelectedValue;
if (ClockIdentifier == "12HourClock")
if (_templateItems is { } items)
{
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();
}
private void InitPicker()
{
if (_pickerContainer == null)
if (_templateItems is not { } items)
return;
bool clock12 = ClockIdentifier == "12HourClock";
_hourSelector!.MaximumValue = clock12 ? 12 : 23;
_hourSelector.MinimumValue = clock12 ? 1 : 0;
_hourSelector.ItemFormat = "%h";
items._hourSelector.MaximumValue = clock12 ? 12 : 23;
items._hourSelector.MinimumValue = clock12 ? 1 : 0;
items._hourSelector.ItemFormat = "%h";
var hr = Time.Hours;
_hourSelector.SelectedValue = !clock12 ? hr :
items._hourSelector.SelectedValue = !clock12 ? hr :
hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
_minuteSelector!.MaximumValue = 59;
_minuteSelector.MinimumValue = 0;
_minuteSelector.Increment = MinuteIncrement;
_minuteSelector.ItemFormat = "mm";
_minuteSelector.SelectedValue = Time.Minutes;
items._minuteSelector.MaximumValue = 59;
items._minuteSelector.MinimumValue = 0;
items._minuteSelector.Increment = MinuteIncrement;
items._minuteSelector.ItemFormat = "mm";
items._minuteSelector.SelectedValue = Time.Minutes;
if (_secondSelector is not null)
if (items._secondSelector is { } secondSelector)
{
_secondSelector.MaximumValue = 59;
_secondSelector.MinimumValue = 0;
_secondSelector.Increment = SecondIncrement;
_secondSelector.ItemFormat = "ss";
_secondSelector.SelectedValue = Time.Seconds;
secondSelector.MaximumValue = 59;
secondSelector.MinimumValue = 0;
secondSelector.Increment = SecondIncrement;
secondSelector.ItemFormat = "ss";
secondSelector.SelectedValue = Time.Seconds;
}
_periodSelector!.MaximumValue = 1;
_periodSelector.MinimumValue = 0;
_periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
items._periodSelector.MaximumValue = 1;
items._periodSelector.MinimumValue = 0;
items._periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
SetGrid();
_hourSelector?.Focus(NavigationMethod.Pointer);
SetGrid(items);
items._hourSelector.Focus(NavigationMethod.Pointer);
}
private void SetGrid()
private void SetGrid(TemplateItems items)
{
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;
_secondHost!.IsVisible = UseSeconds;
_spacer3!.IsVisible = !use24HourClock;
_periodHost!.IsVisible = !use24HourClock;
if (UseSeconds)
{
columnsD.Add(new ColumnDefinition(GridLength.Auto));
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;
Grid.SetColumn(_spacer2, UseSeconds ? 3 : 0);
Grid.SetColumn(_secondHost, UseSeconds ? 4 : 0);
Grid.SetColumn(_spacer3, use24HourClock ? 0 : amPmColumn-1);
Grid.SetColumn(_periodHost, use24HourClock ? 0 : amPmColumn);
Grid.SetColumn(items._secondSpacer, UseSeconds ? 3 : 0);
Grid.SetColumn(items._secondHost, UseSeconds ? 4 : 0);
Grid.SetColumn(items._thirdSpacer, use24HourClock ? 0 : amPmColumn - 1);
Grid.SetColumn(items._periodHost, use24HourClock ? 0 : amPmColumn);
}
else
{
_spacer2!.IsVisible = !use24HourClock;
_periodHost!.IsVisible = !use24HourClock;
Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3);
Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4);
items._secondSpacer.IsVisible = !use24HourClock;
items._periodHost.IsVisible = !use24HourClock;
Grid.SetColumn(items._secondSpacer, use24HourClock ? 0 : 3);
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)
@ -338,33 +370,37 @@ namespace Avalonia.Controls
OnConfirmed();
}
private void OnSelectorButtonClick(object? sender, RoutedEventArgs e)
private void OnSelectorButtonClick(DateTimePickerPanelType type, SpinDirection direction)
{
if (sender == _hourUpButton)
_hourSelector!.ScrollUp();
else if (sender == _hourDownButton)
_hourSelector!.ScrollDown();
else if (sender == _minuteUpButton)
_minuteSelector!.ScrollUp();
else if (sender == _minuteDownButton)
_minuteSelector!.ScrollDown();
else if (sender == _secondUpButton)
_secondSelector!.ScrollUp();
else if (sender == _secondDownButton)
_secondSelector!.ScrollDown();
else if (sender == _periodUpButton)
_periodSelector!.ScrollUp();
else if (sender == _periodDownButton)
_periodSelector!.ScrollDown();
var target = type switch
{
DateTimePickerPanelType.Hour => _templateItems?._hourSelector,
DateTimePickerPanelType.Minute => _templateItems?._minuteSelector,
DateTimePickerPanelType.Second => _templateItems?._secondSelector,
DateTimePickerPanelType.TimePeriod => _templateItems?._periodSelector,
_ => throw new NotImplementedException(),
};
switch (direction)
{
case SpinDirection.Increase:
target?.ScrollDown();
break;
case SpinDirection.Decrease:
target?.ScrollUp();
break;
default:
throw new NotImplementedException();
}
}
internal double GetOffsetForPopup()
{
if (_hourSelector is null)
if (_templateItems is not { } items)
return 0;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector.ItemHeight / 2);
var acceptDismissButtonHeight = items._acceptButton.Bounds.Height;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (items._hourSelector.ItemHeight / 2);
}
}
}

Loading…
Cancel
Save