Browse Source

New TimePicker/TimePickerPresenter

pull/4108/head
amwx 6 years ago
parent
commit
deff41b53d
  1. 288
      src/Avalonia.Controls/DateTimePickers/TimePicker.cs
  2. 259
      src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs

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

@ -1,6 +1,4 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using System;
@ -8,16 +6,288 @@ using System.Globalization;
namespace Avalonia.Controls
{
//TODO: WinUI issue proposal for DurationPicker
//https://github.com/microsoft/microsoft-ui-xaml/issues/1807
//Could just extend this and add other time units, but design idea for this
//is unknown
/// <summary>
/// A control to allow the user to select a time
/// </summary>
public class TimePicker : TemplatedControl
{
/// <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);
//Template Items
private TimePickerPresenter _presenter;
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 Popup _popup;
private TimeSpan? _selectedTime;
private int _minuteIncrement = 1;
private string _clockIdentifier = "12HourClock";
public TimePicker()
{
PseudoClasses.Set(":hasnotime", true);
var timePattern = CultureInfo.CurrentCulture.DateTimeFormat.ShortTimePattern;
if (timePattern.IndexOf("H") != -1)
_clockIdentifier = "24HourClock";
}
/// <summary>
/// Gets or sets the minute increment in the picker
/// </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 header template
/// </summary>
public IDataTemplate HeaderTemplate
{
get => GetValue(HeaderTemplateProperty);
set => SetValue(HeaderTemplateProperty, value);
}
/// <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);
SetGrid();
SetSelectedTimeText();
}
}
/// <summary>
/// Gets or sets the selected time. Can be null.
/// </summary>
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)
{
if (_flyoutButton != null)
_flyoutButton.Click -= OnFlyoutButtonClicked;
if(_presenter != null)
{
_presenter.Confirmed -= OnConfirmed;
_presenter.Dismissed -= OnDismissPicker;
}
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");
_popup = e.NameScope.Find<Popup>("Popup");
_presenter = e.NameScope.Find<TimePickerPresenter>("PickerPresenter");
if (_flyoutButton != null)
_flyoutButton.Click += OnFlyoutButtonClicked;
SetGrid();
SetSelectedTimeText();
if (_presenter != null)
{
_presenter.Confirmed += OnConfirmed;
_presenter.Dismissed += OnDismissPicker;
_presenter[!TimePickerPresenter.MinuteIncrementProperty] = this[!MinuteIncrementProperty];
_presenter[!TimePickerPresenter.ClockIdentifierProperty] = this[!ClockIdentifierProperty];
}
}
private void SetGrid()
{
if (_contentGrid == null)
return;
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)
{
var newTime = SelectedTime.Value;
bool clock12 = ClockIdentifier == "12HourClock";
if (ClockIdentifier == "12HourClock")
{
var hr = newTime.Hours;
hr = hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
newTime = new TimeSpan(hr, newTime.Minutes, 0);
}
_hourText.Text = newTime.ToString("%h");
_minuteText.Text = newTime.ToString("mm");
PseudoClasses.Set(":hasnotime", false);
_periodText.Text = time.Value.Hours >= 12 ? CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator :
CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
}
else
{
_hourText.Text = "hour";
_minuteText.Text = "minute";
PseudoClasses.Set(":hasnotime", true);
_periodText.Text = DateTime.Now.Hour >= 12 ? CultureInfo.CurrentCulture.DateTimeFormat.PMDesignator :
CultureInfo.CurrentCulture.DateTimeFormat.AMDesignator;
}
}
protected virtual void OnSelectedTimeChanged(TimeSpan? oldTime, TimeSpan? newTime)
{
SelectedTimeChanged?.Invoke(this, new TimePickerSelectedValueChangedEventArgs(oldTime, newTime));
}
private void OnFlyoutButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
_presenter.Time = SelectedTime.HasValue ? SelectedTime.Value : DateTime.Now.TimeOfDay;
_popup.IsOpen = true;
var deltaY = _presenter.GetOffsetForPopup();
//The extra 5 px I think is related to default popup placement behavior
_popup.Host.ConfigurePosition(_popup.PlacementTarget, PlacementMode.AnchorAndGravity, new Point(0, deltaY + 5),
Primitives.PopupPositioning.PopupAnchor.Bottom, Primitives.PopupPositioning.PopupGravity.Bottom,
Primitives.PopupPositioning.PopupPositionerConstraintAdjustment.SlideY);
}
private void OnDismissPicker(object sender, EventArgs e)
{
_popup.Close();
Focus();
}
private void OnConfirmed(object sender, EventArgs e)
{
_popup.Close();
SelectedTime = _presenter.Time;
}
}
}

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

@ -1,15 +1,8 @@
using Avalonia;
using Avalonia.Collections;
using Avalonia.Controls;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.VisualTree;
using Avalonia.Interactivity;
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace Avalonia.Controls
{
@ -21,6 +14,252 @@ namespace Avalonia.Controls
/// </summary>
public class TimePickerPresenter : PickerPresenterBase
{
/// <summary>
/// Defines the <see cref="MinuteIncrement"/> property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, int> MinuteIncrementProperty =
TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>(x => x.MinuteIncrement,
(x, v) => x.MinuteIncrement = v);
/// <summary>
/// Defines the <see cref="ClockIdentifier"/> property
/// </summary>
public static readonly DirectProperty<TimePickerPresenter, string> ClockIdentifierProperty =
TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>(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);
public TimePickerPresenter()
{
Time = DateTime.Now.TimeOfDay;
KeyboardNavigation.SetTabNavigation(this, KeyboardNavigationMode.Cycle);
}
//TemplateItems
private Grid _pickerContainer;
private Button _acceptButton;
private Button _dismissButton;
private Rectangle _spacer2;
private Panel _periodHost;
private DateTimePickerPanel _hourSelector;
private DateTimePickerPanel _minuteSelector;
private DateTimePickerPanel _periodSelector;
private Button _hourUpButton;
private Button _minuteUpButton;
private Button _periodUpButton;
private Button _hourDownButton;
private Button _minuteDownButton;
private Button _periodDownButton;
//Backing Fields
private TimeSpan _Time;
private int _minuteIncrement = 1;
private string _clockIdentifier = "12HourClock";
/// <summary>
/// Gets or sets the minute increment in the selector
/// </summary>
public int MinuteIncrement
{
get => _minuteIncrement;
set
{
if (value < 1 || value > 59)
throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59");
SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value);
InitPicker();
}
}
/// <summary>
/// Gets or sets the current 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);
InitPicker();
}
}
/// <summary>
/// Gets or sets the current time
/// </summary>
public TimeSpan Time
{
get => _Time;
set
{
var old = _Time;
SetAndRaise(TimeProperty, ref _Time, value);
InitPicker();
}
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_pickerContainer = e.NameScope.Get<Grid>("PickerContainer");
_periodHost = e.NameScope.Get<Panel>("PeriodHost");
_hourSelector = e.NameScope.Get<DateTimePickerPanel>("HourSelector");
_minuteSelector = e.NameScope.Get<DateTimePickerPanel>("MinuteSelector");
_periodSelector = e.NameScope.Get<DateTimePickerPanel>("PeriodSelector");
_spacer2 = e.NameScope.Get<Rectangle>("SecondSpacer");
_acceptButton = e.NameScope.Get<Button>("AcceptButton");
_acceptButton.Click += OnAcceptButtonClicked;
_hourUpButton = e.NameScope.Find<RepeatButton>("HourUpButton");
if (_hourUpButton != null)
_hourUpButton.Click += OnSelectorButtonClick;
_hourDownButton = e.NameScope.Find<RepeatButton>("HourDownButton");
if (_hourDownButton != null)
_hourDownButton.Click += OnSelectorButtonClick;
_minuteUpButton = e.NameScope.Find<RepeatButton>("MinuteUpButton");
if (_minuteUpButton != null)
_minuteUpButton.Click += OnSelectorButtonClick;
_minuteDownButton = e.NameScope.Find<RepeatButton>("MinuteDownButton");
if (_minuteDownButton != null)
_minuteDownButton.Click += OnSelectorButtonClick;
_periodUpButton = e.NameScope.Find<RepeatButton>("PeriodUpButton");
if (_periodUpButton != null)
_periodUpButton.Click += OnSelectorButtonClick;
_periodDownButton = e.NameScope.Find<RepeatButton>("PeriodDownButton");
if (_periodDownButton != null)
_periodDownButton.Click += OnSelectorButtonClick;
_dismissButton = e.NameScope.Find<Button>("DismissButton");
if (_dismissButton != null)
_dismissButton.Click += OnDismissButtonClicked;
InitPicker();
}
protected override void OnKeyDown(KeyEventArgs e)
{
switch (e.Key)
{
case Key.Escape:
OnDismiss();
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.SelectedValue;
var min = _minuteSelector.SelectedValue;
var per = _periodSelector.SelectedValue;
if (ClockIdentifier == "12HourClock")
{
hr = per == 1 ? hr + 12 : per == 0 && hr == 12 ? 0 : hr;
}
Time = new TimeSpan(hr, min, 0);
base.OnConfirmed();
}
private void InitPicker()
{
if (_pickerContainer == null)
return;
bool clock12 = ClockIdentifier == "12HourClock";
_hourSelector.MaximumValue = clock12 ? 12 : 23;
_hourSelector.MinimumValue = clock12 ? 1 : 0;
_hourSelector.ItemFormat = "%h";
var hr = Time.Hours;
_hourSelector.SelectedValue = !clock12 ? hr :
hr > 12 ? hr - 12 : hr == 0 ? 12 : hr;
_minuteSelector.MaximumValue = 59;
_minuteSelector.MinimumValue = 0;
_minuteSelector.Increment = MinuteIncrement;
_minuteSelector.SelectedValue = Time.Minutes;
_minuteSelector.ItemFormat = "mm";
_periodSelector.MaximumValue = 1;
_periodSelector.MinimumValue = 0;
_periodSelector.SelectedValue = hr >= 12 ? 1 : 0;
SetGrid();
KeyboardDevice.Instance?.SetFocusedElement(_hourSelector, NavigationMethod.Pointer, KeyModifiers.None);
}
private void SetGrid()
{
if (ClockIdentifier == "12HourClock")
{
_pickerContainer.ColumnDefinitions = new ColumnDefinitions("*,Auto,*,Auto,*");
_spacer2.IsVisible = true;
_periodHost.IsVisible = true;
}
else
{
_pickerContainer.ColumnDefinitions = new ColumnDefinitions("*,Auto,*");
_spacer2.IsVisible = false;
_periodHost.IsVisible = false;
}
}
private void OnDismissButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
OnDismiss();
}
private void OnAcceptButtonClicked(object sender, Avalonia.Interactivity.RoutedEventArgs e)
{
OnConfirmed();
}
private void OnSelectorButtonClick(object sender, RoutedEventArgs e)
{
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 == _periodUpButton)
_periodSelector.ScrollUp();
else if (sender == _periodDownButton)
_periodSelector.ScrollDown();
}
internal double GetOffsetForPopup()
{
var acceptDismissButtonHeight = _acceptButton != null ? _acceptButton.Bounds.Height : 41;
return -(MaxHeight - acceptDismissButtonHeight) / 2 - (_hourSelector.ItemHeight / 2);
}
}
}

Loading…
Cancel
Save