Переглянути джерело

Adds support for Seconds to TimePicker (#16079)

* Adds seconds support to TimePicker.

* Updates TimePicker to support UseSeconds. Seconds are not displayed unless UseSeconds == true.

* Fixes & updates Unit Tests related to adding Seconds & UseSeconds to TimePicker.

* Adds a simple TimePicker with seconds enabled to  DateTimePickerPage.xaml.
Sean Begley 1 рік тому
батько
коміт
ae017763fc

+ 71 - 3
samples/ControlCatalog/Pages/DateTimePickerPage.xaml

@@ -77,6 +77,23 @@
         </Panel>
       </StackPanel>
       
+      <TextBlock FontSize="18">A TimePicker with seconds enabled.</TextBlock>
+      <StackPanel Orientation="Vertical">
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
+                BorderThickness="1" Padding="15">
+          <TimePicker UseSeconds="True" />
+        </Border>
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
+          <TextBlock Padding="15">
+            <TextBlock.Text>
+              <x:String>
+                &lt;TimePicker UseSeconds="True" /&gt;
+              </x:String>
+            </TextBlock.Text>
+          </TextBlock>
+        </Panel>
+      </StackPanel>
+      
       <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
               BorderThickness="1" Padding="15">
         <TimePicker>
@@ -85,8 +102,8 @@
           </DataValidationErrors.Error>
         </TimePicker>
       </Border>
-
-      <TextBlock FontSize="18">A TimePicker with minute increments specified.</TextBlock>
+      
+      <TextBlock FontSize="18">A TimePicker with minute increment specified.</TextBlock>
       <StackPanel Orientation="Vertical">
         <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
                 BorderThickness="1" Padding="15">
@@ -96,7 +113,24 @@
           <TextBlock Padding="15">
             <TextBlock.Text>
               <x:String>
-                &lt;TimePicker MinuteIncrement="15" /&gt;
+                &lt;TimePicker MinuteIncrement="15" SecondIncrement="30" /&gt;
+              </x:String>
+            </TextBlock.Text>
+          </TextBlock>
+        </Panel>
+      </StackPanel>
+
+      <TextBlock FontSize="18">A TimePicker with seconds enabled and minute &amp; second increments specified.</TextBlock>
+      <StackPanel Orientation="Vertical">
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
+                BorderThickness="1" Padding="15">
+          <TimePicker UseSeconds="True" MinuteIncrement="15" SecondIncrement="30" />
+        </Border>
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
+          <TextBlock Padding="15">
+            <TextBlock.Text>
+              <x:String>
+                &lt;TimePicker UseSeconds="True" MinuteIncrement="15" SecondIncrement="30" /&gt;
               </x:String>
             </TextBlock.Text>
           </TextBlock>
@@ -137,6 +171,40 @@
         </Panel>
       </StackPanel>
       
+      <TextBlock FontSize="18">A TimePicker using a 12-hour clock and seconds.</TextBlock>
+      <StackPanel Orientation="Vertical">
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
+                BorderThickness="1" Padding="15">
+          <TimePicker ClockIdentifier="12HourClock" UseSeconds="True" />
+        </Border>
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
+          <TextBlock Padding="15">
+            <TextBlock.Text>
+              <x:String>
+                &lt;TimePicker ClockIdentifier="12HourClock" UseSeconds="True" /&gt;
+              </x:String>
+            </TextBlock.Text>
+          </TextBlock>
+        </Panel>
+      </StackPanel>
+
+      <TextBlock FontSize="18">A TimePicker using a 24-hour clock and seconds.</TextBlock>
+      <StackPanel Orientation="Vertical">
+        <Border BorderBrush="{DynamicResource CatalogBaseLowColor}"
+                BorderThickness="1" Padding="15">
+          <TimePicker ClockIdentifier="24HourClock" UseSeconds="True" />
+        </Border>
+        <Panel Background="{DynamicResource CatalogBaseLowColor}">
+          <TextBlock Padding="15">
+            <TextBlock.Text>
+              <x:String>
+                &lt;TimePicker ClockIdentifier="24HourClock" UseSeconds="True" /&gt;
+              </x:String>
+            </TextBlock.Text>
+          </TextBlock>
+        </Panel>
+      </StackPanel>
+      
     </StackPanel>
   </StackPanel>
 </UserControl>

+ 1 - 1
samples/ControlCatalog/Pages/DateTimePickerPage.xaml.cs

@@ -15,7 +15,7 @@ namespace ControlCatalog.Pages
                 "Order of month, day, and year is dynamically set based on user date settings";
 
             this.Get<TextBlock>("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.";
 

+ 3 - 0
src/Avalonia.Controls/DateTimePickers/DateTimePickerPanel.cs

@@ -18,6 +18,7 @@ namespace Avalonia.Controls.Primitives
         Day,
         Hour,
         Minute,
+        Second,
         TimePeriod //AM or PM
     }
 
@@ -516,6 +517,8 @@ namespace Avalonia.Controls.Primitives
                     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:

+ 89 - 9
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 @@ namespace Avalonia.Controls
         /// </summary>
         public static readonly StyledProperty<int> MinuteIncrementProperty =
             AvaloniaProperty.Register<TimePicker, int>(nameof(MinuteIncrement), 1, coerce: CoerceMinuteIncrement);
+        
+        /// <summary>
+        /// Defines the <see cref="SecondIncrement"/> property
+        /// </summary>
+        public static readonly StyledProperty<int> SecondIncrementProperty =
+            AvaloniaProperty.Register<TimePicker, int>(nameof(SecondIncrement), 1, coerce: CoerceSecondIncrement);
 
         /// <summary>
         /// Defines the <see cref="ClockIdentifier"/> property
         /// </summary>
         public static readonly StyledProperty<string> ClockIdentifierProperty =
            AvaloniaProperty.Register<TimePicker, string>(nameof(ClockIdentifier), "12HourClock", coerce: CoerceClockIdentifier);
+        
+        /// <summary>
+        /// Defines the <see cref="UseSeconds"/> property
+        /// </summary>
+        public static readonly StyledProperty<bool> UseSecondsProperty =
+            AvaloniaProperty.Register<TimePicker, bool>(nameof(UseSeconds), false, coerce: CoerceUseSeconds);
 
         /// <summary>
         /// Defines the <see cref="SelectedTime"/> property
@@ -52,11 +67,14 @@ namespace Avalonia.Controls
         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 @@ namespace Avalonia.Controls
 
             return value;
         }
+        
+        /// <summary>
+        /// Gets or sets the second increment in the picker
+        /// </summary>
+        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;
+        }
 
         /// <summary>
         /// Gets or sets the clock identifier, either 12HourClock or 24HourClock
@@ -103,6 +138,24 @@ namespace Avalonia.Controls
 
             return value;
         }
+        
+        /// <summary>
+        /// Gets or sets the use seconds switch, either true or false
+        /// </summary>
+        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;
+        }
 
         /// <summary>
         /// Gets or sets the selected time. Can be null.
@@ -135,13 +188,16 @@ namespace Avalonia.Controls
             _firstPickerHost = e.NameScope.Find<Border>("PART_FirstPickerHost");
             _secondPickerHost = e.NameScope.Find<Border>("PART_SecondPickerHost");
             _thirdPickerHost = e.NameScope.Find<Border>("PART_ThirdPickerHost");
+            _fourthPickerHost = e.NameScope.Find<Border>("PART_FourthPickerHost");
 
             _hourText = e.NameScope.Find<TextBlock>("PART_HourTextBlock");
             _minuteText = e.NameScope.Find<TextBlock>("PART_MinuteTextBlock");
+            _secondText = e.NameScope.Find<TextBlock>("PART_SecondTextBlock");
             _periodText = e.NameScope.Find<TextBlock>("PART_PeriodTextBlock");
 
             _firstSplitter = e.NameScope.Find<Rectangle>("PART_FirstColumnDivider");
             _secondSplitter = e.NameScope.Find<Rectangle>("PART_SecondColumnDivider");
+            _thirdSplitter = e.NameScope.Find<Rectangle>("PART_ThirdColumnDivider");
 
             _contentGrid = e.NameScope.Find<Grid>("PART_FlyoutButtonContentGrid");
 
@@ -160,7 +216,9 @@ namespace Avalonia.Controls
                 _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 @@ namespace Avalonia.Controls
             {
                 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<TimeSpan?>();
@@ -196,29 +263,40 @@ namespace Avalonia.Controls
             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 @@ namespace Avalonia.Controls
                 {
                     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 @@ namespace Avalonia.Controls
                 // 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();

+ 83 - 9
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
     {
         /// <summary>
@@ -32,12 +36,24 @@ namespace Avalonia.Controls
         /// </summary>
         public static readonly StyledProperty<int> MinuteIncrementProperty =
             TimePicker.MinuteIncrementProperty.AddOwner<TimePickerPresenter>();
+        
+        /// <summary>
+        /// Defines the <see cref="SecondIncrement"/> property
+        /// </summary>
+        public static readonly StyledProperty<int> SecondIncrementProperty =
+            TimePicker.SecondIncrementProperty.AddOwner<TimePickerPresenter>();
 
         /// <summary>
         /// Defines the <see cref="ClockIdentifier"/> property
         /// </summary>
         public static readonly StyledProperty<string> ClockIdentifierProperty =
             TimePicker.ClockIdentifierProperty.AddOwner<TimePickerPresenter>();
+        
+        /// <summary>
+        /// Defines the <see cref="UseSeconds"/> property
+        /// </summary>
+        public static readonly StyledProperty<bool> UseSecondsProperty =
+            TimePicker.UseSecondsProperty.AddOwner<TimePickerPresenter>();
 
         /// <summary>
         /// Defines the <see cref="Time"/> property
@@ -60,15 +76,20 @@ namespace Avalonia.Controls
         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;
 
         /// <summary>
@@ -79,6 +100,15 @@ namespace Avalonia.Controls
             get => GetValue(MinuteIncrementProperty);
             set => SetValue(MinuteIncrementProperty, value);
         }
+        
+        /// <summary>
+        /// Gets or sets the second increment in the selector
+        /// </summary>
+        public int SecondIncrement
+        {
+            get => GetValue(SecondIncrementProperty);
+            set => SetValue(SecondIncrementProperty, value);
+        }
 
         /// <summary>
         /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
@@ -88,6 +118,15 @@ namespace Avalonia.Controls
             get => GetValue(ClockIdentifierProperty);
             set => SetValue(ClockIdentifierProperty, value);
         }
+        
+        /// <summary>
+        /// Gets or sets the current clock identifier, either 12HourClock or 24HourClock
+        /// </summary>
+        public bool UseSeconds
+        {
+            get => GetValue(UseSecondsProperty);
+            set => SetValue(UseSecondsProperty, value);
+        }
 
         /// <summary>
         /// Gets or sets the current time
@@ -104,12 +143,15 @@ namespace Avalonia.Controls
 
             _pickerContainer = e.NameScope.Get<Grid>("PART_PickerContainer");
             _periodHost = e.NameScope.Get<Panel>("PART_PeriodHost");
+            _secondHost = e.NameScope.Get<Panel>("PART_SecondHost");
 
             _hourSelector = e.NameScope.Get<DateTimePickerPanel>("PART_HourSelector");
             _minuteSelector = e.NameScope.Get<DateTimePickerPanel>("PART_MinuteSelector");
+            _secondSelector = e.NameScope.Get<DateTimePickerPanel>("PART_SecondSelector");
             _periodSelector = e.NameScope.Get<DateTimePickerPanel>("PART_PeriodSelector");
-
+            
             _spacer2 = e.NameScope.Get<Rectangle>("PART_SecondSpacer");
+            _spacer3 = e.NameScope.Get<Rectangle>("PART_ThirdSpacer");
 
             _acceptButton = e.NameScope.Get<Button>("PART_AcceptButton");
             _acceptButton.Click += OnAcceptButtonClicked;
@@ -127,6 +169,13 @@ namespace Avalonia.Controls
             _minuteDownButton = e.NameScope.Find<RepeatButton>("PART_MinuteDownButton");
             if (_minuteDownButton != null)
                 _minuteDownButton.Click += OnSelectorButtonClick;
+            
+            _secondUpButton = e.NameScope.Find<RepeatButton>("PART_SecondUpButton");
+            if (_secondUpButton != null)
+                _secondUpButton.Click += OnSelectorButtonClick;
+            _secondDownButton = e.NameScope.Find<RepeatButton>("PART_SecondDownButton");
+            if (_secondDownButton != null)
+                _secondDownButton.Click += OnSelectorButtonClick;
 
             _periodUpButton = e.NameScope.Find<RepeatButton>("PART_PeriodUpButton");
             if (_periodUpButton != null)
@@ -146,7 +195,11 @@ namespace Avalonia.Controls
         {
             base.OnPropertyChanged(change);
 
-            if (change.Property == MinuteIncrementProperty || change.Property == ClockIdentifierProperty || change.Property == TimeProperty)
+            if (change.Property == MinuteIncrementProperty ||
+                change.Property == SecondIncrementProperty ||
+                change.Property == ClockIdentifierProperty ||
+                change.Property == UseSecondsProperty ||
+                change.Property == TimeProperty)
             {
                 InitPicker();
             }
@@ -180,6 +233,7 @@ namespace Avalonia.Controls
         {
             var hr = _hourSelector!.SelectedValue;
             var min = _minuteSelector!.SelectedValue;
+            var sec = _secondSelector!.SelectedValue;
             var per = _periodSelector!.SelectedValue;
 
             if (ClockIdentifier == "12HourClock")
@@ -187,7 +241,7 @@ namespace Avalonia.Controls
                 hr = per == 1 ? (hr == 12) ? 12 : hr + 12 : per == 0 && hr == 12 ? 0 : hr;
             }
 
-            SetCurrentValue(TimeProperty, new TimeSpan(hr, min, 0));
+            SetCurrentValue(TimeProperty, new TimeSpan(hr, min, sec));
 
             base.OnConfirmed();
         }
@@ -210,6 +264,12 @@ namespace Avalonia.Controls
             _minuteSelector.Increment = MinuteIncrement;
             _minuteSelector.SelectedValue = Time.Minutes;
             _minuteSelector.ItemFormat = "mm";
+            
+            _secondSelector!.MaximumValue = 59;
+            _secondSelector.MinimumValue = 0;
+            _secondSelector.Increment = SecondIncrement;
+            _secondSelector.SelectedValue = Time.Seconds;
+            _secondSelector.ItemFormat = "ss";
 
             _periodSelector!.MaximumValue = 1;
             _periodSelector.MinimumValue = 0;
@@ -223,14 +283,24 @@ namespace Avalonia.Controls
         {
             bool use24HourClock = ClockIdentifier == "24HourClock";
 
-            var columnsD = use24HourClock ? "*, Auto, *" : "*, Auto, *, Auto, *";
+            var columnsD = "*, Auto, *";
+            if (UseSeconds) columnsD += ", Auto *";
+            if (!use24HourClock) columnsD += ", Auto *";
+            
             _pickerContainer!.ColumnDefinitions = new ColumnDefinitions(columnsD);
 
-            _spacer2!.IsVisible = !use24HourClock;
-            _periodHost!.IsVisible = !use24HourClock;
+            _spacer2!.IsVisible = UseSeconds;
+            _secondHost!.IsVisible = UseSeconds;
 
-            Grid.SetColumn(_spacer2, use24HourClock ? 0 : 3);
-            Grid.SetColumn(_periodHost, use24HourClock ? 0 : 4);
+            _spacer3!.IsVisible = !use24HourClock;
+            _periodHost!.IsVisible = !use24HourClock;
+            
+            var amPmColumn = (UseSeconds) ? 6 : 4;
+            
+            Grid.SetColumn(_spacer2, UseSeconds ? 3 : 0);
+            Grid.SetColumn(_secondHost, UseSeconds ? 4 : 0);
+            Grid.SetColumn(_spacer3, use24HourClock ? 0 : amPmColumn-1);
+            Grid.SetColumn(_periodHost, use24HourClock ? 0 : amPmColumn);
         }
 
         private void OnDismissButtonClicked(object? sender, RoutedEventArgs e)
@@ -253,6 +323,10 @@ namespace Avalonia.Controls
                 _minuteSelector!.ScrollUp();
             else if (sender == _minuteDownButton)
                 _minuteSelector!.ScrollDown();
+            else if (sender == _secondUpButton)
+                _secondSelector!.ScrollUp();
+            else if (sender == _secondDownButton)
+                _secondSelector!.ScrollDown();
             else if (sender == _periodUpButton)
                 _periodSelector!.ScrollUp();
             else if (sender == _periodDownButton)

+ 36 - 3
src/Avalonia.Themes.Fluent/Controls/TimePicker.xaml

@@ -128,11 +128,27 @@
                            HorizontalAlignment="Center"
                            Width="{DynamicResource TimePickerSpacerThemeWidth}"
                            Grid.Column="3" />
-
+                
                 <Border x:Name="PART_ThirdPickerHost"
                         Grid.Column="4"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
+                  <TextBlock x:Name="PART_SecondTextBlock"
+                             Text="{DynamicResource StringTimePickerSecondText}"
+                             HorizontalAlignment="Center"
+                             Padding="{DynamicResource TimePickerHostPadding}"/>
+                </Border>
+
+                <Rectangle Name="PART_ThirdColumnDivider"
+                           Fill="{DynamicResource TimePickerSpacerFill}"
+                           HorizontalAlignment="Center"
+                           Width="{DynamicResource TimePickerSpacerThemeWidth}"
+                           Grid.Column="5" />
+
+                <Border x:Name="PART_FourthPickerHost"
+                        Grid.Column="6"
+                        HorizontalAlignment="Stretch"
+                        VerticalAlignment="Stretch">
                   <TextBlock x:Name="PART_PeriodTextBlock"
                              HorizontalAlignment="Center"
                              Padding="{DynamicResource TimePickerHostPadding}" />
@@ -209,8 +225,20 @@
                 <RepeatButton Name="PART_MinuteUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
                 <RepeatButton Name="PART_MinuteDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
               </Panel>
+              
+              <Panel Name="PART_SecondHost" Grid.Column="4">
+                <ScrollViewer HorizontalScrollBarVisibility="Disabled"
+                              VerticalScrollBarVisibility="Hidden">
+                  <DateTimePickerPanel Name="PART_SecondSelector"
+                                       PanelType="Second"
+                                       ShouldLoop="True"
+                                       ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"/>
+                </ScrollViewer>
+                <RepeatButton Name="PART_SecondUpButton" Theme="{StaticResource FluentDateTimePickerUpButton}"/>
+                <RepeatButton Name="PART_SecondDownButton" Theme="{StaticResource FluentDateTimePickerDownButton}"/>
+              </Panel>
 
-              <Panel Name="PART_PeriodHost" Grid.Column="4">
+              <Panel Name="PART_PeriodHost" Grid.Column="6">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
                   <DateTimePickerPanel Name="PART_PeriodSelector"
@@ -225,7 +253,7 @@
               <Rectangle x:Name="HighlightRect" ZIndex="-1"
                          Fill="{DynamicResource TimePickerFlyoutPresenterHighlightFill}"
                          Grid.Column="0"
-                         Grid.ColumnSpan="5"
+                         Grid.ColumnSpan="7"
                          VerticalAlignment="Center"
                          Height="{DynamicResource TimePickerFlyoutPresenterHighlightHeight}" />
               <Rectangle Name="PART_FirstSpacer"
@@ -238,6 +266,11 @@
                          HorizontalAlignment="Center"
                          Width="{DynamicResource TimePickerSpacerThemeWidth}"
                          Grid.Column="3" />
+              <Rectangle Name="PART_ThirdSpacer"
+                         Fill="{DynamicResource TimePickerFlyoutPresenterSpacerFill}"
+                         HorizontalAlignment="Center"
+                         Width="{DynamicResource TimePickerSpacerThemeWidth}"
+                         Grid.Column="5" />
             </Grid>
 
             <Grid Name="AcceptDismissGrid"

+ 1 - 0
src/Avalonia.Themes.Fluent/Strings/InvariantResources.xaml

@@ -8,6 +8,7 @@
   <!-- TimePicker -->
   <x:String x:Key="StringTimePickerHourText">hour</x:String>
   <x:String x:Key="StringTimePickerMinuteText">minute</x:String>
+  <x:String x:Key="StringTimePickerSecondText">second</x:String>
   <!-- TextBox/SelectableTextBox flyout -->
   <x:String x:Key="StringTextFlyoutCutText">Cut</x:String>
   <x:String x:Key="StringTextFlyoutCopyText">Copy</x:String>

+ 47 - 2
src/Avalonia.Themes.Simple/Controls/TimePicker.xaml

@@ -142,11 +142,30 @@
                            Width="{DynamicResource TimePickerSpacerThemeWidth}"
                            HorizontalAlignment="Center"
                            Fill="{DynamicResource TimePickerSpacerFill}" />
-
+                
                 <Border x:Name="PART_ThirdPickerHost"
                         Grid.Column="4"
                         HorizontalAlignment="Stretch"
                         VerticalAlignment="Stretch">
+                  <TextBlock x:Name="PART_SecondTextBlock"
+                             Text="{DynamicResource StringTimePickerSecondText}"
+                             Padding="{DynamicResource TimePickerHostPadding}"
+                             HorizontalAlignment="Center"
+                             FontFamily="{TemplateBinding FontFamily}"
+                             FontSize="{TemplateBinding FontSize}"
+                             FontWeight="{TemplateBinding FontWeight}" />
+                </Border>
+
+                <Rectangle Name="PART_ThirdColumnDivider"
+                           Grid.Column="5"
+                           Width="{DynamicResource TimePickerSpacerThemeWidth}"
+                           HorizontalAlignment="Center"
+                           Fill="{DynamicResource TimePickerSpacerFill}" />
+
+                <Border x:Name="PART_FourthPickerHost"
+                        Grid.Column="6"
+                        HorizontalAlignment="Stretch"
+                        VerticalAlignment="Stretch">
                   <TextBlock x:Name="PART_PeriodTextBlock"
                              Padding="{DynamicResource TimePickerHostPadding}"
                              HorizontalAlignment="Center"
@@ -154,6 +173,12 @@
                              FontSize="{TemplateBinding FontSize}"
                              FontWeight="{TemplateBinding FontWeight}" />
                 </Border>
+                
+                <Rectangle Name="PART_ThirdColumnDivider"
+                           Grid.Column="5"
+                           Width="{DynamicResource TimePickerSpacerThemeWidth}"
+                           HorizontalAlignment="Center"
+                           Fill="{DynamicResource TimePickerSpacerFill}" />
               </Grid>
             </Button>
 
@@ -230,9 +255,24 @@
                 <RepeatButton Name="PART_MinuteDownButton"
                               Theme="{StaticResource SimpleDateTimePickerDownButton}" />
               </Panel>
+              
+              <Panel Name="PART_SecondHost"
+                     Grid.Column="4">
+                <ScrollViewer HorizontalScrollBarVisibility="Disabled"
+                              VerticalScrollBarVisibility="Hidden">
+                  <DateTimePickerPanel Name="PART_SecondSelector"
+                                       ItemHeight="{DynamicResource TimePickerFlyoutPresenterItemHeight}"
+                                       PanelType="Second"
+                                       ShouldLoop="True" />
+                </ScrollViewer>
+                <RepeatButton Name="PART_SecondUpButton"
+                              Theme="{StaticResource SimpleDateTimePickerUpButton}" />
+                <RepeatButton Name="PART_SecondDownButton"
+                              Theme="{StaticResource SimpleDateTimePickerDownButton}" />
+              </Panel>
 
               <Panel Name="PART_PeriodHost"
-                     Grid.Column="4">
+                     Grid.Column="6">
                 <ScrollViewer HorizontalScrollBarVisibility="Disabled"
                               VerticalScrollBarVisibility="Hidden">
                   <DateTimePickerPanel Name="PART_PeriodSelector"
@@ -267,6 +307,11 @@
                          Width="{DynamicResource TimePickerSpacerThemeWidth}"
                          HorizontalAlignment="Center"
                          Fill="{DynamicResource ThemeControlMidHighBrush}" />
+              <Rectangle Name="PART_ThirdSpacer"
+                         Grid.Column="5"
+                         Width="{DynamicResource TimePickerSpacerThemeWidth}"
+                         HorizontalAlignment="Center"
+                         Fill="{DynamicResource ThemeControlMidHighBrush}" />
             </Grid>
 
             <Grid Name="AcceptDismissGrid"

+ 5 - 1
tests/Avalonia.Controls.UnitTests/DatePickerTests.cs

@@ -287,8 +287,12 @@ namespace Avalonia.Controls.UnitTests
                 {
                     Name = "PART_SecondSpacer"
                 }.RegisterInNameScope(scope);
+                var thirdSpacer = new Rectangle
+                {
+                    Name = "PART_ThirdSpacer"
+                }.RegisterInNameScope(scope);
                
-                contentGrid.Children.AddRange(new Control[] { dayText, monthText, yearText, firstSpacer, secondSpacer });
+                contentGrid.Children.AddRange(new Control[] { dayText, monthText, yearText, firstSpacer, secondSpacer, thirdSpacer });
                 flyoutButton.Content = contentGrid;
                 layoutRoot.Children.Add(flyoutButton);
                 return layoutRoot;

+ 55 - 4
tests/Avalonia.Controls.UnitTests/TimePickerTests.cs

@@ -55,7 +55,7 @@ namespace Avalonia.Controls.UnitTests
                 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.True(periodTextHost != null);
                 Assert.True(periodTextHost.IsVisible);
 
@@ -63,6 +63,36 @@ namespace Avalonia.Controls.UnitTests
                 Assert.False(periodTextHost.IsVisible);
             }
         }
+        
+        [Fact]
+        public void UseSeconds_Equals_False_Should_Hide_Seconds()
+        {
+            using (UnitTestApplication.Start(Services))
+            {
+                TimePicker timePicker = new TimePicker()
+                {
+                    UseSeconds = true,
+                    Template = CreateTemplate()
+                };
+                timePicker.ApplyTemplate();
+
+                var desc = timePicker.GetVisualDescendants();
+                Assert.True(desc.Count() > 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 @@ namespace Avalonia.Controls.UnitTests
                 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 @@ namespace Avalonia.Controls.UnitTests
                 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 @@ namespace Avalonia.Controls.UnitTests
                     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 @@ namespace Avalonia.Controls.UnitTests
                     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;