diff --git a/src/Wpf.Ui.Gallery/Views/Pages/DateAndTime/TimePickerPage.xaml b/src/Wpf.Ui.Gallery/Views/Pages/DateAndTime/TimePickerPage.xaml
index 6d071f379..1058b8bce 100644
--- a/src/Wpf.Ui.Gallery/Views/Pages/DateAndTime/TimePickerPage.xaml
+++ b/src/Wpf.Ui.Gallery/Views/Pages/DateAndTime/TimePickerPage.xaml
@@ -26,8 +26,10 @@
XamlCode="<ui:TimePicker ClockIdentifier="Clock24Hour" />">
+ ClockIdentifier="Clock24Hour" />
diff --git a/src/Wpf.Ui/Controls/TimePicker/TimePicker.cs b/src/Wpf.Ui/Controls/TimePicker/TimePicker.cs
index fa1b74949..960b1b976 100644
--- a/src/Wpf.Ui/Controls/TimePicker/TimePicker.cs
+++ b/src/Wpf.Ui/Controls/TimePicker/TimePicker.cs
@@ -4,52 +4,88 @@
// All Rights Reserved.
// ReSharper disable once CheckNamespace
+using System.Windows.Controls;
+using System.Windows.Controls.Primitives;
+using System.Windows.Threading;
+using ORGButton = System.Windows.Controls.Button;
+
namespace Wpf.Ui.Controls;
///
/// Represents a control that allows a user to pick a time value.
///
-public class TimePicker : System.Windows.Controls.Primitives.ButtonBase
+[TemplatePart(Name = PartPopup, Type = typeof(Popup))]
+[TemplatePart(Name = PartHoursList, Type = typeof(ListBox))]
+[TemplatePart(Name = PartMinutesList, Type = typeof(ListBox))]
+[TemplatePart(Name = PartAmPmList, Type = typeof(ListBox))]
+[TemplatePart(Name = PartAcceptButton, Type = typeof(Button))]
+[TemplatePart(Name = PartDismissButton, Type = typeof(Button))]
+public class TimePicker : ButtonBase
{
+ private const string PartPopup = "PART_Popup";
+ private const string PartHoursList = "PART_HoursList";
+ private const string PartMinutesList = "PART_MinutesList";
+ private const string PartAmPmList = "PART_AmPmList";
+ private const string PartAcceptButton = "PART_AcceptButton";
+ private const string PartDismissButton = "PART_DismissButton";
+
+ ///
+ /// Defines the number of times the item source is repeated to create the illusion of infinite scrolling.
+ ///
+ private const int InfiniteScrollRepeatCount = 3;
+
+ private Popup? _popup;
+ private ListBox? _hoursList;
+ private ListBox? _minutesList;
+ private ListBox? _amPmList;
+ private ORGButton? _acceptButton;
+ private ORGButton? _dismissButton;
+ private ScrollViewer? _hoursScrollViewer;
+ private ScrollViewer? _minutesScrollViewer;
+ private TimeSpan _temporaryTime;
+ private bool _isUpdatingSelection; // Flag to prevent re-entrant event handling while programmatically updating selection.
+ private bool _isScrolling; // Flag to prevent selection changes during programmatic scrolling (e.g., in infinite scroll jump).
+
+ private DispatcherTimer? _hoursDebounceTimer;
+ private DispatcherTimer? _minutesDebounceTimer;
+
/// Identifies the dependency property.
public static readonly DependencyProperty HeaderProperty = DependencyProperty.Register(
- nameof(Header),
- typeof(object),
- typeof(TimePicker),
- new PropertyMetadata(null)
- );
+ nameof(Header), typeof(object), typeof(TimePicker), new PropertyMetadata(null));
/// Identifies the dependency property.
public static readonly DependencyProperty TimeProperty = DependencyProperty.Register(
- nameof(Time),
- typeof(TimeSpan),
- typeof(TimePicker),
- new PropertyMetadata(TimeSpan.Zero)
- );
+ nameof(Time), typeof(TimeSpan), typeof(TimePicker), new PropertyMetadata(TimeSpan.Zero, OnTimeChanged));
/// Identifies the dependency property.
public static readonly DependencyProperty SelectedTimeProperty = DependencyProperty.Register(
- nameof(SelectedTime),
- typeof(TimeSpan?),
- typeof(TimePicker),
- new PropertyMetadata(null)
- );
+ nameof(SelectedTime), typeof(TimeSpan?), typeof(TimePicker), new FrameworkPropertyMetadata(null, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
/// Identifies the dependency property.
public static readonly DependencyProperty MinuteIncrementProperty = DependencyProperty.Register(
- nameof(MinuteIncrement),
- typeof(int),
- typeof(TimePicker),
- new PropertyMetadata(1)
- );
+ nameof(MinuteIncrement), typeof(int), typeof(TimePicker), new PropertyMetadata(1, OnMinuteIncrementChanged));
/// Identifies the dependency property.
public static readonly DependencyProperty ClockIdentifierProperty = DependencyProperty.Register(
- nameof(ClockIdentifier),
- typeof(ClockIdentifier),
- typeof(TimePicker),
- new PropertyMetadata(ClockIdentifier.Clock24Hour)
- );
+ nameof(ClockIdentifier), typeof(ClockIdentifier), typeof(TimePicker), new PropertyMetadata(ClockIdentifier.Clock24Hour, OnClockIdentifierChanged));
+
+ public static readonly DependencyProperty AcceptButtonTextProperty = DependencyProperty.Register(
+ nameof(AcceptButtonText), typeof(string), typeof(TimePicker), new PropertyMetadata("OK"));
+
+ public string AcceptButtonText
+ {
+ get => (string)GetValue(AcceptButtonTextProperty);
+ set => SetValue(AcceptButtonTextProperty, value);
+ }
+
+ public static readonly DependencyProperty DismissButtonTextProperty = DependencyProperty.Register(
+ nameof(DismissButtonText), typeof(string), typeof(TimePicker), new PropertyMetadata("Cancel"));
+
+ public string DismissButtonText
+ {
+ get => (string)GetValue(DismissButtonTextProperty);
+ set => SetValue(DismissButtonTextProperty, value);
+ }
///
/// Gets or sets the content for the control's header.
@@ -70,7 +106,7 @@ public TimeSpan Time
}
///
- /// Gets or sets the time currently selected in the time picker
+ /// Gets or sets the time currently selected in the time picker. This is a nullable version of .
///
public TimeSpan? SelectedTime
{
@@ -80,7 +116,7 @@ public TimeSpan? SelectedTime
///
/// Gets or sets a value that indicates the time increments shown in the minute picker.
- /// For example, 15 specifies that the TimePicker minute control displays only the choices 00, 15, 30, 45.
+ /// The value must be a divisor of 60.
///
public int MinuteIncrement
{
@@ -89,11 +125,528 @@ public int MinuteIncrement
}
///
- /// Gets or sets the clock system to use.
+ /// Gets or sets the clock system to use (12-hour or 24-hour).
///
public ClockIdentifier ClockIdentifier
{
get => (ClockIdentifier)GetValue(ClockIdentifierProperty);
set => SetValue(ClockIdentifierProperty, value);
}
-}
+
+ ///
+ public override void OnApplyTemplate()
+ {
+ // Detach old event handlers to prevent memory leaks
+ if (_acceptButton != null)
+ {
+ _acceptButton.Click -= OnAcceptButtonClick;
+ }
+
+ if (_dismissButton != null)
+ {
+ _dismissButton.Click -= OnDismissButtonClick;
+ }
+
+ if (_hoursList != null)
+ {
+ _hoursList.SelectionChanged -= OnHourChanged;
+ }
+
+ if (_minutesList != null)
+ {
+ _minutesList.SelectionChanged -= OnMinuteChanged;
+ }
+
+ if (_amPmList != null)
+ {
+ _amPmList.SelectionChanged -= OnAmPmChanged;
+ }
+
+ if (_hoursScrollViewer != null)
+ {
+ _hoursScrollViewer.ScrollChanged -= OnHoursScrollChanged;
+ }
+
+ if (_minutesScrollViewer != null)
+ {
+ _minutesScrollViewer.ScrollChanged -= OnMinutesScrollChanged;
+ }
+
+ _hoursDebounceTimer?.Stop();
+ _minutesDebounceTimer?.Stop();
+
+ base.OnApplyTemplate();
+
+ // Get template parts
+ _popup = GetTemplateChild(PartPopup) as Popup;
+ _hoursList = GetTemplateChild(PartHoursList) as ListBox;
+ _minutesList = GetTemplateChild(PartMinutesList) as ListBox;
+ _amPmList = GetTemplateChild(PartAmPmList) as ListBox;
+ _acceptButton = GetTemplateChild(PartAcceptButton) as ORGButton;
+ _dismissButton = GetTemplateChild(PartDismissButton) as ORGButton;
+
+ // Populate lists
+ PopulateHours();
+ PopulateMinutes();
+ PopulateAmPm();
+
+ // Attach new event handlers
+ if (_acceptButton != null)
+ {
+ _acceptButton.Click += OnAcceptButtonClick;
+ }
+
+ if (_dismissButton != null)
+ {
+ _dismissButton.Click += OnDismissButtonClick;
+ }
+
+ if (_hoursList != null)
+ {
+ _hoursList.SelectionChanged += OnHourChanged;
+ }
+
+ if (_minutesList != null)
+ {
+ _minutesList.SelectionChanged += OnMinuteChanged;
+ }
+
+ if (_amPmList != null)
+ {
+ _amPmList.SelectionChanged += OnAmPmChanged;
+ }
+ }
+
+ ///
+ protected override void OnClick()
+ {
+ base.OnClick();
+ OpenPopup();
+ }
+
+ private void OpenPopup()
+ {
+ if (_popup == null)
+ {
+ return;
+ }
+
+ _temporaryTime = Time;
+ UpdateListSelection();
+ _popup.SetCurrentValue(Popup.IsOpenProperty, true);
+
+ // The ScrollViewers are only available after the popup is open and rendered.
+ if (_hoursScrollViewer == null || _minutesScrollViewer == null)
+ {
+ SetupScrollViewers();
+ }
+
+ // Defer scrolling into view to ensure the ListBox items are rendered.
+ _ = Dispatcher.BeginInvoke(DispatcherPriority.Input, () =>
+ {
+ ScrollToSelectedIndex(_hoursList, _hoursScrollViewer);
+ ScrollToSelectedIndex(_minutesList, _minutesScrollViewer);
+ });
+ }
+
+ private void PopulateHours()
+ {
+ if (_hoursList == null)
+ {
+ return;
+ }
+
+ var singleSet = new List();
+ int count = ClockIdentifier == ClockIdentifier.Clock24Hour ? 24 : 12;
+ int start = ClockIdentifier == ClockIdentifier.Clock24Hour ? 0 : 1;
+
+ for (int i = 0; i < count; i++)
+ {
+ var value = start + i;
+ singleSet.Add(ClockIdentifier == ClockIdentifier.Clock24Hour ? value.ToString("D2") : value.ToString());
+ }
+
+ var repeatedSet = new List();
+ for (int i = 0; i < InfiniteScrollRepeatCount; i++)
+ {
+ repeatedSet.AddRange(singleSet);
+ }
+
+ _hoursList.SetCurrentValue(ItemsControl.ItemsSourceProperty, repeatedSet);
+ }
+
+ private void PopulateMinutes()
+ {
+ if (_minutesList == null)
+ {
+ return;
+ }
+
+ var singleSet = new List();
+ for (int i = 0; i < 60; i += MinuteIncrement)
+ {
+ singleSet.Add(i.ToString("D2"));
+ }
+
+ var repeatedSet = new List();
+ for (int i = 0; i < InfiniteScrollRepeatCount; i++)
+ {
+ repeatedSet.AddRange(singleSet);
+ }
+
+ _minutesList.SetCurrentValue(ItemsControl.ItemsSourceProperty, repeatedSet);
+ }
+
+ private void PopulateAmPm()
+ {
+ if (_amPmList == null)
+ {
+ return;
+ }
+
+ if (ClockIdentifier == ClockIdentifier.Clock12Hour)
+ {
+ _amPmList.SetCurrentValue(ItemsControl.ItemsSourceProperty, new List { "AM", "PM" });
+ _amPmList.SetCurrentValue(VisibilityProperty, Visibility.Visible);
+ }
+ else
+ {
+ _amPmList.SetCurrentValue(ItemsControl.ItemsSourceProperty, null);
+ _amPmList.SetCurrentValue(VisibilityProperty, Visibility.Collapsed);
+ }
+ }
+
+ ///
+ /// Updates the selection in the ListBoxes based on the _temporaryTime.
+ ///
+ private void UpdateListSelection()
+ {
+ if (_hoursList == null || _minutesList == null || _amPmList == null)
+ {
+ return;
+ }
+
+ _isUpdatingSelection = true; // Prevent selection events from firing during this update.
+
+ // Update Hours
+ var hour = _temporaryTime.Hours;
+ var displayHour = (ClockIdentifier == ClockIdentifier.Clock12Hour) ? (hour % 12 == 0 ? 12 : hour % 12) : hour;
+ var hourString = (ClockIdentifier == ClockIdentifier.Clock24Hour) ? displayHour.ToString("D2") : displayHour.ToString();
+ var singleHourSetCount = (ClockIdentifier == ClockIdentifier.Clock24Hour) ? 24 : 12;
+
+ if (_hoursList.ItemsSource is IList hourItems)
+ {
+ var hourIndex = hourItems.IndexOf(hourString);
+ if (hourIndex != -1)
+ {
+ // Select the item in the middle set for infinite scrolling.
+ _hoursList.SetCurrentValue(Selector.SelectedIndexProperty, hourIndex + singleHourSetCount);
+ }
+ }
+
+ // Update Minutes
+ var minuteString = _temporaryTime.Minutes.ToString("D2");
+ var singleMinuteSetCount = 60 / MinuteIncrement;
+ if (_minutesList.ItemsSource is IList minuteItems)
+ {
+ var minuteIndex = minuteItems.IndexOf(minuteString);
+ if (minuteIndex != -1)
+ {
+ // Select the item in the middle set for infinite scrolling.
+ _minutesList.SetCurrentValue(Selector.SelectedIndexProperty, minuteIndex + singleMinuteSetCount);
+ }
+ }
+
+ // Update AM/PM
+ if (ClockIdentifier == ClockIdentifier.Clock12Hour)
+ {
+ _amPmList.SetCurrentValue(Selector.SelectedItemProperty, _temporaryTime.Hours < 12 ? "AM" : "PM");
+ }
+
+ _isUpdatingSelection = false; // Re-enable selection events.
+ }
+
+ ///
+ /// Scrolls the ScrollViewer to center the currently selected item.
+ ///
+ private void ScrollToSelectedIndex(ListBox? listBox, ScrollViewer? scrollViewer)
+ {
+ if (listBox?.SelectedIndex < 0 || scrollViewer == null)
+ {
+ return;
+ }
+
+ _isScrolling = true;
+
+ int totalItems = listBox!.Items.Count;
+ if (totalItems == 0 || scrollViewer.ExtentHeight <= 0)
+ {
+ _isScrolling = false;
+ return;
+ }
+
+ double singleItemHeight = scrollViewer.ExtentHeight / totalItems;
+
+ // Calculate the offset to bring the top of the selected item to the top of the viewport.
+ double targetOffset = listBox.SelectedIndex * singleItemHeight;
+
+ // Adjust the offset to center the item within the viewport.
+ targetOffset = targetOffset - (scrollViewer.ViewportHeight / 2) + (singleItemHeight / 2);
+
+ scrollViewer.ScrollToVerticalOffset(targetOffset);
+
+ // Defer resetting the flag to ensure the scroll operation completes.
+ _ = Dispatcher.BeginInvoke(DispatcherPriority.Input, () => { _isScrolling = false; });
+ }
+
+ ///
+ /// Finds the ScrollViewer controls within the ListBoxes and sets up scroll event handling.
+ ///
+ private void SetupScrollViewers()
+ {
+ if (_hoursScrollViewer != null)
+ {
+ _hoursScrollViewer.ScrollChanged -= OnHoursScrollChanged;
+ }
+
+ if (_minutesScrollViewer != null)
+ {
+ _minutesScrollViewer.ScrollChanged -= OnMinutesScrollChanged;
+ }
+
+ _hoursScrollViewer = FindVisualChild(_hoursList);
+ _minutesScrollViewer = FindVisualChild(_minutesList);
+
+ if (_hoursScrollViewer != null)
+ {
+ _hoursScrollViewer.ScrollChanged += OnHoursScrollChanged;
+ }
+
+ if (_minutesScrollViewer != null)
+ {
+ _minutesScrollViewer.ScrollChanged += OnMinutesScrollChanged;
+ }
+
+ // Initialize debouncing timers for scroll events.
+ _hoursDebounceTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(150) };
+ _hoursDebounceTimer.Tick += (s, e) =>
+ {
+ _hoursDebounceTimer.Stop();
+ PerformDeferredJump(_hoursScrollViewer, (ClockIdentifier == ClockIdentifier.Clock24Hour) ? 24 : 12);
+ };
+
+ _minutesDebounceTimer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(150) };
+ _minutesDebounceTimer.Tick += (s, e) =>
+ {
+ _minutesDebounceTimer.Stop();
+ PerformDeferredJump(_minutesScrollViewer, 60 / MinuteIncrement);
+ };
+ }
+
+ ///
+ /// When scrolling stops near the top or bottom boundaries, this method "jumps" the scroll position
+ /// to the corresponding item in the middle set to maintain the infinite-scrolling illusion.
+ ///
+ private void PerformDeferredJump(ScrollViewer? scrollViewer, int singleSetItemCount)
+ {
+ if (scrollViewer == null || _isScrolling || scrollViewer.ExtentHeight <= 0)
+ {
+ return;
+ }
+
+ double singleSetHeight = scrollViewer.ExtentHeight / InfiniteScrollRepeatCount;
+ if (singleSetHeight <= 0)
+ {
+ return;
+ }
+
+ bool needsJump = false;
+ double newOffset = scrollViewer.VerticalOffset;
+
+ // Check if scrolled to the top buffer area.
+ if (scrollViewer.VerticalOffset < singleSetHeight)
+ {
+ newOffset += singleSetHeight;
+ needsJump = true;
+ }
+
+ // Check if scrolled to the bottom buffer area.
+ else if (scrollViewer.VerticalOffset >= singleSetHeight * (InfiniteScrollRepeatCount - 1))
+ {
+ newOffset -= singleSetHeight;
+ needsJump = true;
+ }
+
+ if (needsJump)
+ {
+ _isScrolling = true;
+ scrollViewer.ScrollToVerticalOffset(newOffset);
+
+ // Use Dispatcher to reset the flag after the UI has updated from the programmatic scroll.
+ _ = Dispatcher.BeginInvoke(DispatcherPriority.Input, () => { _isScrolling = false; });
+ }
+ }
+
+ private void OnHoursScrollChanged(object? sender, ScrollChangedEventArgs e)
+ {
+ // Restart the debounce timer on each scroll event.
+ _hoursDebounceTimer?.Stop();
+ _hoursDebounceTimer?.Start();
+ }
+
+ private void OnMinutesScrollChanged(object? sender, ScrollChangedEventArgs e)
+ {
+ // Restart the debounce timer on each scroll event.
+ _minutesDebounceTimer?.Stop();
+ _minutesDebounceTimer?.Start();
+ }
+
+ private void OnHourChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isUpdatingSelection || _isScrolling || e.AddedItems.Count == 0)
+ {
+ return;
+ }
+
+ UpdateTemporaryTimeFromSelection();
+ }
+
+ private void OnMinuteChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isUpdatingSelection || _isScrolling || e.AddedItems.Count == 0)
+ {
+ return;
+ }
+
+ UpdateTemporaryTimeFromSelection();
+ }
+
+ private void OnAmPmChanged(object sender, SelectionChangedEventArgs e)
+ {
+ if (_isUpdatingSelection || _isScrolling || e.AddedItems.Count == 0)
+ {
+ return;
+ }
+
+ UpdateTemporaryTimeFromSelection();
+ }
+
+ ///
+ /// Updates the _temporaryTime field based on the current selections in the ListBoxes.
+ ///
+ private void UpdateTemporaryTimeFromSelection()
+ {
+ if (_hoursList?.SelectedItem == null || _minutesList?.SelectedItem == null)
+ {
+ return;
+ }
+
+ if (!int.TryParse((string)_hoursList.SelectedItem, out var hour) || !int.TryParse((string)_minutesList.SelectedItem, out var minute))
+ {
+ return;
+ }
+
+ if (ClockIdentifier == ClockIdentifier.Clock12Hour)
+ {
+ var amPm = _amPmList?.SelectedItem as string;
+ if (amPm == "PM" && hour != 12)
+ {
+ hour += 12;
+ }
+ else if (amPm == "AM" && hour == 12)
+ {
+ hour = 0; // Midnight case: 12 AM is 00:00
+ }
+ }
+
+ _temporaryTime = new TimeSpan(hour % 24, minute, 0);
+ }
+
+ private void OnAcceptButtonClick(object sender, RoutedEventArgs e)
+ {
+ UpdateTemporaryTimeFromSelection(); // Ensure time is updated with the latest selection.
+ SetCurrentValue(TimeProperty, _temporaryTime);
+ SetCurrentValue(SelectedTimeProperty, Time);
+ if (_popup != null)
+ {
+ _popup.SetCurrentValue(Popup.IsOpenProperty, false);
+ }
+ }
+
+ private void OnDismissButtonClick(object sender, RoutedEventArgs e)
+ {
+ if (_popup != null)
+ {
+ _popup.SetCurrentValue(Popup.IsOpenProperty, false);
+ }
+ }
+
+ private static void OnTimeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = (TimePicker)d;
+ var newTime = (TimeSpan)e.NewValue;
+
+ control.SelectedTime = newTime;
+
+ // If the popup is open, update the list selections to reflect the new Time.
+ if (control._popup is { IsOpen: true })
+ {
+ control._temporaryTime = newTime;
+ control.UpdateListSelection();
+ _ = control.Dispatcher.BeginInvoke(DispatcherPriority.Input, () =>
+ {
+ control.ScrollToSelectedIndex(control._hoursList, control._hoursScrollViewer);
+ control.ScrollToSelectedIndex(control._minutesList, control._minutesScrollViewer);
+ });
+ }
+ }
+
+ private static void OnClockIdentifierChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = (TimePicker)d;
+ control.PopulateHours();
+ control.PopulateAmPm();
+ }
+
+ private static void OnMinuteIncrementChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
+ {
+ var control = (TimePicker)d;
+
+ // Ensure the increment is a valid divisor of 60.
+ if (control.MinuteIncrement < 1 || 60 % control.MinuteIncrement != 0)
+ {
+ control.MinuteIncrement = 1; // Reset to a valid default.
+ }
+
+ control.PopulateMinutes();
+ }
+
+ ///
+ /// Finds a visual child of a specified type in the visual tree.
+ ///
+ private static T? FindVisualChild(DependencyObject? obj)
+ where T : DependencyObject
+ {
+ if (obj == null)
+ {
+ return null;
+ }
+
+ for (int i = 0; i < VisualTreeHelper.GetChildrenCount(obj); i++)
+ {
+ DependencyObject child = VisualTreeHelper.GetChild(obj, i);
+ if (child is T typedChild)
+ {
+ return typedChild;
+ }
+
+ T? childOfChild = FindVisualChild(child);
+ if (childOfChild != null)
+ {
+ return childOfChild;
+ }
+ }
+
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Wpf.Ui/Controls/TimePicker/TimePicker.xaml b/src/Wpf.Ui/Controls/TimePicker/TimePicker.xaml
index a4ef2a839..3b2dcb7db 100644
--- a/src/Wpf.Ui/Controls/TimePicker/TimePicker.xaml
+++ b/src/Wpf.Ui/Controls/TimePicker/TimePicker.xaml
@@ -2,108 +2,201 @@
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Wpf.Ui.Controls"
- xmlns:system="clr-namespace:System;assembly=System.Runtime">
+ xmlns:converters="clr-namespace:Wpf.Ui.Converters">
- 1,1,1,1
+
+
+
+
+
+ 1
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
+
diff --git a/src/Wpf.Ui/Converters/TimeSpanToAmPmStringConverter.cs b/src/Wpf.Ui/Converters/TimeSpanToAmPmStringConverter.cs
new file mode 100644
index 000000000..ac7fe46fe
--- /dev/null
+++ b/src/Wpf.Ui/Converters/TimeSpanToAmPmStringConverter.cs
@@ -0,0 +1,26 @@
+// This Source Code Form is subject to the terms of the MIT License.
+// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
+// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
+// All Rights Reserved.
+
+using System.Windows.Data;
+
+namespace Wpf.Ui.Converters;
+
+public class TimeSpanToAmPmStringConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is TimeSpan time)
+ {
+ return time.Hours < 12 ? "AM" : "PM";
+ }
+
+ return "AM";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/Wpf.Ui/Converters/TimeSpanToHourMultiConverter.cs b/src/Wpf.Ui/Converters/TimeSpanToHourMultiConverter.cs
new file mode 100644
index 000000000..864d98edd
--- /dev/null
+++ b/src/Wpf.Ui/Converters/TimeSpanToHourMultiConverter.cs
@@ -0,0 +1,40 @@
+// This Source Code Form is subject to the terms of the MIT License.
+// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
+// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
+// All Rights Reserved.
+
+using System.Windows.Data;
+using Wpf.Ui.Controls;
+
+namespace Wpf.Ui.Converters;
+
+public class TimeSpanToHourMultiConverter : IMultiValueConverter
+{
+ public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (values.Length < 2 || !(values[0] is TimeSpan time) || !(values[1] is ClockIdentifier clock))
+ {
+ return "00";
+ }
+
+ if (clock == ClockIdentifier.Clock24Hour)
+ {
+ return time.Hours.ToString("D2");
+ }
+ else // Clock12Hour
+ {
+ int hour = time.Hours % 12;
+ if (hour == 0)
+ {
+ hour = 12;
+ }
+
+ return hour.ToString();
+ }
+ }
+
+ public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}
diff --git a/src/Wpf.Ui/Converters/TimeSpanToMinuteStringConverter.cs b/src/Wpf.Ui/Converters/TimeSpanToMinuteStringConverter.cs
new file mode 100644
index 000000000..8d128d23f
--- /dev/null
+++ b/src/Wpf.Ui/Converters/TimeSpanToMinuteStringConverter.cs
@@ -0,0 +1,26 @@
+// This Source Code Form is subject to the terms of the MIT License.
+// If a copy of the MIT was not distributed with this file, You can obtain one at https://opensource.org/licenses/MIT.
+// Copyright (C) Leszek Pomianowski and WPF UI Contributors.
+// All Rights Reserved.
+
+using System.Windows.Data;
+
+namespace Wpf.Ui.Converters;
+
+public class TimeSpanToMinuteStringConverter : IValueConverter
+{
+ public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ if (value is TimeSpan time)
+ {
+ return time.Minutes.ToString("D2");
+ }
+
+ return "00";
+ }
+
+ public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
+ {
+ throw new NotImplementedException();
+ }
+}