diff --git a/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml b/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml
new file mode 100644
index 0000000..ac20b40
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml
@@ -0,0 +1,58 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml.cs b/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml.cs
new file mode 100644
index 0000000..5811b1b
--- /dev/null
+++ b/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Ursa.Demo.Pages;
+
+public partial class TimeBoxDemo : UserControl
+{
+ public TimeBoxDemo()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
index 15af99e..47eec8a 100644
--- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs
@@ -58,6 +58,7 @@ public class MainViewViewModel : ViewModelBase
MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(),
MenuKeys.MenuKeyThemeToggler => new ThemeTogglerDemoViewModel(),
MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(),
+ MenuKeys.MenuKeyTimeBox => new TimeBoxDemoViewModel(),
MenuKeys.MenuKeyVerificationCode => new VerificationCodeDemoViewModel(),
};
}
diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
index 5a345ef..62d76a7 100644
--- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
+++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs
@@ -45,6 +45,7 @@ public class MenuViewModel: ViewModelBase
new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline },
new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon},
new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar },
+ new() { MenuHeader = "Time Box", Key = MenuKeys.MenuKeyTimeBox, Status = "New" },
new() { MenuHeader = "Verification Code", Key = MenuKeys.MenuKeyVerificationCode, Status = "New" },
};
}
@@ -87,5 +88,6 @@ public static class MenuKeys
public const string MenuKeyThemeToggler = "ThemeToggler";
public const string MenuKeyToolBar = "ToolBar";
public const string MenuKeyVerificationCode = "VerificationCode";
+ public const string MenuKeyTimeBox = "TimeBox";
}
\ No newline at end of file
diff --git a/demo/Ursa.Demo/ViewModels/TimeBoxDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/TimeBoxDemoViewModel.cs
new file mode 100644
index 0000000..729b6c5
--- /dev/null
+++ b/demo/Ursa.Demo/ViewModels/TimeBoxDemoViewModel.cs
@@ -0,0 +1,22 @@
+using System;
+using System.Net;
+using CommunityToolkit.Mvvm.ComponentModel;
+using CommunityToolkit.Mvvm.Input;
+
+namespace Ursa.Demo.ViewModels;
+
+public partial class TimeBoxDemoViewModel : ObservableObject
+{
+ [ObservableProperty] private TimeSpan? _timeSpan;
+
+ [RelayCommand]
+ private void ChangeRandomTime()
+ {
+ TimeSpan = new TimeSpan(Random.Shared.NextInt64(0x00000000FFFFFFFF));
+ }
+
+ public TimeBoxDemoViewModel()
+ {
+ TimeSpan = new TimeSpan(0, 21, 11, 36, 54);
+ }
+}
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/TimeBox.axaml b/src/Ursa.Themes.Semi/Controls/TimeBox.axaml
new file mode 100644
index 0000000..79f625b
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Controls/TimeBox.axaml
@@ -0,0 +1,149 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml
index 9a138a9..d05cb2e 100644
--- a/src/Ursa.Themes.Semi/Controls/_index.axaml
+++ b/src/Ursa.Themes.Semi/Controls/_index.axaml
@@ -35,6 +35,7 @@
+
diff --git a/src/Ursa.Themes.Semi/Styles/TimeBox.axaml b/src/Ursa.Themes.Semi/Styles/TimeBox.axaml
new file mode 100644
index 0000000..cab7e70
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Styles/TimeBox.axaml
@@ -0,0 +1,21 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Styles/_index.axaml b/src/Ursa.Themes.Semi/Styles/_index.axaml
index baa0d9b..868fad8 100644
--- a/src/Ursa.Themes.Semi/Styles/_index.axaml
+++ b/src/Ursa.Themes.Semi/Styles/_index.axaml
@@ -8,5 +8,6 @@
+
diff --git a/src/Ursa.Themes.Semi/Themes/Dark/TimeBox.axaml b/src/Ursa.Themes.Semi/Themes/Dark/TimeBox.axaml
new file mode 100644
index 0000000..7caba95
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Dark/TimeBox.axaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
index 63d0c66..7f6bc9c 100644
--- a/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Dark/_index.axaml
@@ -15,5 +15,6 @@
+
diff --git a/src/Ursa.Themes.Semi/Themes/Light/TimeBox.axaml b/src/Ursa.Themes.Semi/Themes/Light/TimeBox.axaml
new file mode 100644
index 0000000..ed69375
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Light/TimeBox.axaml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
index 63d0c66..7f6bc9c 100644
--- a/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Light/_index.axaml
@@ -15,5 +15,6 @@
+
diff --git a/src/Ursa.Themes.Semi/Themes/Shared/TimeBox.axaml b/src/Ursa.Themes.Semi/Themes/Shared/TimeBox.axaml
new file mode 100644
index 0000000..fa6e5d9
--- /dev/null
+++ b/src/Ursa.Themes.Semi/Themes/Shared/TimeBox.axaml
@@ -0,0 +1,8 @@
+
+
+ 32
+ 24
+ 40
+ 1
+ 3
+
diff --git a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
index b11d979..658f6ae 100644
--- a/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
+++ b/src/Ursa.Themes.Semi/Themes/Shared/_index.axaml
@@ -18,5 +18,6 @@
+
diff --git a/src/Ursa/Controls/TimeBox.cs b/src/Ursa/Controls/TimeBox.cs
new file mode 100644
index 0000000..f238936
--- /dev/null
+++ b/src/Ursa/Controls/TimeBox.cs
@@ -0,0 +1,657 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.Metadata;
+using Avalonia.Controls.Presenters;
+using Avalonia.Controls.Primitives;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Interactivity;
+using Avalonia.Media;
+using Avalonia.Media.TextFormatting;
+using Avalonia.Metadata;
+using Irihi.Avalonia.Shared.Helpers;
+
+namespace Ursa.Controls;
+
+public enum TimeBoxInputMode
+{
+ Normal,
+
+ // In fast mode, automatically move to next session after 2 digits input.
+ Fast,
+}
+
+[TemplatePart(PART_HoursTextPresenter, typeof(TextPresenter))]
+[TemplatePart(PART_MinuteTextPresenter, typeof(TextPresenter))]
+[TemplatePart(PART_SecondTextPresenter, typeof(TextPresenter))]
+[TemplatePart(PART_MillisecondTextPresenter, typeof(TextPresenter))]
+[TemplatePart(PART_HourBorder, typeof(Border))]
+[TemplatePart(PART_MinuteBorder, typeof(Border))]
+[TemplatePart(PART_SecondBorder, typeof(Border))]
+[TemplatePart(PART_MilliSecondBorder, typeof(Border))]
+[TemplatePart(PART_HourDragPanel, typeof(Panel))]
+[TemplatePart(PART_MinuteDragPanel, typeof(Panel))]
+[TemplatePart(PART_SecondDragPanel, typeof(Panel))]
+[TemplatePart(PART_MilliSecondDragPanel, typeof(Panel))]
+public class TimeBox : TemplatedControl
+{
+ public const string PART_HoursTextPresenter = "PART_HourTextPresenter";
+ public const string PART_MinuteTextPresenter = "PART_MinuteTextPresenter";
+ public const string PART_SecondTextPresenter = "PART_SecondTextPresenter";
+ public const string PART_MillisecondTextPresenter = "PART_MillisecondTextPresenter";
+ public const string PART_HourBorder = "PART_HourBorder";
+ public const string PART_MinuteBorder = "PART_MinuteBorder";
+ public const string PART_SecondBorder = "PART_SecondBorder";
+ public const string PART_MilliSecondBorder = "PART_MilliSecondBorder";
+ public const string PART_HourDragPanel = "PART_HourDragPanel";
+ public const string PART_MinuteDragPanel = "PART_MinuteDragPanel";
+ public const string PART_SecondDragPanel = "PART_SecondDragPanel";
+ public const string PART_MilliSecondDragPanel = "PART_MilliSecondDragPanel";
+ private TextPresenter? _hourText;
+ private TextPresenter? _minuteText;
+ private TextPresenter? _secondText;
+ private TextPresenter? _milliSecondText;
+ private Border? _hourBorder;
+ private Border? _minuteBorder;
+ private Border? _secondBorder;
+ private Border? _milliSecondBorder;
+ private Panel? _hourDragPanel;
+ private Panel? _minuteDragPanel;
+ private Panel? _secondDragPanel;
+ private Panel? _milliSecondDragPanel;
+ private readonly TextPresenter?[] _presenters = new TextPresenter?[4];
+ private readonly Border?[] _borders = new Border?[4];
+ private readonly Panel?[] _dragPanels = new Panel?[4];
+ private readonly int[] _limits = new[] { 24, 60, 60, 100 };
+ private int[] _values = new int[4];
+ private bool[] _isShowedCaret = new bool[4];
+ private int? _currentActiveSectionIndex;
+ private bool _isAlreadyDrag = false;
+ private Point _pressedPosition = new Point();
+ private Point? _lastDragPoint;
+
+ public static readonly StyledProperty TimeProperty = AvaloniaProperty.Register(
+ nameof(Time), defaultBindingMode: BindingMode.TwoWay);
+
+ public TimeSpan? Time
+ {
+ get => GetValue(TimeProperty);
+ set => SetValue(TimeProperty, value);
+ }
+
+ public static readonly StyledProperty TextAlignmentProperty =
+ TextBox.TextAlignmentProperty.AddOwner();
+
+ public TextAlignment TextAlignment
+ {
+ get => GetValue(TextAlignmentProperty);
+ set => SetValue(TextAlignmentProperty, value);
+ }
+
+ public static readonly StyledProperty SelectionBrushProperty =
+ TextBox.SelectionBrushProperty.AddOwner();
+
+ public IBrush? SelectionBrush
+ {
+ get => GetValue(SelectionBrushProperty);
+ set => SetValue(SelectionBrushProperty, value);
+ }
+
+ public static readonly StyledProperty SelectionForegroundBrushProperty =
+ TextBox.SelectionForegroundBrushProperty.AddOwner();
+
+ public IBrush? SelectionForegroundBrush
+ {
+ get => GetValue(SelectionForegroundBrushProperty);
+ set => SetValue(SelectionForegroundBrushProperty, value);
+ }
+
+ public static readonly StyledProperty CaretBrushProperty = TextBox.CaretBrushProperty.AddOwner();
+
+ public IBrush? CaretBrush
+ {
+ get => GetValue(CaretBrushProperty);
+ set => SetValue(CaretBrushProperty, value);
+ }
+
+ public static readonly StyledProperty ShowLeadingZeroProperty = AvaloniaProperty.Register(
+ nameof(ShowLeadingZero));
+
+ public bool ShowLeadingZero
+ {
+ get => GetValue(ShowLeadingZeroProperty);
+ set => SetValue(ShowLeadingZeroProperty, value);
+ }
+
+ public static readonly StyledProperty InputModeProperty =
+ AvaloniaProperty.Register(
+ nameof(InputMode));
+
+ public TimeBoxInputMode InputMode
+ {
+ get => GetValue(InputModeProperty);
+ set => SetValue(InputModeProperty, value);
+ }
+
+ public static readonly StyledProperty AllowDragProperty = AvaloniaProperty.Register(
+ nameof(AllowDrag), defaultBindingMode: BindingMode.TwoWay);
+
+ public bool AllowDrag
+ {
+ get => GetValue(AllowDragProperty);
+ set => SetValue(AllowDragProperty, value);
+ }
+
+ public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register(
+ nameof(IsReadOnly), defaultValue: false, defaultBindingMode: BindingMode.TwoWay);
+
+ public bool IsReadOnly
+ {
+ get => GetValue(IsReadOnlyProperty);
+ set => SetValue(IsReadOnlyProperty, value);
+ }
+
+ public static readonly StyledProperty IsTimeLoopProperty = AvaloniaProperty.Register(
+ nameof(IsTimeLoop), defaultBindingMode: BindingMode.TwoWay);
+
+ public bool IsTimeLoop
+ {
+ get => GetValue(IsTimeLoopProperty);
+ set => SetValue(IsTimeLoopProperty, value);
+ }
+
+ static TimeBox()
+ {
+ ShowLeadingZeroProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e));
+ TimeProperty.Changed.AddClassHandler((o, e) => o.OnTimeChanged(e));
+ AllowDragProperty.Changed.AddClassHandler((o, e) => o.OnAllowDragChange(e));
+ }
+
+ private void OnAllowDragChange(AvaloniaPropertyChangedEventArgs args)
+ {
+ IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanels);
+ }
+
+ #region Overrides
+
+ protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
+ {
+ base.OnApplyTemplate(e);
+ _hourText = e.NameScope.Find(PART_HoursTextPresenter);
+ _minuteText = e.NameScope.Find(PART_MinuteTextPresenter);
+ _secondText = e.NameScope.Find(PART_SecondTextPresenter);
+ _milliSecondText = e.NameScope.Find(PART_MillisecondTextPresenter);
+ _hourBorder = e.NameScope.Find(PART_HourBorder);
+ _minuteBorder = e.NameScope.Find(PART_MinuteBorder);
+ _secondBorder = e.NameScope.Find(PART_SecondBorder);
+ _milliSecondBorder = e.NameScope.Find(PART_MilliSecondBorder);
+ _hourDragPanel = e.NameScope.Find(PART_HourDragPanel);
+ _minuteDragPanel = e.NameScope.Find(PART_MinuteDragPanel);
+ _secondDragPanel = e.NameScope.Find(PART_SecondDragPanel);
+ _milliSecondDragPanel = e.NameScope.Find(PART_MilliSecondDragPanel);
+ _presenters[0] = _hourText;
+ _presenters[1] = _minuteText;
+ _presenters[2] = _secondText;
+ _presenters[3] = _milliSecondText;
+ _borders[0] = _hourBorder;
+ _borders[1] = _minuteBorder;
+ _borders[2] = _secondBorder;
+ _borders[3] = _milliSecondBorder;
+ _dragPanels[0] = _hourDragPanel;
+ _dragPanels[1] = _minuteDragPanel;
+ _dragPanels[2] = _secondDragPanel;
+ _dragPanels[3] = _milliSecondDragPanel;
+ IsVisibleProperty.SetValue(AllowDrag, _dragPanels);
+
+
+ if (_hourText != null) _hourText.Text = Time != null ? Time.Value.Hours.ToString() : "0";
+ if (_minuteText != null) _minuteText.Text = Time != null ? Time.Value.Minutes.ToString() : "0";
+ if (_secondText != null) _secondText.Text = Time != null ? Time.Value.Seconds.ToString() : "0";
+ if (_milliSecondText != null)
+ _milliSecondText.Text = Time != null ? ClampMilliSecond(Time.Value.Milliseconds).ToString() : "0";
+ ParseTimeSpan(ShowLeadingZero);
+
+ PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanels);
+ }
+
+ protected override void OnKeyDown(KeyEventArgs e)
+ {
+ if (_currentActiveSectionIndex is null) return;
+ var keymap = TopLevel.GetTopLevel(this)?.PlatformSettings?.HotkeyConfiguration;
+ bool Match(List gestures) => gestures.Any(g => g.Matches(e));
+ if (e.Key is Key.Enter or Key.Return)
+ {
+ ParseTimeSpan(ShowLeadingZero);
+ SetTimeSpanInternal();
+ base.OnKeyDown(e);
+ return;
+ }
+
+ if (e.Key == Key.Tab)
+ {
+ if (_currentActiveSectionIndex.Value == 3)
+ {
+ base.OnKeyDown(e);
+ return;
+ }
+
+ MoveToNextSection(_currentActiveSectionIndex.Value);
+ e.Handled = true;
+ }
+ else if (e.Key == Key.Back)
+ {
+ DeleteImplementation(_currentActiveSectionIndex.Value);
+ }
+ else if (e.Key == Key.Right)
+ {
+ OnPressRightKey();
+ }
+ else if (e.Key == Key.Left)
+ {
+ OnPressLeftKey();
+ }
+ else
+ {
+ base.OnKeyDown(e);
+ }
+ }
+
+ protected override void OnTextInput(TextInputEventArgs e)
+ {
+ if (e.Handled) return;
+ string? s = e.Text;
+ if (string.IsNullOrEmpty(s)) return;
+ if (!char.IsNumber(s![0])) return;
+ if (_currentActiveSectionIndex.HasValue && _presenters[_currentActiveSectionIndex.Value] != null)
+ {
+ int caretIndex = Math.Min(_presenters[_currentActiveSectionIndex.Value].CaretIndex
+ , _presenters[_currentActiveSectionIndex.Value].Text.Length);
+ string? oldText = _presenters[_currentActiveSectionIndex.Value].Text;
+ if (oldText is null)
+ {
+ _presenters[_currentActiveSectionIndex.Value].Text = s;
+ _presenters[_currentActiveSectionIndex.Value].MoveCaretHorizontal();
+ }
+ else
+ {
+ _presenters[_currentActiveSectionIndex.Value].DeleteSelection();
+ _presenters[_currentActiveSectionIndex.Value].ClearSelection();
+ oldText = _presenters[_currentActiveSectionIndex.Value].Text;
+
+ string newText = string.IsNullOrEmpty(oldText)
+ ? s
+ : oldText?.Substring(0, caretIndex) + s + oldText?.Substring(Math.Min(caretIndex, oldText.Length));
+ if (newText.Length > 2)
+ {
+ newText = newText.Substring(0, 2);
+ }
+
+ _presenters[_currentActiveSectionIndex.Value].Text = newText;
+ Console.WriteLine(
+ $"OnTextInput @ _secondText HashCode: {_presenters[_currentActiveSectionIndex.Value]?.GetHashCode()}");
+ _presenters[_currentActiveSectionIndex.Value].MoveCaretHorizontal();
+ if (_presenters[_currentActiveSectionIndex.Value].CaretIndex == 2 && InputMode == TimeBoxInputMode.Fast)
+ {
+ MoveToNextSection(_currentActiveSectionIndex.Value);
+ }
+ }
+ }
+ }
+
+ protected override void OnPointerPressed(PointerPressedEventArgs e)
+ {
+ _pressedPosition = e.GetPosition(_hourBorder);
+ _lastDragPoint = _pressedPosition;
+ for (int i = 0; i < 4; ++i)
+ {
+ if (_borders[i]?.Bounds.Contains(_pressedPosition) ?? false)
+ {
+ _currentActiveSectionIndex = i;
+ }
+ else
+ {
+ LeaveSection(i);
+ }
+ }
+ }
+
+ protected override void OnPointerReleased(PointerReleasedEventArgs e)
+ {
+ if (_currentActiveSectionIndex is null) return;
+ if (_isAlreadyDrag)
+ {
+ _isAlreadyDrag = false;
+ }
+ else
+ {
+ EnterSection(_currentActiveSectionIndex.Value);
+ }
+ }
+
+ protected override void OnLostFocus(RoutedEventArgs e)
+ {
+ for (int i = 0; i < 4; ++i)
+ {
+ LeaveSection(i);
+ }
+
+ _currentActiveSectionIndex = null;
+ ParseTimeSpan(ShowLeadingZero);
+ SetTimeSpanInternal();
+ }
+
+ protected override void OnGotFocus(GotFocusEventArgs e)
+ {
+ }
+
+ #endregion
+
+ private void OnFormatChange(AvaloniaPropertyChangedEventArgs arg)
+ {
+ bool showLeadingZero = arg.GetNewValue();
+ ParseTimeSpan(showLeadingZero);
+ }
+
+ private void OnTimeChanged(AvaloniaPropertyChangedEventArgs arg)
+ {
+ TimeSpan? timeSpan = arg.GetNewValue();
+ if (timeSpan is null)
+ {
+ if (_hourText != null) _hourText.Text = String.Empty;
+ if (_minuteText != null) _minuteText.Text = String.Empty;
+ if (_secondText != null) _secondText.Text = String.Empty;
+ if (_milliSecondText != null) _milliSecondText.Text = String.Empty;
+ ParseTimeSpan(ShowLeadingZero);
+ }
+ else
+ {
+ if (_hourText != null) _hourText.Text = timeSpan.Value.Hours.ToString();
+ if (_minuteText != null) _minuteText.Text = timeSpan.Value.Minutes.ToString();
+ if (_secondText != null) _secondText.Text = timeSpan.Value.Seconds.ToString();
+ if (_milliSecondText != null) _milliSecondText.Text = (timeSpan.Value.Milliseconds / 10).ToString();
+ ParseTimeSpan(ShowLeadingZero);
+ }
+ }
+
+ private void ParseTimeSpan(bool showLeadingZero, bool skipParseFromText = false)
+ {
+ string format = showLeadingZero ? "D2" : "";
+ Console.WriteLine($"ParseTimeSpan @ _secondText HashCode: {_secondText?.GetHashCode()}");
+ if (_hourText is null || _minuteText is null || _secondText is null || _milliSecondText is null)
+ {
+ _values[0] = 0;
+ _values[1] = 0;
+ _values[2] = 0;
+ _values[3] = 0;
+ return;
+ }
+
+ if (!skipParseFromText)
+ {
+ _values[0] = int.TryParse(_hourText.Text, out int hour) ? hour : 0;
+ _values[1] = int.TryParse(_minuteText.Text, out int minute) ? minute : 0;
+ _values[2] = int.TryParse(_secondText.Text, out int second) ? second : 0;
+ _values[3] = int.TryParse(_milliSecondText.Text, out int millisecond) ? millisecond : 0;
+ }
+
+ VerifyTimeValue();
+
+ _hourText.Text = _values[0].ToString(format);
+ _minuteText.Text = _values[1].ToString(format);
+ _secondText.Text = _values[2].ToString(format);
+ _milliSecondText.Text = _values[3].ToString(format);
+ }
+ private void OnDragPanelPointerMoved(object sender, PointerEventArgs e)
+ {
+ if (!AllowDrag || IsReadOnly) return;
+ if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
+ var point = e.GetPosition(this);
+ var delta = point - _lastDragPoint;
+ if (delta is null)
+ {
+ return;
+ }
+
+ int d = GetDelta(delta.Value);
+ if (d > 0)
+ {
+ Increase();
+ _isAlreadyDrag = true;
+ }
+ else if (d < 0)
+ {
+ Decrease();
+ _isAlreadyDrag = true;
+ }
+
+ _lastDragPoint = point;
+ }
+
+ private int GetDelta(Point point)
+ {
+ return point.X switch
+ {
+ > 0 => 1,
+ < 0 => -1,
+ _ => 0
+ };
+ }
+
+ private void EnterSection(int index)
+ {
+ if (!_isShowedCaret[index])
+ {
+ if (AllowDrag && _dragPanels[index] != null)
+ _dragPanels[index].IsVisible = false;
+ _presenters[index].ShowCaret();
+ _isShowedCaret[index] = true;
+ _presenters[index].SelectAll();
+ }
+ else
+ {
+ _presenters[index].ClearSelection();
+ var caretPosition =
+ _pressedPosition.WithX(_pressedPosition.X - _borders[index].Bounds.X);
+ _presenters[index].MoveCaretToPoint(caretPosition);
+ }
+ }
+
+ private void LeaveSection(int index)
+ {
+ if (_presenters[index] is null) return;
+ _presenters[index].ClearSelection();
+ if (_isShowedCaret[index])
+ {
+ _presenters[index].HideCaret();
+ _isShowedCaret[index] = false;
+ }
+
+ if (AllowDrag && _dragPanels[index] != null)
+ _dragPanels[index].IsVisible = true;
+ }
+
+ private bool MoveToNextSection(int index)
+ {
+ if (_presenters[index] is null) return false;
+ if (index == 3) return false;
+ LeaveSection(index);
+ _currentActiveSectionIndex = index + 1;
+ EnterSection(_currentActiveSectionIndex.Value);
+ return true;
+ }
+
+ private bool MoveToPreviousSection(int index)
+ {
+ if (_presenters[index] is null) return false;
+ if (index == 0) return false;
+ LeaveSection(index);
+ _currentActiveSectionIndex = index - 1;
+ EnterSection(_currentActiveSectionIndex.Value);
+ return true;
+ }
+
+ private void OnPressRightKey()
+ {
+ if (_currentActiveSectionIndex is null) return;
+ var index = _currentActiveSectionIndex.Value;
+ if (_presenters[index].IsTextSelected())
+ {
+ int end = _presenters[index].SelectionEnd;
+ _presenters[index].ClearSelection();
+ _presenters[index].MoveCaretToTextPosition(end);
+ return;
+ }
+
+ if (_presenters[index].CaretIndex >= _presenters[index].Text?.Length)
+ {
+ MoveToNextSection(index);
+ }
+ else
+ {
+ _presenters[index].ClearSelection();
+ _presenters[index].CaretIndex++;
+ }
+ }
+
+ private void OnPressLeftKey()
+ {
+ if (_currentActiveSectionIndex is null) return;
+ var index = _currentActiveSectionIndex.Value;
+ if (_presenters[index].IsTextSelected())
+ {
+ int start = _presenters[index].SelectionStart;
+ _presenters[index].ClearSelection();
+ _presenters[index].MoveCaretToTextPosition(start);
+ return;
+ }
+
+ if (_presenters[index].CaretIndex == 0)
+ {
+ MoveToPreviousSection(index);
+ }
+ else
+ {
+ _presenters[index].ClearSelection();
+ _presenters[index].CaretIndex--;
+ }
+ }
+
+ private void SetTimeSpanInternal()
+ {
+ try
+ {
+ Time = new TimeSpan(0, _values[0], _values[1], _values[2], _values[3] * 10);
+ }
+ catch
+ {
+ Time = TimeSpan.Zero;
+ }
+ }
+
+ private void DeleteImplementation(int index)
+ {
+ if (_presenters[index] is null) return;
+ var oldText = _presenters[index].Text;
+ if (_presenters[index].SelectionStart != _presenters[index].SelectionEnd)
+ {
+ _presenters[index].DeleteSelection();
+ _presenters[index].ClearSelection();
+ }
+ else if (string.IsNullOrWhiteSpace(oldText) || _presenters[index].CaretIndex == 0)
+ {
+ MoveToPreviousSection(index);
+ }
+ else
+ {
+ int caretIndex = _presenters[index].CaretIndex;
+ string newText = oldText?.Substring(0, caretIndex - 1) +
+ oldText?.Substring(Math.Min(caretIndex, oldText.Length));
+ _presenters[index].MoveCaretHorizontal(LogicalDirection.Backward);
+ _presenters[index].Text = newText;
+ }
+ }
+
+ private bool HandlingCarry(int index, int lowerCarry = 0)
+ {
+ if (index < 0)
+ return IsTimeLoop;
+ _values[index] += lowerCarry;
+ int carry = _values[index] >= 0 ? _values[index] / _limits[index] : -1 + (_values[index] / _limits[index]);
+ if (carry == 0) return true;
+ bool success = false;
+ if (carry > 0)
+ {
+ success = HandlingCarry(index - 1, carry);
+ if (success)
+ {
+ _values[index] %= _limits[index];
+ }
+ else
+ {
+ _values[index] = _limits[index] - 1;
+ }
+ }
+ else
+ {
+ success = HandlingCarry(index - 1, carry);
+ if (success)
+ {
+ _values[index] += _limits[index];
+ }
+ else
+ {
+ _values[index] = 0;
+ }
+ }
+ return success;
+ }
+
+ private void VerifyTimeValue()
+ {
+ for (int i = 3; i >= 0; --i)
+ {
+ HandlingCarry(i);
+ }
+ }
+
+ private void Increase()
+ {
+ if(_currentActiveSectionIndex is null)return;
+ if(_currentActiveSectionIndex.Value == 0)
+ _values[0] += 1;
+ else if(_currentActiveSectionIndex.Value == 1)
+ _values[1] += 1;
+ else if(_currentActiveSectionIndex.Value == 2)
+ _values[2] += 1;
+ else if(_currentActiveSectionIndex.Value == 3)
+ _values[3] += 1;
+ ParseTimeSpan(ShowLeadingZero, true);
+ SetTimeSpanInternal();
+ }
+
+ private void Decrease()
+ {
+ if(_currentActiveSectionIndex is null)return;
+ if(_currentActiveSectionIndex.Value == 0)
+ _values[0] -= 1;
+ else if(_currentActiveSectionIndex.Value == 1)
+ _values[1] -= 1;
+ else if(_currentActiveSectionIndex.Value == 2)
+ _values[2] -= 1;
+ else if(_currentActiveSectionIndex.Value == 3)
+ _values[3] -= 1;
+ ParseTimeSpan(ShowLeadingZero, true);
+ SetTimeSpanInternal();
+ }
+
+ private int ClampMilliSecond(int milliSecond)
+ {
+ while (milliSecond % 100 != milliSecond)
+ {
+ milliSecond /= 10;
+ }
+
+ return milliSecond;
+ }
+}
\ No newline at end of file