diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml index fc3ad9b8957..7a1bfaa824c 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml @@ -77,6 +77,23 @@ + A TimePicker with seconds enabled. + + + + + + + + + <TimePicker UseSeconds="True" /> + + + + + + @@ -85,8 +102,8 @@ - - A TimePicker with minute increments specified. + + A TimePicker with minute increment specified. @@ -96,7 +113,24 @@ - <TimePicker MinuteIncrement="15" /> + <TimePicker MinuteIncrement="15" SecondIncrement="30" /> + + + + + + + A TimePicker with seconds enabled and minute & second increments specified. + + + + + + + + + <TimePicker UseSeconds="True" MinuteIncrement="15" SecondIncrement="30" /> @@ -137,6 +171,40 @@ + A TimePicker using a 12-hour clock and seconds. + + + + + + + + + <TimePicker ClockIdentifier="12HourClock" UseSeconds="True" /> + + + + + + + A TimePicker using a 24-hour clock and seconds. + + + + + + + + + <TimePicker ClockIdentifier="24HourClock" UseSeconds="True" /> + + + + + + diff --git a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs index 7520dabf370..5c7ccc151b0 100644 --- a/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs +++ b/samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs @@ -15,7 +15,7 @@ public DateTimePickerPage() "Order of month, day, and year is dynamically set based on user date settings"; this.Get("TimePickerDesc").Text = "Use a TimePicker to let users set a time in your app, for example " + - "to set a reminder. The TimePicker displays three controls for hour, minute, and AM / PM(if necessary).These controls " + + "to set a reminder. The TimePicker displays four controls for hour, minute, seconds(optional), and AM / PM(if necessary).These controls " + "are easy to use with touch or mouse, and they can be styled and configured in several different ways. " + "12 - hour or 24 - hour clock and visibility of AM / PM is dynamically set based on user time settings, or can be overridden."; diff --git a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs index cbe2eddf538..e9cdd3039a3 100644 --- a/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs +++ b/src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs @@ -18,6 +18,7 @@ public enum DateTimePickerPanelType Day, Hour, Minute, + Second, TimePeriod //AM or PM } @@ -516,6 +517,8 @@ private string FormatContent(int value, DateTimePickerPanelType panelType) return new TimeSpan(value, 0, 0).ToString(ItemFormat); case DateTimePickerPanelType.Minute: return new TimeSpan(0, value, 0).ToString(ItemFormat); + case DateTimePickerPanelType.Second: + return new TimeSpan(0, 0, value).ToString(ItemFormat); case DateTimePickerPanelType.TimePeriod: return value == MinimumValue ? TimeUtils.GetAMDesignator() : TimeUtils.GetPMDesignator(); default: diff --git a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs index 62ac76e71ca..111c9ff6235 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePicker.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePicker.cs @@ -18,12 +18,15 @@ namespace Avalonia.Controls [TemplatePart("PART_FlyoutButtonContentGrid", typeof(Grid))] [TemplatePart("PART_HourTextBlock", typeof(TextBlock))] [TemplatePart("PART_MinuteTextBlock", typeof(TextBlock))] + [TemplatePart("PART_SecondTextBlock", typeof(TextBlock))] [TemplatePart("PART_PeriodTextBlock", typeof(TextBlock))] [TemplatePart("PART_PickerPresenter", typeof(TimePickerPresenter))] [TemplatePart("PART_Popup", typeof(Popup))] [TemplatePart("PART_SecondColumnDivider", typeof(Rectangle))] [TemplatePart("PART_SecondPickerHost", typeof(Border))] - [TemplatePart("PART_ThirdPickerHost", typeof(Border))] + [TemplatePart("PART_ThirdColumnDivider", typeof(Rectangle))] + [TemplatePart("PART_ThirdPickerHost", typeof(Border))] + [TemplatePart("PART_FourthPickerHost", typeof(Border))] [PseudoClasses(":hasnotime")] public class TimePicker : TemplatedControl { @@ -32,12 +35,24 @@ public class TimePicker : TemplatedControl /// public static readonly StyledProperty MinuteIncrementProperty = AvaloniaProperty.Register(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement); + + /// + /// Defines the property + /// + public static readonly StyledProperty SecondIncrementProperty = + AvaloniaProperty.Register(nameof(SecondIncrement), 1, coerce: CoerceSecondIncrement); /// /// Defines the property /// public static readonly StyledProperty ClockIdentifierProperty = AvaloniaProperty.Register(nameof(ClockIdentifier), "12HourClock", coerce: CoerceClockIdentifier); + + /// + /// Defines the property + /// + public static readonly StyledProperty UseSecondsProperty = + AvaloniaProperty.Register(nameof(UseSeconds), false, coerce: CoerceUseSeconds); /// /// Defines the property @@ -52,11 +67,14 @@ public class TimePicker : TemplatedControl private Border? _firstPickerHost; private Border? _secondPickerHost; private Border? _thirdPickerHost; + private Border? _fourthPickerHost; private TextBlock? _hourText; private TextBlock? _minuteText; + private TextBlock? _secondText; private TextBlock? _periodText; private Rectangle? _firstSplitter; private Rectangle? _secondSplitter; + private Rectangle? _thirdSplitter; private Grid? _contentGrid; private Popup? _popup; @@ -85,6 +103,23 @@ private static int CoerceMinuteIncrement(AvaloniaObject sender, int value) return value; } + + /// + /// Gets or sets the second increment in the picker + /// + public int SecondIncrement + { + get => GetValue(SecondIncrementProperty); + set => SetValue(SecondIncrementProperty, value); + } + + private static int CoerceSecondIncrement(AvaloniaObject sender, int value) + { + if (value < 1 || value > 59) + throw new ArgumentOutOfRangeException(null, "1 >= SecondIncrement <= 59"); + + return value; + } /// /// Gets or sets the clock identifier, either 12HourClock or 24HourClock @@ -103,6 +138,24 @@ private static string CoerceClockIdentifier(AvaloniaObject sender, string value) return value; } + + /// + /// Gets or sets the use seconds switch, either true or false + /// + public bool UseSeconds + { + + get => GetValue(UseSecondsProperty); + set => SetValue(UseSecondsProperty, value); + } + + private static bool CoerceUseSeconds(AvaloniaObject sender, bool value) + { + if (!(value == true || value == false)) + throw new ArgumentException("Invalid UseSeconds", default(bool).ToString()); + + return value; + } /// /// Gets or sets the selected time. Can be null. @@ -135,13 +188,16 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _firstPickerHost = e.NameScope.Find("PART_FirstPickerHost"); _secondPickerHost = e.NameScope.Find("PART_SecondPickerHost"); _thirdPickerHost = e.NameScope.Find("PART_ThirdPickerHost"); + _fourthPickerHost = e.NameScope.Find("PART_FourthPickerHost"); _hourText = e.NameScope.Find("PART_HourTextBlock"); _minuteText = e.NameScope.Find("PART_MinuteTextBlock"); + _secondText = e.NameScope.Find("PART_SecondTextBlock"); _periodText = e.NameScope.Find("PART_PeriodTextBlock"); _firstSplitter = e.NameScope.Find("PART_FirstColumnDivider"); _secondSplitter = e.NameScope.Find("PART_SecondColumnDivider"); + _thirdSplitter = e.NameScope.Find("PART_ThirdColumnDivider"); _contentGrid = e.NameScope.Find("PART_FlyoutButtonContentGrid"); @@ -160,7 +216,9 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _presenter.Dismissed += OnDismissPicker; _presenter[!TimePickerPresenter.MinuteIncrementProperty] = this[!MinuteIncrementProperty]; + _presenter[!TimePickerPresenter.SecondIncrementProperty] = this[!SecondIncrementProperty]; _presenter[!TimePickerPresenter.ClockIdentifierProperty] = this[!ClockIdentifierProperty]; + _presenter[!TimePickerPresenter.UseSecondsProperty] = this[!UseSecondsProperty]; } } @@ -172,11 +230,20 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang { SetSelectedTimeText(); } + else if (change.Property == SecondIncrementProperty) + { + SetSelectedTimeText(); + } else if (change.Property == ClockIdentifierProperty) { SetGrid(); SetSelectedTimeText(); } + else if (change.Property == UseSecondsProperty) + { + SetGrid(); + SetSelectedTimeText(); + } else if (change.Property == SelectedTimeProperty) { var (oldValue, newValue) = change.GetOldAndNewValue(); @@ -196,29 +263,40 @@ private void SetGrid() columnsD.Add(new ColumnDefinition(GridLength.Star)); columnsD.Add(new ColumnDefinition(GridLength.Auto)); columnsD.Add(new ColumnDefinition(GridLength.Star)); + if (UseSeconds) + { + columnsD.Add(new ColumnDefinition(GridLength.Auto)); + columnsD.Add(new ColumnDefinition(GridLength.Star)); + } if (!use24HourClock) { columnsD.Add(new ColumnDefinition(GridLength.Auto)); columnsD.Add(new ColumnDefinition(GridLength.Star)); } - + _contentGrid.ColumnDefinitions = columnsD; + + _thirdPickerHost!.IsVisible = UseSeconds; + _secondSplitter!.IsVisible = UseSeconds; - _thirdPickerHost!.IsVisible = !use24HourClock; - _secondSplitter!.IsVisible = !use24HourClock; + _fourthPickerHost!.IsVisible = !use24HourClock; + _thirdSplitter!.IsVisible = !use24HourClock; + + var amPmColumn = (UseSeconds) ? 6 : 4; Grid.SetColumn(_firstPickerHost!, 0); Grid.SetColumn(_secondPickerHost!, 2); - - Grid.SetColumn(_thirdPickerHost, use24HourClock ? 0 : 4); + Grid.SetColumn(_thirdPickerHost!, UseSeconds ? 4 : 0); + Grid.SetColumn(_fourthPickerHost, use24HourClock ? 0 : amPmColumn); Grid.SetColumn(_firstSplitter!, 1); - Grid.SetColumn(_secondSplitter, use24HourClock ? 0 : 3); + Grid.SetColumn(_secondSplitter!, UseSeconds ? 3 : 0); + Grid.SetColumn(_thirdSplitter, use24HourClock ? 0 : amPmColumn-1); } private void SetSelectedTimeText() { - if (_hourText == null || _minuteText == null || _periodText == null) + if (_hourText == null || _minuteText == null || _secondText == null || _periodText == null) return; var time = SelectedTime; @@ -230,11 +308,12 @@ private void SetSelectedTimeText() { var hr = newTime.Hours; hr = hr > 12 ? hr - 12 : hr == 0 ? 12 : hr; - newTime = new TimeSpan(hr, newTime.Minutes, 0); + newTime = new TimeSpan(hr, newTime.Minutes, newTime.Seconds); } _hourText.Text = newTime.ToString("%h"); _minuteText.Text = newTime.ToString("mm"); + _secondText.Text = newTime.ToString("ss"); PseudoClasses.Set(":hasnotime", false); _periodText.Text = time.Value.Hours >= 12 ? TimeUtils.GetPMDesignator() : TimeUtils.GetAMDesignator(); @@ -244,6 +323,7 @@ private void SetSelectedTimeText() // By clearing local value, we reset text property to the value from the template. _hourText.ClearValue(TextBlock.TextProperty); _minuteText.ClearValue(TextBlock.TextProperty); + _secondText.ClearValue(TextBlock.TextProperty); PseudoClasses.Set(":hasnotime", true); _periodText.Text = DateTime.Now.Hour >= 12 ? TimeUtils.GetPMDesignator() : TimeUtils.GetAMDesignator(); diff --git a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs index 7f19d0545e0..be5c2db6fab 100644 --- a/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs +++ b/src/Avalonia.Controls/DateTimePickers/TimePickerPresenter.cs @@ -19,12 +19,16 @@ namespace Avalonia.Controls [TemplatePart("PART_MinuteDownButton", typeof(RepeatButton))] [TemplatePart("PART_MinuteSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_MinuteUpButton", typeof(RepeatButton))] + [TemplatePart("PART_SecondDownButton", typeof(RepeatButton))] + [TemplatePart("PART_SecondHost", typeof(Panel), IsRequired = true)] + [TemplatePart("PART_SecondSelector", typeof(DateTimePickerPanel), IsRequired = true)] + [TemplatePart("PART_SecondUpButton", typeof(RepeatButton))] [TemplatePart("PART_PeriodDownButton", typeof(RepeatButton))] [TemplatePart("PART_PeriodHost", typeof(Panel), IsRequired = true)] [TemplatePart("PART_PeriodSelector", typeof(DateTimePickerPanel), IsRequired = true)] [TemplatePart("PART_PeriodUpButton", typeof(RepeatButton))] [TemplatePart("PART_PickerContainer", typeof(Grid), IsRequired = true)] - [TemplatePart("PART_SecondSpacer", typeof(Rectangle), IsRequired = true)] + [TemplatePart("PART_ThirdSpacer", typeof(Rectangle), IsRequired = true)] public class TimePickerPresenter : PickerPresenterBase { /// @@ -32,12 +36,24 @@ public class TimePickerPresenter : PickerPresenterBase /// public static readonly StyledProperty MinuteIncrementProperty = TimePicker.MinuteIncrementProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty SecondIncrementProperty = + TimePicker.SecondIncrementProperty.AddOwner(); /// /// Defines the property /// public static readonly StyledProperty ClockIdentifierProperty = TimePicker.ClockIdentifierProperty.AddOwner(); + + /// + /// Defines the property + /// + public static readonly StyledProperty UseSecondsProperty = + TimePicker.UseSecondsProperty.AddOwner(); /// /// Defines the property @@ -60,15 +76,20 @@ static TimePickerPresenter() private Button? _acceptButton; private Button? _dismissButton; private Rectangle? _spacer2; + private Rectangle? _spacer3; + private Panel? _secondHost; private Panel? _periodHost; private DateTimePickerPanel? _hourSelector; private DateTimePickerPanel? _minuteSelector; + private DateTimePickerPanel? _secondSelector; private DateTimePickerPanel? _periodSelector; private Button? _hourUpButton; private Button? _minuteUpButton; + private Button? _secondUpButton; private Button? _periodUpButton; private Button? _hourDownButton; private Button? _minuteDownButton; + private Button? _secondDownButton; private Button? _periodDownButton; /// @@ -79,6 +100,15 @@ public int MinuteIncrement get => GetValue(MinuteIncrementProperty); set => SetValue(MinuteIncrementProperty, value); } + + /// + /// Gets or sets the second increment in the selector + /// + public int SecondIncrement + { + get => GetValue(SecondIncrementProperty); + set => SetValue(SecondIncrementProperty, value); + } /// /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock @@ -88,6 +118,15 @@ public string ClockIdentifier get => GetValue(ClockIdentifierProperty); set => SetValue(ClockIdentifierProperty, value); } + + /// + /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock + /// + public bool UseSeconds + { + get => GetValue(UseSecondsProperty); + set => SetValue(UseSecondsProperty, value); + } /// /// Gets or sets the current time @@ -104,12 +143,15 @@ protected override void OnApplyTemplate(TemplateAppliedEventArgs e) _pickerContainer = e.NameScope.Get("PART_PickerContainer"); _periodHost = e.NameScope.Get("PART_PeriodHost"); + _secondHost = e.NameScope.Get("PART_SecondHost"); _hourSelector = e.NameScope.Get("PART_HourSelector"); _minuteSelector = e.NameScope.Get("PART_MinuteSelector"); + _secondSelector = e.NameScope.Get("PART_SecondSelector"); _periodSelector = e.NameScope.Get("PART_PeriodSelector"); - + _spacer2 = e.NameScope.Get("PART_SecondSpacer"); + _spacer3 = e.NameScope.Get("PART_ThirdSpacer"); _acceptButton = e.NameScope.Get @@ -230,9 +255,24 @@ + + + + + + + + + Grid.Column="6"> + 1);//Should be layoutroot grid & button + Grid container = null; + + Assert.True(desc.ElementAt(1) is Button); + + container = (desc.ElementAt(1) as Button).Content as Grid; + Assert.True(container != null); + + var periodTextHost = container.Children[4] as Border; + Assert.True(periodTextHost != null); + Assert.True(periodTextHost.IsVisible); + + timePicker.UseSeconds = false; + Assert.False(periodTextHost.IsVisible); + } + } [Fact] public void SelectedTime_null_Should_Use_Placeholders() @@ -90,15 +120,20 @@ public void SelectedTime_null_Should_Use_Placeholders() var minuteTextHost = container.Children[2] as Border; Assert.True(minuteTextHost != null); var minuteText = minuteTextHost.Child as TextBlock; + var secondTextHost = container.Children[4] as Border; + Assert.True(secondTextHost != null); + var secondText = secondTextHost.Child as TextBlock; TimeSpan ts = TimeSpan.FromHours(10); timePicker.SelectedTime = ts; Assert.NotNull(hourText.Text); Assert.NotNull(minuteText.Text); + Assert.NotNull(secondText.Text); timePicker.SelectedTime = null; Assert.Null(hourText.Text); Assert.Null(minuteText.Text); + Assert.Null(secondText.Text); } } @@ -122,7 +157,7 @@ public void Using_12HourClock_On_Culture_With_Empty_Period_Should_Show_Period() var container = (desc.ElementAt(1) as Button).Content as Grid; Assert.True(container != null); - var periodTextHost = container.Children[4] as Border; + var periodTextHost = container.Children[6] as Border; Assert.NotNull(periodTextHost); var periodText = periodTextHost.Child as TextBlock; Assert.NotNull(periodTextHost); @@ -227,10 +262,20 @@ private static IControlTemplate CreateTemplate() Name = "PART_ThirdPickerHost", Child = new TextBlock { - Name = "PART_PeriodTextBlock" + Name = "PART_SecondTextBlock" }.RegisterInNameScope(scope) }.RegisterInNameScope(scope); Grid.SetColumn(thirdPickerHost, 4); + + var fourthPickerHost = new Border + { + Name = "PART_FourthPickerHost", + Child = new TextBlock + { + Name = "PART_PeriodTextBlock" + }.RegisterInNameScope(scope) + }.RegisterInNameScope(scope); + Grid.SetColumn(fourthPickerHost, 6); var firstSpacer = new Rectangle { @@ -243,8 +288,14 @@ private static IControlTemplate CreateTemplate() Name = "PART_SecondColumnDivider" }.RegisterInNameScope(scope); Grid.SetColumn(secondSpacer, 3); + + var thirdSpacer = new Rectangle + { + Name = "PART_ThirdColumnDivider" + }.RegisterInNameScope(scope); + Grid.SetColumn(thirdSpacer, 5); - contentGrid.Children.AddRange(new Control[] { firstPickerHost, firstSpacer, secondPickerHost, secondSpacer, thirdPickerHost }); + contentGrid.Children.AddRange(new Control[] { firstPickerHost, firstSpacer, secondPickerHost, secondSpacer, thirdPickerHost, thirdSpacer, fourthPickerHost }); flyoutButton.Content = contentGrid; layoutRoot.Children.Add(flyoutButton); return layoutRoot;