Browse Source

Revert and prep for new impl

pull/4108/head
amwx 6 years ago
parent
commit
54a4f45d87
  1. 500
      src/Avalonia.Controls/DateTimePickers/DatePicker.cs
  2. 817
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenter.cs
  3. 31
      src/Avalonia.Controls/DateTimePickers/DatePickerPresenterItem.cs
  4. 19
      src/Avalonia.Controls/DateTimePickers/DatePickerValueChangedEventArgs.cs
  5. 259
      src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs
  6. 711
      src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs
  7. 58
      src/Avalonia.Controls/DateTimePickers/LoopingSelectorItem.cs
  8. 45
      src/Avalonia.Controls/DateTimePickers/PickerPresenterBase.cs
  9. 270
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  10. 470
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
  11. 27
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenterItem.cs
  12. 2
      src/Avalonia.Controls/DateTimePickers/TimePickerSelectedValueChangedEventArgs.cs
  13. 15
      src/Avalonia.Controls/DateTimePickers/TimePickerValueChangedEventArgs.cs

500
src/Avalonia.Controls/DateTimePickers/DatePicker.cs

@ -14,504 +14,6 @@ namespace Avalonia.Controls
/// </summary>
public class DatePicker : TemplatedControl
{
public DatePicker()
{
PseudoClasses.Set(":hasnodate", true);
_presenter = new DatePickerPresenter();
var now = DateTimeOffset.Now;
_minYear = new DateTimeOffset(now.Date.Year - 100, 1, 1, 0, 0, 0, now.Offset);
_maxYear = new DateTimeOffset(now.Date.Year + 100, 12, 31, 0, 0, 0, now.Offset);
_presenter.DatePicked += OnPresenterDatePicked;
}
/// <summary>
/// Define the <see cref="DayFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, string> DayFormatProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>("DayFormat",
x => x.DayFormat, (x, v) => x.DayFormat = v);
/// <summary>
/// Defines the <see cref="DayVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, bool> DayVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePicker, bool>("DayVisible",
x => x.DayVisible, (x, v) => x.DayVisible = v);
/// <summary>
/// Defines the <see cref="Header"/> Property
/// </summary>
public static readonly StyledProperty<object> HeaderProperty =
AvaloniaProperty.Register<DatePicker, object>("Header");
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> Property
/// </summary>
public static readonly StyledProperty<IDataTemplate> HeaderTemplateProperty =
AvaloniaProperty.Register<DatePicker, IDataTemplate>("HeaderTemplate");
/// <summary>
/// Defines the <see cref="MaxYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, DateTimeOffset> MaxYearProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset>("MaxYear", x => x.MaxYear, (x, v) => x.MaxYear = v);
/// <summary>
/// Defines the <see cref="MinYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, DateTimeOffset> MinYearProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset>("MinYear", x => x.MinYear, (x, v) => x.MinYear = v);
/// <summary>
/// Defines the <see cref="MonthFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, string> MonthFormatProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>("MonthFormat", x => x.MonthFormat, (x, v) => x.MonthFormat = v);
/// <summary>
/// Defines the <see cref="MonthVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, bool> MonthVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePicker, bool>("MonthVisible", x => x.MonthVisible, (x, v) => x.MonthVisible = v);
/// <summary>
/// Defiens the <see cref="YearFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, string> YearFormatProperty =
AvaloniaProperty.RegisterDirect<DatePicker, string>("YearFormat", x => x.YearFormat, (x, v) => x.YearFormat = v);
/// <summary>
/// Defines the <see cref="YearVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, bool> YearVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePicker, bool>("YearVisible", x => x.YearVisible, (x, v) => x.YearVisible = v);
/// <summary>
/// Defines the <see cref="SelectedDate"/> Property
/// </summary>
public static readonly DirectProperty<DatePicker, DateTimeOffset?> SelectedDateProperty =
AvaloniaProperty.RegisterDirect<DatePicker, DateTimeOffset?>("SelectedDate", x => x.SelectedDate, (x, v) => x.SelectedDate = v);
/// <summary>
/// Gets or sets the day format
/// </summary>
public string DayFormat
{
get => _dayFormat;
set => SetAndRaise(DayFormatProperty, ref _dayFormat, value);
}
/// <summary>
/// Gets or sets whether the day is visible
/// </summary>
public bool DayVisible
{
get => _dayVisible;
set
{
SetAndRaise(DayVisibleProperty, ref _dayVisible, value);
SetGrid();
}
}
/// <summary>
/// Gets or sets the DatePicker header
/// </summary>
public object Header
{
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
/// <summary>
/// Gets or sets the header template
/// </summary>
public IDataTemplate HeaderTemplate
{
get => GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
/// <summary>
/// Gets or sets the maximum year for the picker
/// </summary>
public DateTimeOffset MaxYear
{
get => _maxYear;
set
{
if (value < MinYear)
throw new InvalidOperationException("MaxDate cannot be less than MinDate");
SetAndRaise(MaxYearProperty, ref _maxYear, value);
if (SelectedDate.HasValue && SelectedDate.Value > value)
SelectedDate = value;
}
}
/// <summary>
/// Gets or sets the minimum year for the picker
/// </summary>
public DateTimeOffset MinYear
{
get => _minYear;
set
{
if (value > MaxYear)
throw new InvalidOperationException("MinDate cannot be greater than MaxDate");
SetAndRaise(MinYearProperty, ref _minYear, value);
if (SelectedDate.HasValue && SelectedDate.Value < value)
SelectedDate = value;
}
}
/// <summary>
/// Gets or sets the month format
/// </summary>
public string MonthFormat
{
get => _monthFormat;
set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value);
}
/// <summary>
/// Gets or sets whether the month is visible
/// </summary>
public bool MonthVisible
{
get => _monthVisible;
set
{
SetAndRaise(MonthVisibleProperty, ref _monthVisible, value);
SetGrid();
}
}
/// <summary>
/// Gets or sets the year format
/// </summary>
public string YearFormat
{
get => _yearFormat;
set => SetAndRaise(YearFormatProperty, ref _yearFormat, value);
}
/// <summary>
/// Gets or sets whether the year is visible
/// </summary>
public bool YearVisible
{
get => _yearVisible;
set
{
SetAndRaise(YearVisibleProperty, ref _yearVisible, value);
SetGrid();
}
}
/// <summary>
/// Gets or sets the Selected Date for the picker, can be null
/// </summary>
public DateTimeOffset? SelectedDate
{
get => _selectedDate;
set
{
var old = _selectedDate;
SetAndRaise(SelectedDateProperty, ref _selectedDate, value);
SetSelectedDateText();
OnSelectedDateChanged(this, new DatePickerSelectedValueChangedEventArgs(old, value));
}
}
/// <summary>
/// Raised when the <see cref="SelectedDate"/> changes
/// </summary>
public event EventHandler<DatePickerSelectedValueChangedEventArgs> SelectedDateChanged;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_areControlsAvailable = false;
base.OnApplyTemplate(e);
_flyoutButton = e.NameScope.Find<Button>("FlyoutButton");
_dayText = e.NameScope.Find<TextBlock>("DayText");
_monthText = e.NameScope.Find<TextBlock>("MonthText");
_yearText = e.NameScope.Find<TextBlock>("YearText");
_container = e.NameScope.Find<Grid>("ButtonContentGrid");
_spacer1 = e.NameScope.Find<Rectangle>("FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("SecondSpacer");
_areControlsAvailable = true;
SetGrid();
SetSelectedDateText();
if (_flyoutButton != null)
{
_flyoutButton.Click += OnFlyoutButtonClicked;
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
}
/// <summary>
/// Sets up the container grid and makes sure all label and spacers are placed correctly
/// </summary>
private void SetGrid()
{
//Brute force method to setup the container grid, probably a better way to do this
//but it works...
if (!_areControlsAvailable) //hopefully this never happens
return;
if (!_hasInit)
{
//Display order of date is based on user's culture, we attempt
//to figure out the normal date pattern
//TODO: Find better way to do this, but for now it works...
var fmt = System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var monthfmt = Regex.Match(fmt, "(M|MM)");
var yearfmt = Regex.Match(fmt, "(Y|YY|YYY|YYYY|y|yy|yyy|yyyy)");
var dayfmt = Regex.Match(fmt, "(d|dd)");
//Default is M-D-Y (en-us), this gives us fallback if pattern matching fails
_monthIndex = 0;
_yearIndex = 2;
_dayIndex = 1;
if (monthfmt.Success && yearfmt.Success && dayfmt.Success)
{
_monthIndex = monthfmt.Index;
_yearIndex = yearfmt.Index;
_dayIndex = dayfmt.Index;
}
//Six possible combos, some probably don't actually exist,
//but prep for them anyway
//M d y [x]
//M y d [x]
//d M y [x]
//d y M [x]
//y d M [x]
//y M d [x]
_hasInit = true;
}
bool showMonth = MonthVisible;
bool showDay = DayVisible;
bool showYear = YearVisible;
_container.ColumnDefinitions.Clear();
if (showMonth && !showDay && !showYear) //Month Only
{
_container.ColumnDefinitions.Add(new ColumnDefinition(132, GridUnitType.Star));
_monthText.IsVisible = true;
_dayText.IsVisible = false;
_yearText.IsVisible = false;
_spacer1.IsVisible = false;
_spacer2.IsVisible = false;
Grid.SetColumn(_monthText, 0);
}
else if (!showMonth && showDay && !showYear) //Day Only
{
_container.ColumnDefinitions.Add(new ColumnDefinition(132, GridUnitType.Star));
_monthText.IsVisible = false;
_dayText.IsVisible = true;
_yearText.IsVisible = false;
_spacer1.IsVisible = false;
_spacer2.IsVisible = false;
Grid.SetColumn(_dayText, 0);
}
else if (!showMonth && !showDay && showYear) //Year Only
{
_container.ColumnDefinitions.Add(new ColumnDefinition(132, GridUnitType.Star));
_monthText.IsVisible = false;
_dayText.IsVisible = false;
_yearText.IsVisible = true;
_spacer1.IsVisible = false;
_spacer2.IsVisible = false;
Grid.SetColumn(_yearText, 0);
}
else if (showMonth && showDay && !showYear) //Month and Day Only
{
_container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _dayIndex ? 132 : 78, GridUnitType.Star));
_container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
_container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _dayIndex ? 78 : 132, GridUnitType.Star));
_monthText.IsVisible = true;
_dayText.IsVisible = true;
_yearText.IsVisible = false;
_spacer1.IsVisible = true;
_spacer2.IsVisible = false;
Grid.SetColumn(_monthText, _monthIndex < _dayIndex ? 0 : 2);
Grid.SetColumn(_dayText, _monthIndex < _dayIndex ? 2 : 0);
Grid.SetColumn(_spacer1, 1);
}
else if (showMonth && !showDay && showYear) //Month and Year Only
{
_container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _yearIndex ? 132 : 78, GridUnitType.Star));
_container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
_container.ColumnDefinitions.Add(new ColumnDefinition(_monthIndex < _yearIndex ? 78 : 132, GridUnitType.Star));
_monthText.IsVisible = true;
_dayText.IsVisible = false;
_yearText.IsVisible = true;
_spacer1.IsVisible = true;
_spacer2.IsVisible = false;
Grid.SetColumn(_monthText, _monthIndex < _yearIndex ? 0 : 2);
Grid.SetColumn(_yearText, _monthIndex < _yearIndex ? 2 : 0);
Grid.SetColumn(_spacer1, 1);
}
else if (!showMonth && showDay && showYear) //Day and Year Only
{
_container.ColumnDefinitions.Add(new ColumnDefinition(78, GridUnitType.Star));
_container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
_container.ColumnDefinitions.Add(new ColumnDefinition(78, GridUnitType.Star));
_monthText.IsVisible = false;
_dayText.IsVisible = true;
_yearText.IsVisible = true;
_spacer1.IsVisible = true;
_spacer2.IsVisible = false;
Grid.SetColumn(_yearText, _dayIndex < _yearIndex ? 2 : 0);
Grid.SetColumn(_dayText, _dayIndex < _yearIndex ? 0 : 2);
Grid.SetColumn(_spacer1, 1);
}
else if (showMonth && showDay && showYear) //All Visible
{
bool isMonthFirst = _monthIndex < _dayIndex && _monthIndex < _yearIndex;
bool isMonthSecond = (_monthIndex > _dayIndex && _monthIndex < _yearIndex) ||
(_monthIndex < _dayIndex && _monthIndex > _yearIndex);
_container.ColumnDefinitions.Add(new ColumnDefinition(isMonthFirst ? 138 : 78, GridUnitType.Star));
_container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
_container.ColumnDefinitions.Add(new ColumnDefinition(isMonthSecond ? 138 : 78, GridUnitType.Star));
_container.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
_container.ColumnDefinitions.Add(new ColumnDefinition((!isMonthFirst && !isMonthSecond) ? 138 : 78, GridUnitType.Star));
_monthText.IsVisible = true;
_dayText.IsVisible = true;
_yearText.IsVisible = true;
_spacer1.IsVisible = true;
_spacer2.IsVisible = true;
bool isDayFirst = !isMonthFirst && _dayIndex < _yearIndex;
bool isDaySecond = (_dayIndex > _monthIndex && _dayIndex < _yearIndex) ||
(_dayIndex < _monthIndex && _dayIndex > _yearIndex);
bool isYearFirst = !isDayFirst && !isMonthFirst;
bool isYearSecond = (_yearIndex > _monthIndex && _yearIndex < _dayIndex) ||
(_yearIndex < _monthIndex && _yearIndex > _dayIndex);
Grid.SetColumn(_monthText, isMonthFirst ? 0 : isMonthSecond ? 2 : 4);
Grid.SetColumn(_yearText, isYearFirst ? 0 : (isMonthSecond || isDaySecond) ? 4 : 2);
Grid.SetColumn(_dayText, isDayFirst ? 0 : (isMonthSecond || isYearSecond) ? 4 : 2);
Grid.SetColumn(_spacer1, 1);
Grid.SetColumn(_spacer2, 3);
}
else
{
_monthText.IsVisible = false;
_dayText.IsVisible = false;
_yearText.IsVisible = false;
_spacer1.IsVisible = false;
_spacer2.IsVisible = false;
}
}
/// <summary>
/// Sets the TextBlocks when the SelectedDate changes
/// </summary>
private void SetSelectedDateText()
{
if (!_areControlsAvailable)
return;
if (SelectedDate.HasValue)
{
PseudoClasses.Set(":hasnodate", false);
var selDate = SelectedDate.Value;
_monthText.Text = selDate.ToString(MonthFormat);
_yearText.Text = selDate.ToString(YearFormat);
_dayText.Text = selDate.ToString(DayFormat);
}
else
{
PseudoClasses.Set(":hasnodate", true);
_monthText.Text = "month";
_yearText.Text = "year";
_dayText.Text = "day";
}
}
private void OnFlyoutButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_presenter.YearFormat = YearFormat;
_presenter.DayFormat = DayFormat;
_presenter.MonthFormat = MonthFormat;
_presenter.MonthVisible = MonthVisible;
_presenter.YearVisible = YearVisible;
_presenter.DayVisible = DayVisible;
//If SelectedDate hasn't been set, fallback to now
_presenter.Date = SelectedDate.HasValue ? SelectedDate.Value : DateTimeOffset.Now;
_presenter.MaxYear = MaxYear;
_presenter.MinYear = MinYear;
_presenter.ShowAt(this);
}
private void OnPresenterDatePicked(object sender, DatePickerValueChangedEventArgs args)
{
SelectedDate = args.NewDate;
}
protected virtual void OnSelectedDateChanged(object sender, DatePickerSelectedValueChangedEventArgs args)
{
SelectedDateChanged?.Invoke(sender, args);
}
//Template Items
private Button _flyoutButton;
private TextBlock _dayText;
private TextBlock _monthText;
private TextBlock _yearText;
private Grid _container;
private Rectangle _spacer1;
private Rectangle _spacer2;
private DatePickerPresenter _presenter;
private bool _hasInit;
private bool _areControlsAvailable;
private int _monthIndex;
private int _dayIndex;
private int _yearIndex;
private string _dayFormat = "%d";
private bool _dayVisible = true;
private DateTimeOffset _maxYear;
private DateTimeOffset _minYear;
private string _monthFormat = "MMMM";
private bool _monthVisible = true;
private string _yearFormat = "yyyy";
private bool _yearVisible = true;
private DateTimeOffset? _selectedDate;
}
}

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

@ -13,822 +13,11 @@ using System.Text.RegularExpressions;
namespace Avalonia.Controls
{
/// <summary>
/// Defines the presenter used for selecting a date. Intended for use with
/// <see cref="DatePicker"/> but can be used independently. Combines
/// DatePickerFlyout and DatePickerFlyoutPresenter
/// Defines the presenter used for selecting a date for a
/// <see cref="DatePicker"/>
/// </summary>
public class DatePickerPresenter : PickerPresenterBase
{
public DatePickerPresenter()
{
var now = DateTimeOffset.Now;
_minYear = new DateTimeOffset(now.Year - 100, 1, 1, 0, 0, 0, now.Offset);
_maxYear = new DateTimeOffset(now.Year + 100, 12, 31, 0, 0, 0, now.Offset);
_date = now;
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
}
/// <summary>
/// Defines the <see cref="Date"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, DateTimeOffset> DateProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, DateTimeOffset>("Date", x => x.Date, (x, v) => x.Date = v);
/// <summary>
/// Defines the <see cref="DayFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, string> DayFormatProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, string>("DayFormat", x => x.DayFormat, (x, v) => x.DayFormat = v);
/// <summary>
/// Defines the <see cref="DayVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, bool> DayVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, bool>("DayVisible", x => x.DayVisible, (x, v) => x.DayVisible = v);
/// <summary>
/// Defines the <see cref="MaxYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, DateTimeOffset> MaxYearProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, DateTimeOffset>("MaxYear", x => x.MaxYear, (x, v) => x.MaxYear = v);
/// <summary>
/// Defines the <see cref="MinYear"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, DateTimeOffset> MinYearProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, DateTimeOffset>("MinYear", x => x.MinYear, (x, v) => x.MinYear = v);
/// <summary>
/// Defines the <see cref="MonthFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, string> MonthFormatProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, string>("MonthFormat", x => x.MonthFormat, (x, v) => x.MonthFormat = v);
/// <summary>
/// Defines the <see cref="MonthVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, bool> MonthVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, bool>("MonthVisible", x => x.MonthVisible, (x, v) => x.MonthVisible = v);
/// <summary>
/// Defines the <see cref="YearFormat"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, string> YearFormatProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, string>("YearFormat", x => x.YearFormat, (x, v) => x.YearFormat = v);
/// <summary>
/// Defines the <see cref="YearVisible"/> Property
/// </summary>
public static readonly DirectProperty<DatePickerPresenter, bool> YearVisibleProperty =
AvaloniaProperty.RegisterDirect<DatePickerPresenter, bool>("YearVisible", x => x.YearVisible, (x, v) => x.YearVisible = v);
//These aren't in WinUI
/// <summary>
/// Defines the <see cref="YearSelectorItemTemplate"/> Property
/// </summary>
public static readonly StyledProperty<IDataTemplate> YearSelectorItemTemplateProperty =
AvaloniaProperty.Register<DatePickerPresenter, IDataTemplate>("YearSelectorItemTemplate");
/// <summary>
/// Defines the <see cref="MonthSelectorItemTemplate"/> Property
/// </summary>
public static readonly StyledProperty<IDataTemplate> MonthSelectorItemTemplateProperty =
AvaloniaProperty.Register<DatePickerPresenter, IDataTemplate>("MonthSelectorItemTemplate");
/// <summary>
/// Defines the <see cref="DaySelectorItemTemplate"/> Property
/// </summary>
public static readonly StyledProperty<IDataTemplate> DaySelectorItemTemplateProperty =
AvaloniaProperty.Register<DatePickerPresenter, IDataTemplate>("DaySelectorItemTemplate");
/// <summary>
/// Gets or sets the current Date for the picker
/// </summary>
public DateTimeOffset Date
{
get => _date;
set => SetAndRaise(DateProperty, ref _date, value);
}
/// <summary>
/// Gets or sets the DayFormat
/// </summary>
public string DayFormat
{
get => _dayFormat;
set => SetAndRaise(DayFormatProperty, ref _dayFormat, value);
}
/// <summary>
/// Get or sets whether the Day selector is visible
/// </summary>
public bool DayVisible
{
get => _dayVisible;
set
{
SetAndRaise(DayVisibleProperty, ref _dayVisible, value);
}
}
/// <summary>
/// Gets or sets the maximum pickable year
/// </summary>
public DateTimeOffset MaxYear
{
get => _maxYear;
set
{
if (value < MinYear)
throw new InvalidOperationException("MaxDate cannot be less than MinDate");
SetAndRaise(MaxYearProperty, ref _maxYear, value);
if (Date > value)
Date = value;
}
}
/// <summary>
/// Gets or sets the minimum pickable year
/// </summary>
public DateTimeOffset MinYear
{
get => _minYear;
set
{
if (value > MaxYear)
throw new InvalidOperationException("MinDate cannot be greater than MaxDate");
SetAndRaise(MinYearProperty, ref _minYear, value);
if (Date < value)
Date = value;
}
}
/// <summary>
/// Gets or sets the month format
/// </summary>
public string MonthFormat
{
get => _monthFormat;
set => SetAndRaise(MonthFormatProperty, ref _monthFormat, value);
}
/// <summary>
/// Gets or sets whether the month selector is visible
/// </summary>
public bool MonthVisible
{
get => _monthVisible;
set
{
SetAndRaise(MonthVisibleProperty, ref _monthVisible, value);
}
}
/// <summary>
/// Gets or sets the year format
/// </summary>
public string YearFormat
{
get => _yearFormat;
set => SetAndRaise(YearFormatProperty, ref _yearFormat, value);
}
/// <summary>
/// Gets or sets whether the year selector is visible
/// </summary>
public bool YearVisible
{
get => _yearVisible;
set
{
SetAndRaise(YearVisibleProperty, ref _yearVisible, value);
}
}
/// <summary>
/// Gets or sets the item template for the YearSelector items
/// </summary>
public IDataTemplate YearSelectorItemTemplate
{
get => GetValue(YearSelectorItemTemplateProperty);
set => SetValue(YearSelectorItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets the item template for the MonthSelector items
/// </summary>
public IDataTemplate MonthSelectorItemTemplate
{
get => GetValue(MonthSelectorItemTemplateProperty);
set => SetValue(MonthSelectorItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets the item template for the DaySelector items
/// </summary>
public IDataTemplate DaySelectorItemTemplate
{
get => GetValue(DaySelectorItemTemplateProperty);
set => SetValue(DaySelectorItemTemplateProperty, value);
}
/// <summary>
/// Raised when the AcceptButton is clicked or Enter is pressed
/// </summary>
public event EventHandler<DatePickerValueChangedEventArgs> DatePicked;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
//This is a requirement, so throw if not found
_pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
_acceptButton = e.NameScope.Find<Button>("AcceptButton");
_dismissButton = e.NameScope.Find<Button>("DismissButton");
_spacer1 = e.NameScope.Find<Rectangle>("FirstSpacer");
_spacer2 = e.NameScope.Find<Rectangle>("SecondSpacer");
if (_acceptButton != null)
{
_acceptButton.Click += OnAcceptButtonClicked;
}
if (_dismissButton != null)
{
_dismissButton.Click += OnDismissButtonClicked;
}
//If template is reapplied (theme change, etc), make sure the looping selectors
//are removed from the old grid & placed into new one. However, since placement
//logic is complex, it's easier to just destroy and recreate the loopingselectors
if(_yearSelector != null)
{
_yearSelector.SelectionChanged -= OnYearSelectionChanged;
_yearSelector.Items = null;
_yearSelector = null;
}
if(_monthSelector != null)
{
_monthSelector.SelectionChanged -= OnMonthSelectionChanged;
_monthSelector.Items = null;
_monthSelector = null;
}
if(_daySelector != null)
{
_daySelector.SelectionChanged -= OnDaySelectionChanged;
_daySelector.Items = null;
_daySelector = null;
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
_hostPopup.IsOpen = false;
e.Handled = true;
break;
case Key.Tab:
var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next);
KeyboardDevice.Instance?.SetFocusedElement(nextFocus, NavigationMethod.Tab, KeyModifiers.None);
e.Handled = true;
break;
case Key.Enter:
OnConfirmed();
e.Handled = true;
break;
}
base.OnKeyDown(e);
}
private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_hostPopup.IsOpen = false;
}
private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
OnConfirmed();
}
protected override void OnConfirmed()
{
var old = _date;
OnDateChanged(new DatePickerValueChangedEventArgs(_initDate, Date));
_hostPopup.IsOpen = false;
}
protected virtual void OnDateChanged(DatePickerValueChangedEventArgs args)
{
DatePicked?.Invoke(this, args);
}
/// <inheritdoc/>
public override void ShowAt(Control target)
{
if (_hostPopup == null)
{
_hostPopup = new Avalonia.Controls.Primitives.Popup();
_hostPopup.Child = this;
_hostPopup.PlacementMode = PlacementMode.Bottom;
_hostPopup.StaysOpen = false;
((ISetLogicalParent)_hostPopup).SetParent(target);
_hostPopup.Closed += OnPopupClosed;
_hostPopup.WindowManagerAddShadowHint = false;
_hostPopup.Focusable = false;
}
if (target == null)
throw new ArgumentNullException("Target cannot be null");
_hostPopup.PlacementTarget = target;
//Need to open the popup first, so the template is applied & our
//template items are available
_hostPopup.IsOpen = true;
var yearVis = YearVisible;
var monthVis = MonthVisible;
var dayVis = DayVisible;
//Creates or destroys the selectors based on our settings
EnsureSelectors(dayVis, monthVis, yearVis);
//Set up the container grid
SetGrid();
//Focus the first Selector
SetInitialFocus();
_initDate = Date;
//As we init the Selectors, prevent any selection behavior for occuring
_suppressUpdateSelection = true;
//If we're display a specific date component, create the items needed,
//and set the selected index
//If we're not using it & we previously had, make sure the items list is
//clear so we're not using unneeded memory
if (dayVis)
{
CreateDayItems();
_daySelector.Items = _dayItems;
_daySelector.SelectedIndex = Date.Date.Day - 1;
}
else
{
if (_dayItems != null)
{
_daySelector.Items = null;
_dayItems.Clear();
_dayItems = null;
}
}
if (monthVis)
{
CreateMonthItems();
_monthSelector.Items = _monthItems;
_monthSelector.SelectedIndex = Date.Date.Month - 1;
}
else
{
if (_monthItems != null)
{
_monthSelector.Items = null;
_monthItems.Clear();
_monthItems = null;
}
}
if (yearVis)
{
CreateYearItems();
_yearSelector.Items = _yearItems;
_yearSelector.SelectedIndex = Date.Date.Year - MinYear.Date.Year;
}
else
{
if (_yearItems != null)
{
_yearSelector.Items = null;
_yearItems.Clear();
_yearItems = null;
}
}
_suppressUpdateSelection = false;
OnOpened();
//Dynamic position logic for popup
//Get item height from an available looping selector
//Get the max height of the popup (constrained in template) and subtract the accept/dismiss region out of that
//Popup is placed below the control, so we subtract (half of the remaining distance + half an item)
var itemHeight = _monthSelector != null ? _monthSelector.ItemHeight : _yearSelector != null ? _yearSelector.ItemHeight :
_daySelector != null ? _daySelector.ItemHeight : 0;
var maxHeight = MaxHeight;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
var deltaY = -(maxHeight - acceptDismissButtonHeight) / 2 - itemHeight / 2;
//The extra 5 px I think is related to default popup placement behavior
_hostPopup.Host.ConfigurePosition(_hostPopup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY+5),
Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);
}
/// <summary>
/// Ensures the selectors are created and setup
/// </summary>
private void EnsureSelectors(bool dayVis, bool monthVis, bool yearVis)
{
Contract.Requires<NullReferenceException>(_pickerContainer != null);
//If creating the selectors, we only create, but don't add...
//See note in SetGrid() for reasoning
//We do remove here though, if needed
if (yearVis && _yearSelector == null)
{
_yearSelector = new LoopingSelector();
_yearSelector.SelectionChanged += OnYearSelectionChanged;
_yearSelector.ShouldLoop = false;
_yearSelector.ItemTemplate = YearSelectorItemTemplate;
}
else if (!yearVis && _yearSelector != null)
{
_yearSelector.SelectionChanged -= OnYearSelectionChanged;
if (_yearSelector.Parent != null)
_pickerContainer.Children.Remove(_yearSelector);
_yearSelector.Items = null;
_yearSelector = null;
}
if (monthVis && _monthSelector == null)
{
_monthSelector = new LoopingSelector();
_monthSelector.SelectionChanged += OnMonthSelectionChanged;
_monthSelector.ShouldLoop = true;
_monthSelector.ItemTemplate = MonthSelectorItemTemplate;
}
else if (!monthVis && _monthSelector != null)
{
_monthSelector.SelectionChanged -= OnMonthSelectionChanged;
if (_monthSelector.Parent != null)
_pickerContainer.Children.Remove(_monthSelector);
_monthSelector.Items = null;
_monthSelector = null;
}
if (dayVis && _daySelector == null)
{
_daySelector = new LoopingSelector();
_daySelector.SelectionChanged += OnDaySelectionChanged;
_daySelector.ShouldLoop = true;
_daySelector.ItemTemplate = DaySelectorItemTemplate;
}
else if (!dayVis && _daySelector != null)
{
_daySelector.SelectionChanged -= OnMonthSelectionChanged;
if (_daySelector.Parent != null)
_pickerContainer.Children.Remove(_daySelector);
_daySelector.Items = null;
_daySelector = null;
}
}
/// <summary>
/// Creates the items for the day selector
/// </summary>
private void CreateDayItems()
{
if (_dayItems == null)
_dayItems = new AvaloniaList<DatePickerPresenterItem>();
if (_dayItems.Count > 0)
_dayItems.Clear();
var format = DayFormat;
var date = Date;
GregorianCalendar gc = new GregorianCalendar();
var daysInMonth = gc.GetDaysInMonth(date.Date.Year, date.Date.Month);
int dayIndex = 1;
DateTimeOffset curDt;
while (dayIndex <= daysInMonth)
{
curDt = new DateTimeOffset(date.Date.Year, date.Date.Month, dayIndex, 0, 0, 0, date.Offset);
DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt);
dppi.DisplayText = curDt.ToString(format);
_dayItems.Add(dppi);
dayIndex++;
}
}
/// <summary>
/// Creates the items for the month selector
/// </summary>
private void CreateMonthItems()
{
if (_monthItems == null)
_monthItems = new AvaloniaList<DatePickerPresenterItem>();
if (_monthItems.Count > 0)
_monthItems.Clear();
var format = MonthFormat;
var date = Date;
int monthIndex = 1;
DateTimeOffset curDt;
while (monthIndex <= 12) //12 months in Gregorian Calendar
{
curDt = new DateTimeOffset(date.Date.Year, monthIndex, 1, 0, 0, 0, date.Offset);
DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt);
dppi.DisplayText = curDt.ToString(format);
_monthItems.Add(dppi);
monthIndex++;
}
}
/// <summary>
/// Creates the items for the year selector
/// </summary>
private void CreateYearItems()
{
if (_yearItems == null)
_yearItems = new AvaloniaList<DatePickerPresenterItem>();
if (_yearItems.Count > 0)
_yearItems.Clear();
var format = YearFormat;
var curDt = MinYear.Date;
var max = MaxYear.Date;
while (curDt <= max)
{
DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt);
dppi.DisplayText = curDt.ToString(format);
_yearItems.Add(dppi);
curDt = curDt.AddYears(1);
}
}
/// <summary>
/// Updates the dayitems list, if necessary, when the month or year changes
/// </summary>
private void UpdateDayItems(bool forceRecreate = false)
{
var date = Date;
GregorianCalendar gc = new GregorianCalendar();
var daysInMonth = gc.GetDaysInMonth(date.Date.Year, date.Date.Month);
//Same number of days, no need to change unless forceRecreate == true
if (!forceRecreate && daysInMonth == _dayItems.Count)
return;
var format = DayFormat;
if (forceRecreate)
{
//We're not actually going to recreate the entire list, we're just
//going to update the Date & DisplayText of existing items
//The logic below will handle adding/removing items, if needed...
DateTimeOffset curDt = new DateTimeOffset(Date.Date.Year, Date.Date.Month, 1, 0, 0, 0, Date.Offset);
for (int i = 0; i < _dayItems.Count; i++)
{
_dayItems[i].UpdateStoredDate(curDt, curDt.ToString(format));
curDt = curDt.AddDays(1);
}
}
//Changes only occur at the end of the month, so don't recreate the entire
//collection, just add/remove items where necessary;
if (daysInMonth >= _dayItems.Count) //Add items
{
int dayIndex = _dayItems[_dayItems.Count - 1].GetStoredDate().Date.Day + 1;
DateTimeOffset curDt;
while (_dayItems.Count < daysInMonth)
{
curDt = new DateTimeOffset(date.Date.Year, date.Date.Month, dayIndex, 0, 0, 0, date.Offset);
DatePickerPresenterItem dppi = new DatePickerPresenterItem(curDt);
dppi.DisplayText = curDt.ToString(format);
_dayItems.Add(dppi);
dayIndex++;
}
}
else //remove items
{
while (_dayItems.Count > daysInMonth)
{
_dayItems.RemoveAt(_dayItems.Count - 1);
}
}
//Make sure the SelectedIndex of the day selector is still valid
if (_daySelector.SelectedIndex >= daysInMonth)
{
_daySelector.SelectedIndex = daysInMonth - 1;
}
}
/// <summary>
/// Sets the selector grid up, adding and placing the selectors and spacers in the
/// correct location
/// </summary>
private void SetGrid()
{
var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortDatePattern;
var columns = new List<(LoopingSelector, int)>
{
(_monthSelector, MonthVisible ? fmt.IndexOf("m", StringComparison.OrdinalIgnoreCase) : -1),
(_yearSelector, YearVisible ? fmt.IndexOf("y", StringComparison.OrdinalIgnoreCase) : -1),
(_daySelector, DayVisible ? fmt.IndexOf("d", StringComparison.OrdinalIgnoreCase) : -1),
};
columns.Sort((x, y) => x.Item2 - y.Item2);
_pickerContainer.ColumnDefinitions.Clear();
var columnIndex = 0;
foreach (var column in columns)
{
if (column.Item1 is null)
continue;
column.Item1.IsVisible = column.Item2 != -1;
if (column.Item2 != -1)
{
if (columnIndex > 0)
{
_pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(0, GridUnitType.Auto));
}
_pickerContainer.ColumnDefinitions.Add(new ColumnDefinition(132, GridUnitType.Star));
if (column.Item1.Parent is null)
{
_pickerContainer.Children.Add(column.Item1);
}
Grid.SetColumn(column.Item1, (columnIndex++ * 2));
}
}
Grid.SetColumn(_spacer1, 1);
Grid.SetColumn(_spacer2, 3);
_spacer1.IsVisible = columnIndex > 1;
_spacer2.IsVisible = columnIndex > 2;
}
private void OnPopupClosed(object sender, PopupClosedEventArgs e)
{
_hostPopup.PlacementTarget.Focus();
KeyboardDevice.Instance?.SetFocusedElement(_hostPopup.PlacementTarget, NavigationMethod.Pointer, KeyModifiers.None);
OnClosed();
}
/// <summary>
/// Keeps the date in sync with the day selector, and updates the day selector if needed
/// </summary>
private void OnDaySelectionChanged(object sender, SelectionChangedEventArgs e)
{
//throw new NotImplementedException();
if (_suppressUpdateSelection)
return;
if (e.AddedItems[0] != null)
{
var currentDate = Date.Date;
if (e.AddedItems[0] is DatePickerPresenterItem dppi)
{
var itemDate = dppi.GetStoredDate();
Date = new DateTimeOffset(currentDate.Date.Year, currentDate.Date.Month, itemDate.Date.Day, 0, 0, 0, Date.Offset);
}
}
}
/// <summary>
/// Keeps the date in sync with the month selector, and updates the day selector if needed
/// </summary>
private void OnMonthSelectionChanged(object sender, SelectionChangedEventArgs e)
{
if (_suppressUpdateSelection)
return;
_suppressUpdateSelection = true;
if (e.AddedItems[0] != null)
{
var currentDate = Date.Date;
if (e.AddedItems[0] is DatePickerPresenterItem dppi)
{
var itemDate = dppi.GetStoredDate();
var day = currentDate.Date.Day;
int maxDays = new GregorianCalendar().GetDaysInMonth(currentDate.Date.Year, itemDate.Date.Month);
if (day > maxDays)
day = maxDays;
Date = new DateTimeOffset(currentDate.Date.Year, itemDate.Date.Month, day, 0, 0, 0, Date.Offset);
if (DayVisible && _daySelector != null)
UpdateDayItems(DayFormat.Contains("dayofweek") /*forceRecreate*/);
}
}
_suppressUpdateSelection = false;
}
/// <summary>
/// Keeps the date in sync with the year selector, and updates the day selector if needed
/// </summary>
private void OnYearSelectionChanged(object sender, SelectionChangedEventArgs e)
{
//throw new NotImplementedException();
if (_suppressUpdateSelection)
return;
if (e.AddedItems[0] != null)
{
var currentDate = Date.Date;
if (e.AddedItems[0] is DatePickerPresenterItem dppi)
{
var itemDate = dppi.GetStoredDate();
//Year selection changed also needs to update the day items, incase:
//Month is February (leap years)
//Day of week is displayed in DayFormat
//We can check for both of these here
if ((DayVisible && _daySelector != null) && Date.Date.Month == 2 || DayFormat.Contains("dayofweek"))
{
var day = currentDate.Date.Day;
int maxDays = new GregorianCalendar().GetDaysInMonth(itemDate.Date.Year, currentDate.Date.Month);
if (day > maxDays)
day = maxDays;
Date = new DateTimeOffset(itemDate.Date.Year, currentDate.Date.Month, day, 0, 0, 0, Date.Offset);
UpdateDayItems(true /*ForceRecreate*/);
}
else
{
Date = new DateTimeOffset(itemDate.Date.Year, currentDate.Date.Month, currentDate.Date.Day, 0, 0, 0, Date.Offset);
}
}
}
}
/// <summary>
/// Forces selection on the leftmost selector
/// </summary>
private void SetInitialFocus()
{
int monthCol = MonthVisible && _monthSelector != null ? Grid.GetColumn(_monthSelector) : int.MaxValue;
int dayCol = DayVisible && _daySelector != null ? Grid.GetColumn(_daySelector) : int.MaxValue;
int yearCol = YearVisible && _yearSelector != null ? Grid.GetColumn(_yearSelector) : int.MaxValue;
if (monthCol < dayCol && monthCol < yearCol)
{
KeyboardDevice.Instance?.SetFocusedElement(_monthSelector, NavigationMethod.Pointer, KeyModifiers.None);
}
else if (dayCol < monthCol && dayCol < yearCol)
{
KeyboardDevice.Instance?.SetFocusedElement(_monthSelector, NavigationMethod.Pointer, KeyModifiers.None);
}
else if (yearCol < monthCol && yearCol < dayCol)
{
KeyboardDevice.Instance?.SetFocusedElement(_monthSelector, NavigationMethod.Pointer, KeyModifiers.None);
}
}
//Item Lists
private IList<DatePickerPresenterItem> _dayItems { get; set; }
private IList<DatePickerPresenterItem> _monthItems { get; set; }
private IList<DatePickerPresenterItem> _yearItems { get; set; }
//Template Items
private Grid _pickerContainer;
private Button _acceptButton;
private Button _dismissButton;
private Rectangle _spacer1;
private Rectangle _spacer2;
//Selectors
private LoopingSelector _yearSelector;
private LoopingSelector _monthSelector;
private LoopingSelector _daySelector;
private DateTimeOffset _date;
private string _dayFormat = "%d";
private bool _dayVisible = true;
private DateTimeOffset _maxYear;
private DateTimeOffset _minYear;
private string _monthFormat = "MMMM";
private bool _monthVisible = true;
private string _yearFormat = "yyyy";
private bool _yearVisible = true;
private DateTimeOffset _initDate;
private bool _suppressUpdateSelection;
}
}

31
src/Avalonia.Controls/DateTimePickers/DatePickerPresenterItem.cs

@ -1,31 +0,0 @@
using Avalonia;
using System;
namespace Avalonia.Controls
{
public sealed class DatePickerPresenterItem : AvaloniaObject
{
internal DatePickerPresenterItem(DateTimeOffset date)
{
_date = date;
}
public static readonly StyledProperty<string> DisplayTextProperty =
AvaloniaProperty.Register<DatePickerPresenterItem, string>("DisplayText");
public string DisplayText
{
get => GetValue(DisplayTextProperty);
set => SetValue(DisplayTextProperty, value);
}
internal DateTimeOffset GetStoredDate() => _date;
internal void UpdateStoredDate(DateTimeOffset newDate, string text)
{
_date = newDate;
DisplayText = text;
}
private DateTimeOffset _date;
}
}

19
src/Avalonia.Controls/DateTimePickers/DatePickerValueChangedEventArgs.cs

@ -1,19 +0,0 @@
using System;
namespace Avalonia.Controls
{
/// <summary>
/// Defines the argument passed when the Date changes on the <see cref="DatePickerPresenter"/>
/// </summary>
public class DatePickerValueChangedEventArgs
{
public DateTimeOffset NewDate { get; }
public DateTimeOffset OldDate { get; }
public DatePickerValueChangedEventArgs(DateTimeOffset oldDate, DateTimeOffset newDate)
{
NewDate = newDate;
OldDate = OldDate;
}
}
}

259
src/Avalonia.Controls/DateTimePickers/LoopingPanel.cs

@ -1,259 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using System;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Defines the Panel used by the <see cref="LoopingSelector"/>
/// </summary>
public sealed class LoopingPanel : Panel, ILogicalScrollable
{
public LoopingPanel(LoopingSelector owner)
{
_owner = owner;
}
protected override Size MeasureOverride(Size availableSize)
{
//It's assumed here that availableSize will have finite values
//If used in the DatePickerPresenter or TimePickerPresenter, this
//is met and works fine. If used elsewhere, ensure this is met
//Width is required for the Items & height is required for the viewportsize
if (double.IsInfinity(availableSize.Width) || double.IsInfinity(availableSize.Height))
throw new InvalidOperationException("LoopingPanel needs finite bounds");
//For the measure pass, remember we only have a subset of the total items
//available. So we need to ask the LoopingSelector for it's Item count
//return a size based on the extent of all items
var itmHgt = _owner.ItemHeight;
var itemCt = _owner.ItemCount;
var children = Children;
var itemWid = availableSize.Width;
for (int i = 0; i < children.Count; i++)
{
//Ensure we have a proper size when measuring
(children[i] as LoopingSelectorItem).Width = itemWid;
(children[i] as LoopingSelectorItem).Height = itmHgt;
children[i].Measure(availableSize);
}
var hei = itmHgt * itemCt;
if (_owner.ShouldLoop)
{
//WinUI preps for somewhere around 1000 items? in loop mode, then positions the offset in the middle
//based on the SelectedItem index, that way scrolling is enabled in both directions
//Here we prep for 10 * ItemCount & position in middle to start
_extent = new Size(0, 10 * (itmHgt * itemCt) + (availableSize.Height - itmHgt));
_viewport = new Size(0, availableSize.Height);
if (!_hasInitLoop)
{
var selIndex = _owner.SelectedIndex;
selIndex = selIndex == -1 ? 0 : selIndex;
//We know we are measuring for 10x items,
//so our init index is the selecteditems' index * 5
if (double.IsNaN(initOffset))
{
_offset = new Vector(0, (selIndex * itmHgt) * 5);
}
else
{
_offset = new Vector(0, initOffset);
initOffset = double.NaN;
}
_hasInitLoop = true;
}
}
else
{
//SelectedItem is in the middle of the LoopingSelector, so we need to account for that
//so all items can end up in this position
_extent = new Size(0, hei + (availableSize.Height - itmHgt));
_viewport = new Size(0, availableSize.Height);
if (!double.IsNaN(initOffset))
{
_offset = new Vector(0, initOffset);
initOffset = double.NaN;
}
}
//Total items visible, whether fully or partially visible
_totalItemsInViewport = (int)Math.Ceiling(_viewport.Height / itmHgt);
RaiseScrollInvalidated(null);
return _extent;
}
protected override Size ArrangeOverride(Size finalSize)
{
if (_owner == null || Children.Count == 0)
return base.ArrangeOverride(finalSize);
var itemWid = finalSize.Width;
var itemHgt = _owner.ItemHeight;
var initY = (finalSize.Height / 2.0) - (itemHgt / 2.0);
var children = Children;
var offY = Offset.Y;
//When not looping, currentSet will always be 0
//When looping, we measure for 10x _owner.ItemCount, so we need to figure out
//which "set" of items we're in based on the offset so we know where to properly
//place items
var singleExtent = _owner.ItemCount * itemHgt;
var currentSet = Math.Truncate(offY / singleExtent);
int selIndex = _owner.SelectedIndex;
selIndex = selIndex == -1 ? 0 : selIndex;
//When looping, the selected item should always be the middle item, equivalent in index
//to _totalItemsInViewport
//When not looping, if we're near the beginning of the list, our first item may be less than
//_totalItemsInViewport, so we need to make sure in that case to make the selected item, the
//actual selected index
var childIndexOfSelected = _totalItemsInViewport;
if (!_owner.ShouldLoop && selIndex < _totalItemsInViewport)
childIndexOfSelected = selIndex;
//Our selected container forms our "anchor" and all other containers are placed around this
IControl containerOfSelected = children[childIndexOfSelected];
//Then we need to know how many containers are above and below the selected item
//Should be _totalItemsInViewport for both, unless not looping & near items start
//# containers above is just the index of the selected item container
var numContainersAboveSelected = childIndexOfSelected;
//Move initY to where we actually want to start placing the items
initY += (singleExtent * currentSet) + (selIndex * itemHgt);
//We first arrange the selected item
Rect rc = new Rect(0, initY - offY, itemWid, itemHgt);
containerOfSelected.Arrange(rc);
//Arrange all items above
var prevY = initY - itemHgt;
for (int i = numContainersAboveSelected - 1; i >= 0; i--)
{
rc = new Rect(0, prevY - offY, itemWid, itemHgt);
children[i].Arrange(rc);
prevY -= itemHgt;
}
//Finally arrange all items below
var nextY = initY + itemHgt;
for (int i = childIndexOfSelected + 1; i < children.Count; i++)
{
rc = new Rect(0, nextY - offY, itemWid, itemHgt);
children[i].Arrange(rc);
nextY += itemHgt;
}
return new Size(itemWid, _extent.Height);
}
public bool CanHorizontallyScroll { get => false; set => _ = value; }
public bool CanVerticallyScroll { get => true; set => _ = value; }
public bool IsLogicalScrollEnabled => true;
public Size ScrollSize => new Size(0, _owner?.ItemHeight ?? 32);
//4 items
public Size PageScrollSize => new Size(0, _owner?.ItemHeight * 4 ?? 128);
public Size Extent => _extent;
public Vector Offset
{
get => _offset;
set
{
if (Extent.Height == 0)
{
initOffset = value.Y;
return;
}
var old = _offset.Y;
_offset = value;
if (Children.Count == 0)
return;
var itemHgt = _owner.ItemHeight;
var totalItemCount = _owner.ItemCount;
if (_owner.ShouldLoop)
{
//If we're looping, we need to detect when we're approaching
//the min/max bounds of the scrollviewer & reset to the otherside
//to make sure we always have scrolling
//To do this, since we plan for 10x total items, we move if we're
//in the first or last "block" of items & return it to somewhere near the middle
if (value.Y > old) //Scrolling Down
{
var extentOne = totalItemCount * itemHgt;
var scrollableHeight = (_extent.Height - _viewport.Height);
if (value.Y >= scrollableHeight - extentOne)
{
_offset = new Vector(0, value.Y - (extentOne * 5));
old = old - (extentOne * 5);
}
}
else if (value.Y < old) //Scrolling Up
{
var extentOne = totalItemCount * itemHgt;
if (value.Y < extentOne)
{
_offset = new Vector(0, value.Y + (extentOne * 5));
old = old + (extentOne * 5);
}
}
}
_owner.SetSelectedIndexFromOffset(old, _offset.Y);
RaiseScrollInvalidated(EventArgs.Empty);
InvalidateArrange();
}
}
public Size Viewport => _viewport;
//Not used
public bool BringIntoView(IControl target, Rect targetRect)
{
return false;
}
//Not used
public IControl GetControlInDirection(NavigationDirection direction, IControl from)
{
return null;
}
public void RaiseScrollInvalidated(EventArgs e)
{
ScrollInvalidated?.Invoke(this, e);
}
private double initOffset = double.NaN;
private LoopingSelector _owner;
private Size _extent;
private Size _viewport;
private Vector _offset;
private bool _hasInitLoop;
private int _totalItemsInViewport;
public event EventHandler ScrollInvalidated;
}
}

711
src/Avalonia.Controls/DateTimePickers/LoopingSelector.cs

@ -1,711 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Interactivity;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
namespace Avalonia.Controls.Primitives
{
public delegate void SelectionChangedEventHandler(object sender, SelectionChangedEventArgs e);
/// <summary>
/// An items control with the ability for infinite looping
/// <para>
/// Supports UI virtualization, though doesn't inherit from ItemsControl so we manage containers and
/// realized items ourselves
/// </para>
/// <para>
/// In WinUI, this control isn't usable outside of the Date/Time Pickers, but here it technically can be
/// Note, it's default behavior is for the pickers though.
/// </para>
/// </summary>
public class LoopingSelector : TemplatedControl
{
public LoopingSelector()
{
_panel = new LoopingPanel(this);
LogicalChildren.Add(_panel);
AddHandler(LoopingSelectorItem.SelectedEvent, OnItemSelected);
this.GetObservable(BoundsProperty).Subscribe(x => OnBoundsChanged(x));
}
static LoopingSelector()
{
FocusableProperty.OverrideDefaultValue<LoopingSelector>(true);
ItemsProperty.Changed.AddClassHandler<LoopingSelector>((x, v) => x.OnItemsChanged(v));
}
/// <summary>
/// Defines the <see cref="Items"/> Property
/// </summary>
public static readonly DirectProperty<LoopingSelector, IEnumerable> ItemsProperty =
AvaloniaProperty.RegisterDirect<LoopingSelector, IEnumerable>("Items",
x => x.Items, (x, v) => x.Items = v);
/// <summary>
/// Defines the <see cref="ItemCount"/> Property
/// </summary>
public static readonly DirectProperty<LoopingSelector, int> ItemCountProperty =
AvaloniaProperty.RegisterDirect<LoopingSelector, int>("ItemCount",
x => x.ItemCount);
/// <summary>
/// Defines the <see cref="SelectedItem"/> Property
/// </summary>
public static readonly DirectProperty<LoopingSelector, object> SelectedItemProperty =
AvaloniaProperty.RegisterDirect<LoopingSelector, object>("SelectedItem",
x => x.SelectedItem, (x, v) => x.SelectedItem = v);
/// <summary>
/// Defines the <see cref="SelectedIndex"/> Property
/// </summary>
public static readonly DirectProperty<LoopingSelector, int> SelectedIndexProperty =
AvaloniaProperty.RegisterDirect<LoopingSelector, int>("SelectedIndex",
x => x.SelectedIndex, (x, v) => x.SelectedIndex = v);
//UWP/WinUI has ItemWidth, ignoring, will have items just fill the width of the container
/// <summary>
/// Defines the <see cref="ItemHeight"/> Property
/// </summary>
public static readonly DirectProperty<LoopingSelector, double> ItemHeightProperty =
AvaloniaProperty.RegisterDirect<LoopingSelector, double>("ItemHeight",
x => x.ItemHeight, (x, v) => x.ItemHeight = v);
/// <summary>
/// Defines the <see cref="ItemTemplate"/> Property
/// </summary>
public static readonly StyledProperty<IDataTemplate> ItemTemplateProperty =
AvaloniaProperty.Register<LoopingSelector, IDataTemplate>("ItemTemplate");
/// <summary>
/// Defines the <see cref="ShouldLoop"/> Property
/// </summary>
public static readonly DirectProperty<LoopingSelector, bool> ShouldLoopProperty =
AvaloniaProperty.RegisterDirect<LoopingSelector, bool>("ShouldLoop",
x => x.ShouldLoop, (x, v) => x.ShouldLoop = v);
/// <summary>
/// Gets or sets the Items
/// </summary>
public IEnumerable Items
{
get => _items;
set => SetAndRaise(ItemsProperty, ref _items, value);
}
/// <summary>
/// Gets the number of items
/// </summary>
public int ItemCount
{
get => _itemCount;
private set => SetAndRaise(ItemCountProperty, ref _itemCount, value);
}
/// <summary>
/// Gets or sets the SelectedIndex
/// </summary>
public int SelectedIndex
{
get => _selectedIndex;
set
{
if (Items == null || ItemCount == 0)
return;
var old = _selectedIndex;
SetAndRaise(SelectedIndexProperty, ref _selectedIndex, value);
var oldItem = _selectedItem;
if (value == -1)
{
_selectedItem = null;
}
else
{
if (Items is IList l)
_selectedItem = l[value];
else
_selectedItem = Items.ElementAt(value);
}
RaisePropertyChanged(SelectedItemProperty, oldItem, _selectedItem);
if (!_preventMovingScrollWhenSelecting)
UpdateOffset();
SelectionChangedEventArgs args = new SelectionChangedEventArgs(null, new object[] { oldItem }, new object[] { _selectedItem });
OnSelectionChanged(this, args);
}
}
/// <summary>
/// Gets or sets the SelectedItem
/// </summary>
public object SelectedItem
{
get
{
if (SelectedIndex == -1)
return null;
if (Items is IList l)
return l[SelectedIndex];
else
return Items.ElementAt(SelectedIndex);
}
set
{
if (value == null)
{
SelectedIndex = -1;
}
else
{
if (Items is IList l)
SelectedIndex = l.IndexOf(value);
else
SelectedIndex = Items.IndexOf(value);
}
}
}
/// <summary>
/// Gets or sets the height of the items
/// </summary>
public double ItemHeight
{
get => _itemHeight;
set
{
SetAndRaise(ItemHeightProperty, ref _itemHeight, value);
_totalItemsInViewport = (int)Math.Ceiling(Bounds.Height / (value == 0 ? 1 : value));
if (_totalItemsInViewport % 2 == 0)
_totalItemsInViewport += 1;
UpdateOffset();
}
}
/// <summary>
/// Gets or sets the item template
/// </summary>
public IDataTemplate ItemTemplate
{
get => GetValue(ItemTemplateProperty);
set => SetValue(ItemTemplateProperty, value);
}
/// <summary>
/// Gets or sets whether the items should loop
/// </summary>
public bool ShouldLoop
{
get => _shouldLoop;
set
{
SetAndRaise(ShouldLoopProperty, ref _shouldLoop, value);
}
}
/// <summary>
/// Raised when the SelectedItem/Index changes
/// </summary>
public event SelectionChangedEventHandler SelectionChanged;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_scroller != null)
{
_scroller.Content = null;
}
base.OnApplyTemplate(e);
_scroller = e.NameScope.Find<ScrollViewer>("Scroller");
_scroller.Content = _panel;
_upButton = e.NameScope.Find<RepeatButton>("UpButton");
if (_upButton != null)
{
_upButton.Click += OnUpButtonClick;
}
_downButton = e.NameScope.Find<RepeatButton>("DownButton");
if (_downButton != null)
{
_downButton.Click += OnDownButtonClick;
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Up:
if (ShouldLoop)
{
var selIndex = SelectedIndex;
selIndex--;
if (selIndex < 0)
selIndex += ItemCount;
SelectedIndex = selIndex;
}
else
{
SelectedIndex = Math.Max(0, SelectedIndex - 1);
}
e.Handled = true;
break;
case Key.Down:
if (ShouldLoop)
{
var selIndex = SelectedIndex;
selIndex++;
if (selIndex >= ItemCount)
selIndex -= ItemCount;
SelectedIndex = selIndex;
}
else
{
SelectedIndex = Math.Min(ItemCount, SelectedIndex + 1);
}
e.Handled = true;
break;
case Key.PageUp:
if (ShouldLoop)
{
var selIndex = SelectedIndex;
selIndex -= 4;
if (selIndex < 0)
selIndex += ItemCount;
SelectedIndex = selIndex;
}
else
{
SelectedIndex = Math.Max(0, SelectedIndex - 4);
}
e.Handled = true;
break;
}
base.OnKeyDown(e);
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
base.OnPointerPressed(e);
KeyboardDevice.Instance.SetFocusedElement(this, NavigationMethod.Pointer, KeyModifiers.None);
FocusManager.Instance.Focus(this, NavigationMethod.Pointer, KeyModifiers.None);
}
private void OnDownButtonClick(object sender, RoutedEventArgs e)
{
var selIndex = SelectedIndex;
if (selIndex == ItemCount - 1)
{
if (ShouldLoop)
SelectedIndex = 0;
}
else
{
SelectedIndex++;
}
e.Handled = true;
}
private void OnUpButtonClick(object sender, RoutedEventArgs e)
{
var selIndex = SelectedIndex;
if (selIndex == 0)
{
if (ShouldLoop)
SelectedIndex = ItemCount - 1;
}
else
{
SelectedIndex--;
}
e.Handled = true;
}
private void OnItemsChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.OldValue is INotifyCollectionChanged oldC)
{
oldC.CollectionChanged -= OnItemsCollectionChanged;
}
if (e.NewValue is INotifyCollectionChanged newC)
{
newC.CollectionChanged += OnItemsCollectionChanged;
}
//When the entire items list changes, reset selection quietly
_selectedIndex = -1;
_selectedItem = null;
if (Items is IList l)
{
ItemCount = l.Count;
}
else
{
ItemCount = Items.Count();
}
EnsureContainers();
UpdateOffset();
}
private void OnItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
{
switch (e.Action)
{
case NotifyCollectionChangedAction.Add:
ItemCount += e.NewItems.Count;
var index = e.NewStartingIndex;
EnsureContainers();
UpdateOffset();
break;
case NotifyCollectionChangedAction.Remove:
ItemCount -= e.OldItems.Count;
EnsureContainers();
UpdateOffset();
break;
case NotifyCollectionChangedAction.Reset:
_selectedIndex = 0;
_selectedItem = 0;
ItemCount = 0;
_panel.Children.Clear();
UpdateOffset();
break;
//TODO - items source should be static anyway
case NotifyCollectionChangedAction.Replace:
case NotifyCollectionChangedAction.Move:
throw new NotSupportedException("Can't move or replace items in ItemsCollection");
}
}
/// <summary>
/// Ensures we have the correct number of containers in the LoopingSelectorPanel
/// This will add, remove, or clear the panel as necessary
/// </summary>
private void EnsureContainers(bool setContent = true)
{
if (Bounds.Height == 0)
return;
int itemCount = ItemCount;
//How many containers we ideally want
int desiredItemsLoaded = (_totalItemsInViewport * 2) + 1;
var realizedContainerCount = _panel.Children.Count;
if (ShouldLoop)
{
//When looping we should ALWAYS have desiredItemsLoaded number of containers
//available
var delta = Math.Abs(realizedContainerCount - desiredItemsLoaded);
if (realizedContainerCount < desiredItemsLoaded) //Add more containers
{
List<LoopingSelectorItem> panelItems = new List<LoopingSelectorItem>();
for (int i = 0; i < delta; i++)
{
LoopingSelectorItem lsi = new LoopingSelectorItem();
lsi.Height = ItemHeight;
lsi.ContentTemplate = ItemTemplate;
panelItems.Add(lsi);
}
_panel.Children.AddRange(panelItems);
}
else if (realizedContainerCount > desiredItemsLoaded) //Remove extra containers
{
//Technically not needed now as we don't move the containers when looping, just
//swap content, but may be called if resized
_panel.Children.RemoveRange(realizedContainerCount - delta, delta);
}
}
else
{
//When we're not looping, things are a little trickier, if we're near the bounds of scrolling,
//We may not have desiredItemsLoaded containers, so we need to account for that
//NumContainers here should be in range [_totalItemsInViewport, desiredItemsLoaded]
//First index of realized items
int selIndex = SelectedIndex;
selIndex = selIndex == -1 ? 0 : selIndex;
int numItemsAboveSelected = _totalItemsInViewport;
if (selIndex - numItemsAboveSelected < 0)
numItemsAboveSelected = selIndex;
int numItemsBelowSelected = _totalItemsInViewport;
if (selIndex + _totalItemsInViewport >= ItemCount)
numItemsBelowSelected = ItemCount - selIndex - 1;
int neededContainers = numItemsBelowSelected + numItemsAboveSelected + 1;
int currentCount = _panel.Children.Count;
//Do we need containers?
var numContsToAddRemove = neededContainers - currentCount;
if (numContsToAddRemove > 0) //Add Containers
{
List<LoopingSelectorItem> panelItems = new List<LoopingSelectorItem>();
for (int i = 0; i < numContsToAddRemove; i++)
{
LoopingSelectorItem lsi = new LoopingSelectorItem();
lsi.Height = ItemHeight;
lsi.ContentTemplate = ItemTemplate;
panelItems.Add(lsi);
}
_panel.Children.AddRange(panelItems);
}
else if (numContsToAddRemove < 0) //Remove containers
{
numContsToAddRemove = Math.Abs(numContsToAddRemove);
_panel.Children.RemoveRange(currentCount - numContsToAddRemove, numContsToAddRemove);
}
}
if (setContent && ItemCount > 0)
SetItemContent();
}
/// <summary>
/// Sets the content of the loaded containers
/// </summary>
private void SetItemContent()
{
int itemCount = ItemCount;
if (ShouldLoop)
{
var selIndex = SelectedIndex;
var panelItems = _panel.Children;
int c = 0;
int index = selIndex == -1 ? -_totalItemsInViewport : selIndex - _totalItemsInViewport;
while (c < panelItems.Count)
{
if (index >= ItemCount)
index -= ItemCount;
if (index < 0)
index += ItemCount;
if (index == selIndex)
(panelItems[c] as LoopingSelectorItem).IsSelected = true;
else
(panelItems[c] as LoopingSelectorItem).IsSelected = false;
(panelItems[c] as LoopingSelectorItem).Content = GetElementAt(index);
c++;
index++;
}
}
else
{
//We first need the first item in the realized items...
var selIndex = SelectedIndex;
var firstIndex = Math.Max(0, selIndex - _totalItemsInViewport);
var panelItems = _panel.Children;
for (int i = 0; i < panelItems.Count; i++)
{
if (firstIndex == selIndex)
(panelItems[i] as LoopingSelectorItem).IsSelected = true;
else
(panelItems[i] as LoopingSelectorItem).IsSelected = false;
(panelItems[i] as LoopingSelectorItem).Content = GetElementAt(firstIndex);
firstIndex++;
}
}
}
/// <summary>
/// Handles recycling of containers
/// </summary>
private void RecycleContainersIfNecessaryOnScroll(double newOffset, double oldOffset)
{
var children = _panel.Children;
var initY = (Bounds.Height / 2.0) - (ItemHeight / 2.0);
var recThresTop = initY - (_totalItemsInViewport * ItemHeight);
var recThresBot = initY + (_totalItemsInViewport * ItemHeight);
var scrollChange = newOffset - oldOffset;
int numContsAbove = 0;
for(int i = 0; i < children.Count; i++)
{
if (children[i].Bounds.Bottom - scrollChange <= recThresTop)
numContsAbove++;
}
int numContsBelow = 0;
for (int i = 0; i < children.Count; i++)
{
if (children[i].Bounds.Bottom - scrollChange >= recThresBot)
numContsBelow++;
}
//var numContsAbove = children.Where(x => (x.Bounds.Bottom - scrollChange) <= recThresTop).Count();
//var numContsBelow = children.Where(x => (x.Bounds.Top - scrollChange) >= recThresBot).Count();
if (numContsAbove > 0)
{
var recycleCount = numContsAbove;
var lastItemContent = (children[children.Count - 1] as LoopingSelectorItem).Content;
var index = Items.IndexOf(lastItemContent);
_panel.Children.MoveRange(0, recycleCount, children.Count);
}
else if (numContsBelow > 0)
{
var recycleCount = numContsBelow;
var firstItemContent = (children[0] as LoopingSelectorItem).Content;
var index = Items.IndexOf(firstItemContent);
var paneItemCount = _panel.Children.Count;
_panel.Children.MoveRange(paneItemCount - recycleCount, recycleCount, 0);
}
//Probably not ideal to re-set every item's content, but trying to set
//the content of just the recycled items was doing weird things
//We have a small number of containers loaded, so this shouldn't have
//too big of impact
SetItemContent();
}
private object GetElementAt(int index)
{
if (index < 0 || index >= ItemCount)
return null;
if (Items is IList l)
return l[index];
else
return Items.Cast<object>().ToList()[index];
}
/// <summary>
/// Updates the scrollviewer offset when the selectedindex changed
/// </summary>
private void UpdateOffset()
{
if (_panel == null || ItemCount == 0)
return;
_preventUpdateSelection = true;
var oldOffY = _panel.Offset.Y;
if (ShouldLoop)
{
//We measure for 10x as many items, so when we set the SelectedIndex
//and need to change the offset, should set it towards the middle
//so we preserve scrolling in both directions
int selIndex = SelectedIndex;
selIndex = selIndex == -1 ? 0 : selIndex;
var extent = ItemCount * ItemHeight;
_panel.Offset = new Vector(0, (selIndex * ItemHeight) + (extent * 5));
}
else
{
//Not looping, just convert the SelectedIndex to an offset
//if -1, set to 0;
int selIndex = SelectedIndex;
if (ItemCount == 0 || SelectedIndex == -1)
_panel.Offset = new Vector(0, 0);
else
_panel.Offset = new Vector(0, selIndex * ItemHeight);
EnsureContainers(false);
}
RecycleContainersIfNecessaryOnScroll(_panel.Offset.Y, oldOffY);
_preventUpdateSelection = false;
}
/// <summary>
/// Updates the SelectedIndex when scrolling occurs
/// </summary>
/// <param name="offsetY"></param>
internal void SetSelectedIndexFromOffset(double oldOffsetY, double offsetY)
{
if (_preventUpdateSelection)
return;
_preventMovingScrollWhenSelecting = true;
if (ShouldLoop)
{
var extent = ItemCount * ItemHeight;
var numExtents = offsetY / extent;
numExtents = numExtents < 0 ? 0 : Math.Truncate(numExtents);
var pixelOffset = offsetY - extent * numExtents;
SelectedIndex = (int)(pixelOffset / ItemHeight);
RecycleContainersIfNecessaryOnScroll(offsetY, oldOffsetY);
}
else
{
SelectedIndex = (int)(offsetY / ItemHeight);
EnsureContainers(false);
RecycleContainersIfNecessaryOnScroll(offsetY, oldOffsetY);
}
_preventMovingScrollWhenSelecting = false;
}
private void OnItemSelected(object sender, RoutedEventArgs e)
{
var item = (e.Source as LoopingSelectorItem).Content;
SelectedItem = item;
}
protected virtual void OnSelectionChanged(object sender, SelectionChangedEventArgs e)
{
SelectionChanged?.Invoke(sender, e);
}
private void OnBoundsChanged(Rect x)
{
//Ideally we always want this to be odd, since the selected item is placed in the middle,
//so we have the same number of items above and below at all times
var itmHgt = ItemHeight;
_totalItemsInViewport = (int)Math.Ceiling(x.Height / (itmHgt == 0 ? 1 : itmHgt));
if (_totalItemsInViewport % 2 == 0)
_totalItemsInViewport += 1;
EnsureContainers();
}
//TemplateItems
private RepeatButton _downButton;
private RepeatButton _upButton;
private ScrollViewer _scroller;
private LoopingPanel _panel;
private int _totalItemsInViewport;
private IEnumerable _items;
private int _itemCount;
private int _selectedIndex = -1;
private object _selectedItem;
private double _itemHeight = 32;
private bool _shouldLoop = true;
private bool _preventUpdateSelection;
private bool _preventMovingScrollWhenSelecting;
}
}

58
src/Avalonia.Controls/DateTimePickers/LoopingSelectorItem.cs

@ -1,58 +0,0 @@
using System;
using Avalonia.Controls.Mixins;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Defines the containers used by the <see cref="LoopingSelector"/>
/// </summary>
public sealed class LoopingSelectorItem : ContentControl
{
static LoopingSelectorItem()
{
PressedMixin.Attach<LoopingSelectorItem>();
IsSelectedProperty.Changed.AddClassHandler<LoopingSelectorItem>((x, e) => x.OnIsSelectedChanged(e));
}
/// <summary>
/// Defines the <see cref="IsSelected"/> Property
/// </summary>
internal static readonly StyledProperty<bool> IsSelectedProperty =
AvaloniaProperty.Register<LoopingSelectorItem, bool>(nameof(IsSelected));
public static readonly RoutedEvent<RoutedEventArgs> SelectedEvent =
RoutedEvent.Register<LoopingSelectorItem, RoutedEventArgs>(nameof(Selected), RoutingStrategies.Bubble);
internal bool IsSelected
{
get => GetValue(IsSelectedProperty);
set => SetValue(IsSelectedProperty, value);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (e.GetCurrentPoint(this).Properties.PointerUpdateKind == PointerUpdateKind.LeftButtonReleased)
{
//The selection event only raises when invoked by pointer events
RaiseEvent(new RoutedEventArgs(SelectedEvent, this));
}
}
private void OnIsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
{
var newValue = (bool)e.NewValue;
PseudoClasses.Set(":selected", newValue);
}
public event EventHandler<RoutedEventArgs> Selected
{
add => AddHandler(SelectedEvent, value);
remove => RemoveHandler(SelectedEvent, value);
}
}
}

45
src/Avalonia.Controls/DateTimePickers/PickerPresenterBase.cs

@ -10,49 +10,6 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public abstract class PickerPresenterBase : TemplatedControl
{
/// <summary>
/// Raised when the AcceptButton is clicked on the Picker and the selected value is changed
/// </summary>
protected virtual void OnConfirmed()
{
}
/// <summary>
/// Gets whether the Accept/Dismiss buttons are shown in the picker
/// </summary>
/// <returns></returns>
protected virtual bool ShouldShowConfirmationButtons()
{
return true;
}
/// <summary>
/// Raised when the Popup opens
/// </summary>
public event EventHandler Opened;
/// <summary>
/// Raised when the Popup closes
/// </summary>
public event EventHandler Closed;
protected virtual void OnOpened()
{
Opened?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnClosed()
{
Closed?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Shows the popup for the PickerPresenter at the specified control.
/// </summary>
/// <param name="target"></param>
public abstract void ShowAt(Control target);
protected Popup _hostPopup;
}
}

270
src/Avalonia.Controls/DateTimePickers/TimePicker.cs

@ -18,274 +18,6 @@ namespace Avalonia.Controls
/// </summary>
public class TimePicker : TemplatedControl
{
public TimePicker()
{
PseudoClasses.Set(":hasnotime", true);
_presenter = new TimePickerPresenter();
_presenter.TimeChanged += OnPresenterTimeChanged;
//Init the clock property to the System setting
var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern;
if (timePattern.IndexOf("H") != -1)
_clockIdentifier = "24HourClock";
}
/// <summary>
/// Defines the <see cref="MinuteIncrement"/> Property
/// </summary>
public static readonly DirectProperty<TimePicker, int> MinuteIncrementProperty =
AvaloniaProperty.RegisterDirect<TimePicker, int>("MinuteIncrement", x => x.MinuteIncrement,
(x, v) => x.MinuteIncrement = v);
/// <summary>
/// Defines the <see cref="Header"/> Property
/// </summary>
public static readonly StyledProperty<object> HeaderProperty =
AvaloniaProperty.Register<DatePicker, object>("Header");
/// <summary>
/// Defines the <see cref="HeaderTemplate"/> Property
/// </summary>
public static readonly StyledProperty<IDataTemplate> HeaderTemplateProperty =
AvaloniaProperty.Register<DatePicker, IDataTemplate>("HeaderTemplate");
/// <summary>
/// Defines the <see cref="ClockIdentifier"/> Property
/// </summary>
public static readonly DirectProperty<TimePicker, string> ClockIdentifierProperty =
AvaloniaProperty.RegisterDirect<TimePicker, string>("ClockIdentifier", x => x.ClockIdentifier,
(x, v) => x.ClockIdentifier = v);
/// <summary>
/// Defines the <see cref="SelectedTime"/> Property
/// </summary>
public static readonly DirectProperty<TimePicker, TimeSpan?> SelectedTimeProperty =
AvaloniaProperty.RegisterDirect<TimePicker, TimeSpan?>("Time", x => x.SelectedTime, (x, v) => x.SelectedTime = v);
/// <summary>
/// Gets or sets the MinuteIncrement
/// </summary>
public int MinuteIncrement
{
get => _minuteIncrement;
set
{
if (value < 1 || value > 59)
throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59");
SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value);
SetSelectedTimeText();
}
}
/// <summary>
/// Gets or sets the Header
/// </summary>
public object Header
{
get => GetValue(HeaderProperty);
set => SetValue(HeaderProperty, value);
}
/// <summary>
/// Gets or sets the HeaderTemplate
/// </summary>
public IDataTemplate HeaderTemplate
{
get => GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
/// <summary>
/// Gets or sets the ClockIdentifier, either 12HourClock or 24HourClock
/// </summary>
public string ClockIdentifier
{
get => _clockIdentifier;
set
{
if (!(string.IsNullOrEmpty(value) || value == "" || value == "12HourClock" || value == "24HourClock"))
throw new ArgumentException("Invalid ClockIdentifier");
SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value);
SetGrid();
SetSelectedTimeText();
}
}
public TimeSpan? SelectedTime
{
get => _selectedTime;
set
{
var old = _selectedTime;
SetAndRaise(SelectedTimeProperty, ref _selectedTime, value);
OnSelectedTimeChanged(old, value);
SetSelectedTimeText();
}
}
/// <summary>
/// Raised when the <see cref="SelectedTime"/> property changes
/// </summary>
public event EventHandler<TimePickerSelectedValueChangedEventArgs> SelectedTimeChanged;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_flyoutButton = e.NameScope.Find<Button>("FlyoutButton");
_firstPickerHost = e.NameScope.Find<Border>("FirstPickerHost");
_secondPickerHost = e.NameScope.Find<Border>("SecondPickerHost");
_thirdPickerHost = e.NameScope.Find<Border>("ThirdPickerHost");
_hourText = e.NameScope.Find<TextBlock>("HourTextBlock");
_minuteText = e.NameScope.Find<TextBlock>("MinuteTextBlock");
_periodText = e.NameScope.Find<TextBlock>("PeriodTextBlock");
_firstSplitter = e.NameScope.Find<Rectangle>("FirstColumnDivider");
_secondSplitter = e.NameScope.Find<Rectangle>("SecondColumnDivider");
_contentGrid = e.NameScope.Find<Grid>("FlyoutButtonContentGrid");
if (_flyoutButton != null)
_flyoutButton.Click += OnFlyoutButtonClicked;
SetGrid();
SetSelectedTimeText();
}
protected virtual void OnSelectedTimeChanged(TimeSpan? oldTime, TimeSpan? newTime)
{
SelectedTimeChanged?.Invoke(this, new TimePickerSelectedValueChangedEventArgs(oldTime, newTime));
}
private void SetGrid()
{
if (_contentGrid == null)
return;
//This is much simpler than the DatePicker to setup
//Hour and minute selectors are always present, and the period
//selector only appears if we're using a 12HourClock
bool use24HourClock = ClockIdentifier == "24HourClock";
if (!use24HourClock)
{
_contentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*,Auto,*");
_thirdPickerHost.IsVisible = true;
_secondSplitter.IsVisible = true;
Grid.SetColumn(_firstPickerHost, 0);
Grid.SetColumn(_secondPickerHost, 2);
Grid.SetColumn(_thirdPickerHost, 4);
Grid.SetColumn(_firstSplitter, 1);
Grid.SetColumn(_secondSplitter, 3);
}
else
{
_contentGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*");
_thirdPickerHost.IsVisible = false;
_secondSplitter.IsVisible = false;
Grid.SetColumn(_firstPickerHost, 0);
Grid.SetColumn(_secondPickerHost, 2);
Grid.SetColumn(_firstSplitter, 1);
}
}
private void SetSelectedTimeText()
{
if (_hourText == null || _minuteText == null || _periodText == null)
return;
var time = SelectedTime;
if (time.HasValue)
{
_hourText.Text = GetTimeFormat(time.Value, true, ClockIdentifier);
_minuteText.Text = GetTimeFormat(time.Value);
PseudoClasses.Set(":hasnotime", false);
if (time.Value.Hours >= 12)
_periodText.Text = CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator;
else
_periodText.Text = CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
}
else
{
_hourText.Text = "hour";
_minuteText.Text = "minute";
PseudoClasses.Set(":hasnotime", true);
if (DateTime.Now.Hour >= 12)
_periodText.Text = CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator;
else
_periodText.Text = CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
}
}
/// <summary>
/// Helps formatting timespans for use in TimePicker and TimePickerPresenter
/// </summary>
/// <param name="timeSpan">Timespan to format</param>
/// <param name="forHour">True if formatting hour, false if minute</param>
/// <param name="clockIdentifier"></param>
/// <returns></returns>
internal static string GetTimeFormat(TimeSpan timeSpan, bool forHour = false, string clockIdentifier = "12HourClock")
{
var fmt = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern.ToLower();
if (forHour)
{
var hr = timeSpan.Hours;
if (clockIdentifier == "12HourClock" && hr > 12)
hr -= 12;
else if (clockIdentifier == "12HourClock" && hr == 0)
hr = 12;
return fmt.Contains("hh") ? hr.ToString("D2") : hr.ToString();
}
else
{
return timeSpan.ToString("mm");
}
}
private void OnFlyoutButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_presenter.ClockIdentifier = ClockIdentifier;
_presenter.MinuteIncrement = MinuteIncrement;
_presenter.Time = SelectedTime.HasValue ? SelectedTime.Value : DateTime.Now.TimeOfDay;
_presenter.ShowAt(this);
}
private void OnPresenterTimeChanged(object sender, TimePickerValueChangedEventArgs e)
{
SelectedTime = e.NewTime;
}
//Template Items
private Button _flyoutButton;
private Border _firstPickerHost;
private Border _secondPickerHost;
private Border _thirdPickerHost;
private TextBlock _hourText;
private TextBlock _minuteText;
public TextBlock _periodText;
private Rectangle _firstSplitter;
private Rectangle _secondSplitter;
private Grid _contentGrid;
private TimePickerPresenter _presenter;
private TimeSpan? _selectedTime;
private int _minuteIncrement = 1;
private string _clockIdentifier = "12HourClock";
}
}

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

@ -21,474 +21,6 @@ namespace Avalonia.Controls
/// </summary>
public class TimePickerPresenter : PickerPresenterBase
{
public TimePickerPresenter()
{
Time = DateTime.Now.TimeOfDay;
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
}
/// <summary>
/// Defines the <see cref="MinuteIncrement"/> Property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, int> MinuteIncrementProperty =
AvaloniaProperty.RegisterDirect<TimePickerPresenter, int>("MinuteIncrement", x => x.MinuteIncrement,
(x, v) => x.MinuteIncrement = v);
/// <summary>
/// Defines the <see cref="ClockIdentifier"/> Property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, string> ClockIdentifierProperty =
AvaloniaProperty.RegisterDirect<TimePickerPresenter, string>("ClockIdentifier", x => x.ClockIdentifier,
(x, v) => x.ClockIdentifier = v);
/// <summary>
/// Defines the <see cref="Time"/> Property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, TimeSpan> TimeProperty =
AvaloniaProperty.RegisterDirect<TimePickerPresenter, TimeSpan>("Time", x => x.Time, (x, v) => x.Time = v);
/// <summary>
/// Defines the <see cref="SelectorItemTemplate"/> Property
/// </summary>
public static readonly StyledProperty<IDataTemplate> SelectorItemTemplateProperty =
AvaloniaProperty.Register<TimePickerPresenter, IDataTemplate>("SelectorItemTemplate");
/// <summary>
/// Gets or sets the MinuteIncrement
/// </summary>
public int MinuteIncrement
{
get => _minuteIncrement;
set
{
if (value < 1 || value > 59)
throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59");
SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value);
_hasMinuteIncChanged = true;
}
}
/// <summary>
/// Gets or sets the clock identifier, either 12HourClock or 24HourClock
/// </summary>
public string ClockIdentifier
{
get => _clockIdentifier;
set
{
if (!(string.IsNullOrEmpty(value) || value == "" || value == "12HourClock" || value == "24HourClock"))
throw new ArgumentException("Invalid ClockIdentifier");
SetAndRaise(ClockIdentifierProperty, ref _clockIdentifier, value);
_hasClockChanged = true;
}
}
/// <summary>
/// Gets or sets the time in the selectors
/// </summary>
public TimeSpan Time
{
get => _Time;
set
{
var old = _Time;
SetAndRaise(TimeProperty, ref _Time, value);
}
}
/// <summary>
/// Gets or sets the template for items in the selectors
/// </summary>
public IDataTemplate SelectorItemTemplate
{
get => GetValue(SelectorItemTemplateProperty);
set => SetValue(SelectorItemTemplateProperty, value);
}
/// <summary>
/// Raised when the AcceptButton is clicked and the time changes
/// </summary>
public event EventHandler<TimePickerValueChangedEventArgs> TimeChanged;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
//If template is reapplied (theme change, etc.), remove the looping selector
//from the existing host, placement logic is a lot nice here compared to DatePicker,
//so we don't destroy the loopingselectors
if(_firstPickerHost != null && _firstPickerHost.Child != null)
{
_firstPickerHost.Child = null;
}
if (_secondPickerHost != null && _secondPickerHost.Child != null)
{
_secondPickerHost.Child = null;
}
if (_thirdPickerHost != null && _thirdPickerHost.Child != null)
{
_thirdPickerHost.Child = null;
}
base.OnApplyTemplate(e);
//Requirement, throw if not found
_pickerGrid = e.NameScope.Get<Grid>("PickerHost");
_firstPickerHost = e.NameScope.Get<Border>("FirstPickerHost");
_secondPickerHost = e.NameScope.Get<Border>("SecondPickerHost");
_thirdPickerHost = e.NameScope.Get<Border>("ThirdPickerHost");
_firstSplitter = e.NameScope.Find<Rectangle>("FirstPickerSpacing");
_secondSplitter = e.NameScope.Find<Rectangle>("SecondPickerSpacing");
_acceptButton = e.NameScope.Find<Button>("AcceptButton");
_dismissButton = e.NameScope.Find<Button>("DismissButton");
if (_acceptButton != null)
_acceptButton.Click += OnAcceptButtonClicked;
if (_dismissButton != null)
_dismissButton.Click += OnDismissButtonClicked;
}
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
_hostPopup.IsOpen = false;
e.Handled = true;
break;
case Key.Tab:
var nextFocus = KeyboardNavigationHandler.GetNext(FocusManager.Instance.Current, NavigationDirection.Next);
KeyboardDevice.Instance?.SetFocusedElement(nextFocus, NavigationMethod.Tab, KeyModifiers.None);
e.Handled = true;
break;
case Key.Enter:
OnConfirmed();
e.Handled = true;
break;
}
base.OnKeyDown(e);
}
protected override void OnConfirmed()
{
var hr = (_hourSelector.SelectedItem as TimePickerPresenterItem).GetStoredTime().Hours;
var min = (_minuteSelector.SelectedItem as TimePickerPresenterItem).GetStoredTime().Minutes;
var period = _periodSelector != null ? _periodSelector.SelectedIndex : -1;
//Adjust hour to store correctly when using 12HourClock
if (ClockIdentifier == "12HourClock") //PM
{
if (hr == 12 && period == 0)
hr = 0;
else if (period == 1)
hr = hr == 12 ? 12 : hr + 12;
}
Time = new TimeSpan(hr, min, 0);
OnTimeChanged(new TimePickerValueChangedEventArgs(_initTime, Time));
_hostPopup.IsOpen = false;
}
protected virtual void OnTimeChanged(TimePickerValueChangedEventArgs args)
{
TimeChanged?.Invoke(this, args);
}
/// <inheritdoc/>
public override void ShowAt(Control target)
{
if (_hostPopup == null)
{
_hostPopup = new Popup();
_hostPopup.Child = this;
_hostPopup.PlacementMode = PlacementMode.Bottom;
_hostPopup.StaysOpen = false;
((ISetLogicalParent)_hostPopup).SetParent(target);
_hostPopup.Closed += OnPopupClosed;
_hostPopup.WindowManagerAddShadowHint = false;
_hostPopup.Focusable = false;
}
if (target == null)
throw new ArgumentNullException("Target cannot be null");
_hostPopup.PlacementTarget = target;
//Need to open the popup first, so the template is applied & our
//template items are available
_hostPopup.IsOpen = true;
EnsureSelectorsAndItems();
SetGrid();
//Set focus on HourContainer
KeyboardDevice.Instance?.SetFocusedElement(_hourSelector, NavigationMethod.Pointer, KeyModifiers.None);
_initTime = Time;
SetInitialSelection();
OnOpened();
//Dynamic position logic for popup
//Get item height from a hour looping selector (always available)
//Get the max height of the popup (constrained in template) and subtract the accept/dismiss region out of that
//Popup is placed below the control, so we subtract (half of the remaining distance + half an item)
var itemHeight = _hourSelector.ItemHeight;
var maxHeight = MaxHeight;
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
var deltaY = -(maxHeight - acceptDismissButtonHeight) / 2 - itemHeight / 2;
//The extra 5 px I think is related to default popup placement behavior
_hostPopup.Host.ConfigurePosition(_hostPopup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5),
Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);
}
/// <summary>
/// Ensures selectors and items are creates and ready to go
/// </summary>
private void EnsureSelectorsAndItems()
{
Contract.Requires<NullReferenceException>(_pickerGrid != null);
var clock = ClockIdentifier;
if (_hourSelector == null)
{
_hourSelector = new LoopingSelector();
_hourSelector.ShouldLoop = true;
_hourSelector.ItemTemplate = SelectorItemTemplate;
_firstPickerHost.Child = _hourSelector;
if (_hourItems == null)
{
_hourItems = new AvaloniaList<TimePickerPresenterItem>();
int numItems = clock == "12HourClock" ? 12 : 24;
for (int i = 0; i < numItems; i++)
{
var hr = clock == "12HourClock" ? TimeSpan.FromHours(i + 1) : TimeSpan.FromHours(i);
TimePickerPresenterItem tppi = new TimePickerPresenterItem(hr);
tppi.DisplayText = TimePicker.GetTimeFormat(hr, true, clock);
_hourItems.Add(tppi);
}
_hourSelector.Items = _hourItems;
}
else if (_hasClockChanged)
{
_hourItems.Clear();
int numItems = clock == "12HourClock" ? 12 : 24;
for (int i = 0; i < numItems; i++)
{
var hr = clock == "12HourClock" ? TimeSpan.FromHours(i + 1) : TimeSpan.FromHours(i);
TimePickerPresenterItem tppi = new TimePickerPresenterItem(hr);
tppi.DisplayText = TimePicker.GetTimeFormat(hr, true, clock);
_hourItems.Add(tppi);
}
}
}
if (_hourSelector.Parent == null)
_firstPickerHost.Child = _hourSelector;
if (_minuteSelector == null)
{
_minuteSelector = new LoopingSelector();
_minuteSelector.ShouldLoop = true;
_minuteSelector.ItemTemplate = SelectorItemTemplate;
_secondPickerHost.Child = _minuteSelector;
}
if (_minuteSelector.Parent == null)
_secondPickerHost.Child = _minuteSelector;
//Ensure minute selector items are up to date
if (_minuteItems == null)
{
_minuteItems = new AvaloniaList<TimePickerPresenterItem>();
var inc = MinuteIncrement;
for (int i = 0; i < 60; i += inc)
{
var min = TimeSpan.FromMinutes(i);
TimePickerPresenterItem tppi = new TimePickerPresenterItem(min);
tppi.DisplayText = TimePicker.GetTimeFormat(min);
_minuteItems.Add(tppi);
}
_minuteSelector.Items = _minuteItems;
}
else if (_hasMinuteIncChanged)
{
_minuteItems.Clear();
var inc = MinuteIncrement;
for (int i = 0; i < 60; i += inc)
{
var min = TimeSpan.FromMinutes(i);
TimePickerPresenterItem tppi = new TimePickerPresenterItem(min);
tppi.DisplayText = TimePicker.GetTimeFormat(min);
_minuteItems.Add(tppi);
}
}
if (clock == "12HourClock")
{
if(_periodSelector == null)
{
_periodSelector = new LoopingSelector();
_periodSelector.ShouldLoop = false;
_periodSelector.ItemTemplate = SelectorItemTemplate;
}
if(_periodSelector.Parent == null)
_thirdPickerHost.Child = _periodSelector;
if (_periodItems == null || _periodItems.Count == 0)
{
_periodItems = new AvaloniaList<TimePickerPresenterItem>();
TimePickerPresenterItem amItem = new TimePickerPresenterItem(TimeSpan.Zero);
amItem.DisplayText = CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
TimePickerPresenterItem pmItem = new TimePickerPresenterItem(TimeSpan.Zero);
pmItem.DisplayText = CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator;
_periodItems.Add(amItem);
_periodItems.Add(pmItem);
_periodSelector.Items = _periodItems;
}
}
else if (clock == "24HourClock")
{
_thirdPickerHost.Child = null;
if(_periodSelector != null)
{
_periodSelector.Items = null;
_periodSelector = null;
}
if (_periodItems != null && _periodItems.Count > 0)
_periodItems.Clear();
}
_hasMinuteIncChanged = false;
_hasClockChanged = false;
}
/// <summary>
/// Sets the selector container grid
/// </summary>
private void SetGrid()
{
if (ClockIdentifier == "12HourClock")
{
_pickerGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*,Auto,*");
_secondSplitter.IsVisible = true;
}
else
{
_pickerGrid.ColumnDefinitions = new ColumnDefinitions("*,Auto,*");
_secondSplitter.IsVisible = false;
}
}
/// <summary>
/// Sets the SelectedIndex of the selectors when first loading
/// </summary>
private void SetInitialSelection()
{
//Set selection on Hour & Period Selectors
if (ClockIdentifier == "12HourClock")
{
var hr = Time.Hours;
_periodSelector.SelectedIndex = hr >= 12 ? 1 : 0;
if (hr == 0)
hr += 12;
if (hr > 12)
hr -= 12;
_hourSelector.SelectedIndex = hr - 1;
}
else
{
var hr = Time.Hours;
_hourSelector.SelectedIndex = hr;
}
//Set selection on Minute Selector
//This can be trickier
//If MinuteIncrement != 1, we can't necessarily set the SelectedIndex
//Instead, we need to find the closest item, and set it to that
var minInc = MinuteIncrement;
var min = Time.Minutes;
if (minInc == 1)
{
_minuteSelector.SelectedIndex = min;
}
else
{
//Basically, Get the minutes by their increment, we just regenerate here for simplicity
//Ascending sort by the difference between the current minute & item then get the first item
//Then find the index of that item
var items = Enumerable.Range(0, 60).Where(i => i % minInc == 0);
var nearest = items.OrderBy(x => Math.Abs(x - min)).First();
_minuteSelector.SelectedIndex = items.ToList().IndexOf(nearest);
}
}
private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_hostPopup.IsOpen = false;
}
private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
OnConfirmed();
}
private void OnPopupClosed(object sender, PopupClosedEventArgs e)
{
_hostPopup.PlacementTarget.Focus();
KeyboardDevice.Instance?.SetFocusedElement(_hostPopup.PlacementTarget, NavigationMethod.Pointer, KeyModifiers.None);
OnClosed();
}
//ItemsLists
private IList<TimePickerPresenterItem> _hourItems;
private IList<TimePickerPresenterItem> _minuteItems;
private IList<TimePickerPresenterItem> _periodItems;
//Template Items
private Button _acceptButton;
private Button _dismissButton;
private Border _firstPickerHost;
private Border _secondPickerHost;
private Border _thirdPickerHost;
private Rectangle _firstSplitter;
private Rectangle _secondSplitter;
private Grid _pickerGrid;
//Selectors
private LoopingSelector _hourSelector;
private LoopingSelector _minuteSelector;
private LoopingSelector _periodSelector;
private TimeSpan _initTime;
private bool _hasMinuteIncChanged;
private bool _hasClockChanged;
private TimeSpan _Time;
private int _minuteIncrement;
private string _clockIdentifier;
}
}

27
src/Avalonia.Controls/DateTimePickers/TimePickerPresenterItem.cs

@ -1,27 +0,0 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls
{
public sealed class TimePickerPresenterItem : AvaloniaObject
{
internal TimePickerPresenterItem(TimeSpan date)
{
_date = date;
}
public static readonly StyledProperty<string> DisplayTextProperty =
AvaloniaProperty.Register<TimePickerPresenterItem, string>("DisplayText");
public string DisplayText
{
get => GetValue(DisplayTextProperty);
set => SetValue(DisplayTextProperty, value);
}
internal TimeSpan GetStoredTime() => _date;
private TimeSpan _date;
}
}

2
src/Avalonia.Controls/DateTimePickers/TimePickerSelectedValueChangedEventArgs.cs

@ -1,6 +1,4 @@
using System;
using System.Collections.Generic;
using System.Text;
namespace Avalonia.Controls
{

15
src/Avalonia.Controls/DateTimePickers/TimePickerValueChangedEventArgs.cs

@ -1,15 +0,0 @@
using System;
namespace Avalonia.Controls
{
public class TimePickerValueChangedEventArgs
{
public TimeSpan OldTime { get; }
public TimeSpan NewTime { get; }
public TimePickerValueChangedEventArgs(TimeSpan old, TimeSpan newT)
{
OldTime = old;
NewTime = newT;
}
}
}
Loading…
Cancel
Save