diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs
index 5fdc3e5eb0..8b80cd8414 100644
--- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs
+++ b/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
-
///
/// A control to allow the user to select a time
///
public class TimePicker : TemplatedControl
{
-
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty MinuteIncrementProperty =
+ AvaloniaProperty.RegisterDirect("MinuteIncrement", x => x.MinuteIncrement,
+ (x, v) => x.MinuteIncrement = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty HeaderProperty =
+ AvaloniaProperty.Register("Header");
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly StyledProperty HeaderTemplateProperty =
+ AvaloniaProperty.Register("HeaderTemplate");
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty ClockIdentifierProperty =
+ AvaloniaProperty.RegisterDirect("ClockIdentifier", x => x.ClockIdentifier,
+ (x, v) => x.ClockIdentifier = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty SelectedTimeProperty =
+ AvaloniaProperty.RegisterDirect("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";
+ }
+
+ ///
+ /// Gets or sets the minute increment in the picker
+ ///
+ public int MinuteIncrement
+ {
+ get => _minuteIncrement;
+ set
+ {
+ if (value < 1 || value > 59)
+ throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59");
+ SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value);
+ SetSelectedTimeText();
+ }
+ }
+
+ ///
+ /// Gets or sets the header
+ ///
+ public object Header
+ {
+ get => GetValue(HeaderProperty);
+ set => SetValue(HeaderProperty, value);
+ }
+
+ ///
+ /// Gets or sets the header template
+ ///
+ public IDataTemplate HeaderTemplate
+ {
+ get => GetValue(HeaderTemplateProperty);
+ set => SetValue(HeaderTemplateProperty, value);
+ }
+
+ ///
+ /// Gets or sets the clock identifier, either 12HourClock or 24HourClock
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Gets or sets the selected time. Can be null.
+ ///
+ public TimeSpan? SelectedTime
+ {
+ get => _selectedTime;
+ set
+ {
+ var old = _selectedTime;
+ SetAndRaise(SelectedTimeProperty, ref _selectedTime, value);
+ OnSelectedTimeChanged(old, value);
+ SetSelectedTimeText();
+ }
+ }
+
+ ///
+ /// Raised when the property changes
+ ///
+ public event EventHandler 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("FlyoutButton");
+
+ _firstPickerHost = e.NameScope.Find("FirstPickerHost");
+ _secondPickerHost = e.NameScope.Find("SecondPickerHost");
+ _thirdPickerHost = e.NameScope.Find("ThirdPickerHost");
+
+ _hourText = e.NameScope.Find("HourTextBlock");
+ _minuteText = e.NameScope.Find("MinuteTextBlock");
+ _periodText = e.NameScope.Find("PeriodTextBlock");
+
+ _firstSplitter = e.NameScope.Find("FirstColumnDivider");
+ _secondSplitter = e.NameScope.Find("SecondColumnDivider");
+
+ _contentGrid = e.NameScope.Find("FlyoutButtonContentGrid");
+
+ _popup = e.NameScope.Find("Popup");
+ _presenter = e.NameScope.Find("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;
+ }
+
}
}
diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
index ab615ed790..0788cbfe95 100644
--- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs
+++ b/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
///
public class TimePickerPresenter : PickerPresenterBase
{
-
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty MinuteIncrementProperty =
+ TimePicker.MinuteIncrementProperty.AddOwner(x => x.MinuteIncrement,
+ (x, v) => x.MinuteIncrement = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty ClockIdentifierProperty =
+ TimePicker.ClockIdentifierProperty.AddOwner(x => x.ClockIdentifier,
+ (x, v) => x.ClockIdentifier = v);
+
+ ///
+ /// Defines the property
+ ///
+ public static readonly DirectProperty TimeProperty =
+ AvaloniaProperty.RegisterDirect("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";
+
+ ///
+ /// Gets or sets the minute increment in the selector
+ ///
+ public int MinuteIncrement
+ {
+ get => _minuteIncrement;
+ set
+ {
+ if (value < 1 || value > 59)
+ throw new ArgumentOutOfRangeException("1 >= MinuteIncrement <= 59");
+ SetAndRaise(MinuteIncrementProperty, ref _minuteIncrement, value);
+ InitPicker();
+ }
+ }
+
+ ///
+ /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
+ ///
+ 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();
+ }
+ }
+
+ ///
+ /// Gets or sets the current time
+ ///
+ 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("PickerContainer");
+ _periodHost = e.NameScope.Get("PeriodHost");
+
+ _hourSelector = e.NameScope.Get("HourSelector");
+ _minuteSelector = e.NameScope.Get("MinuteSelector");
+ _periodSelector = e.NameScope.Get("PeriodSelector");
+
+ _spacer2 = e.NameScope.Get("SecondSpacer");
+
+ _acceptButton = e.NameScope.Get("AcceptButton");
+ _acceptButton.Click += OnAcceptButtonClicked;
+
+ _hourUpButton = e.NameScope.Find("HourUpButton");
+ if (_hourUpButton != null)
+ _hourUpButton.Click += OnSelectorButtonClick;
+ _hourDownButton = e.NameScope.Find("HourDownButton");
+ if (_hourDownButton != null)
+ _hourDownButton.Click += OnSelectorButtonClick;
+
+ _minuteUpButton = e.NameScope.Find("MinuteUpButton");
+ if (_minuteUpButton != null)
+ _minuteUpButton.Click += OnSelectorButtonClick;
+ _minuteDownButton = e.NameScope.Find("MinuteDownButton");
+ if (_minuteDownButton != null)
+ _minuteDownButton.Click += OnSelectorButtonClick;
+
+ _periodUpButton = e.NameScope.Find("PeriodUpButton");
+ if (_periodUpButton != null)
+ _periodUpButton.Click += OnSelectorButtonClick;
+ _periodDownButton = e.NameScope.Find("PeriodDownButton");
+ if (_periodDownButton != null)
+ _periodDownButton.Click += OnSelectorButtonClick;
+
+ _dismissButton = e.NameScope.Find("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);
+ }
}
}