From 72d962ab45eb531f29caef78e8dcc1cce292015c Mon Sep 17 00:00:00 2001 From: LiWenhao Date: Sat, 6 Apr 2024 21:28:14 +0800 Subject: [PATCH 1/6] Add TimeBox Control --- demo/Ursa.Demo/Pages/TimeBoxDemo.axaml | 58 ++ demo/Ursa.Demo/Pages/TimeBoxDemo.axaml.cs | 13 + .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 2 + .../ViewModels/TimeBoxDemoViewModel.cs | 22 + src/Ursa.Themes.Semi/Controls/TimeBox.axaml | 149 ++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + src/Ursa.Themes.Semi/Styles/TimeBox.axaml | 21 + src/Ursa.Themes.Semi/Styles/_index.axaml | 1 + .../Themes/Dark/TimeBox.axaml | 12 + src/Ursa.Themes.Semi/Themes/Dark/_index.axaml | 1 + .../Themes/Light/TimeBox.axaml | 12 + .../Themes/Light/_index.axaml | 1 + .../Themes/Shared/TimeBox.axaml | 8 + .../Themes/Shared/_index.axaml | 1 + src/Ursa/Controls/TimeBox.cs | 657 ++++++++++++++++++ 16 files changed, 960 insertions(+) create mode 100644 demo/Ursa.Demo/Pages/TimeBoxDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/TimeBoxDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/TimeBoxDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/TimeBox.axaml create mode 100644 src/Ursa.Themes.Semi/Styles/TimeBox.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/Dark/TimeBox.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/Light/TimeBox.axaml create mode 100644 src/Ursa.Themes.Semi/Themes/Shared/TimeBox.axaml create mode 100644 src/Ursa/Controls/TimeBox.cs 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 From 436faccd9b4a0265949dd55c0a9ec019271e66da Mon Sep 17 00:00:00 2001 From: LiWenhao Date: Sun, 7 Apr 2024 10:57:57 +0800 Subject: [PATCH 2/6] delete console.writeline; add vertical drag; improve code style; --- demo/Ursa.Demo/Pages/TimeBoxDemo.axaml | 9 +++ src/Ursa.Themes.Semi/Controls/TimeBox.axaml | 22 ++++--- src/Ursa/Controls/TimeBox.cs | 65 ++++++++++++--------- 3 files changed, 61 insertions(+), 35 deletions(-) diff --git a/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml b/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml index ac20b40..f691986 100644 --- a/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml +++ b/demo/Ursa.Demo/Pages/TimeBoxDemo.axaml @@ -42,6 +42,15 @@ AllowDrag="{Binding #allowDrag.IsChecked}" IsTimeLoop="{Binding #isTimeLoop.IsChecked}"/> + + + + Background="Transparent"/> + Background="Transparent"/> + Background="Transparent"/> + Background="Transparent"/> @@ -145,5 +141,15 @@ + + + + \ No newline at end of file diff --git a/src/Ursa/Controls/TimeBox.cs b/src/Ursa/Controls/TimeBox.cs index f238936..92c24b2 100644 --- a/src/Ursa/Controls/TimeBox.cs +++ b/src/Ursa/Controls/TimeBox.cs @@ -22,6 +22,12 @@ public enum TimeBoxInputMode Fast, } +public enum TimeBoxDragOrientation +{ + Horizontal, + Vertical, +} + [TemplatePart(PART_HoursTextPresenter, typeof(TextPresenter))] [TemplatePart(PART_MinuteTextPresenter, typeof(TextPresenter))] [TemplatePart(PART_SecondTextPresenter, typeof(TextPresenter))] @@ -143,15 +149,15 @@ public class TimeBox : TemplatedControl set => SetValue(AllowDragProperty, value); } - public static readonly StyledProperty IsReadOnlyProperty = AvaloniaProperty.Register( - nameof(IsReadOnly), defaultValue: false, defaultBindingMode: BindingMode.TwoWay); + public static readonly StyledProperty DragOrientationProperty + = AvaloniaProperty.Register(nameof(DragOrientation), defaultValue: TimeBoxDragOrientation.Horizontal); - public bool IsReadOnly + public TimeBoxDragOrientation DragOrientation { - get => GetValue(IsReadOnlyProperty); - set => SetValue(IsReadOnlyProperty, value); + get => GetValue(DragOrientationProperty); + set => SetValue(DragOrientationProperty, value); } - + public static readonly StyledProperty IsTimeLoopProperty = AvaloniaProperty.Register( nameof(IsTimeLoop), defaultBindingMode: BindingMode.TwoWay); @@ -168,11 +174,6 @@ public class TimeBox : TemplatedControl 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) @@ -288,8 +289,6 @@ public class TimeBox : TemplatedControl } _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) { @@ -341,10 +340,6 @@ public class TimeBox : TemplatedControl SetTimeSpanInternal(); } - protected override void OnGotFocus(GotFocusEventArgs e) - { - } - #endregion private void OnFormatChange(AvaloniaPropertyChangedEventArgs arg) @@ -353,6 +348,11 @@ public class TimeBox : TemplatedControl ParseTimeSpan(showLeadingZero); } + private void OnAllowDragChange(AvaloniaPropertyChangedEventArgs args) + { + IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanels); + } + private void OnTimeChanged(AvaloniaPropertyChangedEventArgs arg) { TimeSpan? timeSpan = arg.GetNewValue(); @@ -369,7 +369,7 @@ public class TimeBox : TemplatedControl 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(); + if (_milliSecondText != null) _milliSecondText.Text = ClampMilliSecond(timeSpan.Value.Milliseconds).ToString(); ParseTimeSpan(ShowLeadingZero); } } @@ -377,7 +377,6 @@ public class TimeBox : TemplatedControl 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; @@ -404,7 +403,7 @@ public class TimeBox : TemplatedControl } private void OnDragPanelPointerMoved(object sender, PointerEventArgs e) { - if (!AllowDrag || IsReadOnly) return; + if (!AllowDrag) return; if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; var point = e.GetPosition(this); var delta = point - _lastDragPoint; @@ -430,12 +429,24 @@ public class TimeBox : TemplatedControl private int GetDelta(Point point) { - return point.X switch + switch (DragOrientation) { - > 0 => 1, - < 0 => -1, - _ => 0 - }; + case TimeBoxDragOrientation.Horizontal: + return point.X switch + { + > 0 => 1, + < 0 => -1, + _ => 0 + }; + case TimeBoxDragOrientation.Vertical: + return point.Y switch + { + > 0 => -1, + < 0 => 1, + _ => 0 + }; + } + return 0; } private void EnterSection(int index) @@ -627,7 +638,7 @@ public class TimeBox : TemplatedControl else if(_currentActiveSectionIndex.Value == 3) _values[3] += 1; ParseTimeSpan(ShowLeadingZero, true); - SetTimeSpanInternal(); + //SetTimeSpanInternal(); } private void Decrease() @@ -642,7 +653,7 @@ public class TimeBox : TemplatedControl else if(_currentActiveSectionIndex.Value == 3) _values[3] -= 1; ParseTimeSpan(ShowLeadingZero, true); - SetTimeSpanInternal(); + //SetTimeSpanInternal(); } private int ClampMilliSecond(int milliSecond) From f31ba22a5a94803cffc08e17c1f4377e471e2757 Mon Sep 17 00:00:00 2001 From: LiWenhao Date: Mon, 8 Apr 2024 16:10:44 +0800 Subject: [PATCH 3/6] fix nullable issue --- src/Ursa/Controls/TimeBox.cs | 198 +++++++++++++++++------------------ 1 file changed, 99 insertions(+), 99 deletions(-) diff --git a/src/Ursa/Controls/TimeBox.cs b/src/Ursa/Controls/TimeBox.cs index 92c24b2..57b3a6f 100644 --- a/src/Ursa/Controls/TimeBox.cs +++ b/src/Ursa/Controls/TimeBox.cs @@ -66,12 +66,12 @@ public class TimeBox : TemplatedControl 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 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[] _values = new[] { 0, 0, 0, 0 }; + private bool[] _isShowedCaret = new[] { false, false, false, false }; private int? _currentActiveSectionIndex; private bool _isAlreadyDrag = false; private Point _pressedPosition = new Point(); @@ -96,7 +96,7 @@ public class TimeBox : TemplatedControl } public static readonly StyledProperty SelectionBrushProperty = - TextBox.SelectionBrushProperty.AddOwner(); + TextBox.SelectionBrushProperty.AddOwner(); public IBrush? SelectionBrush { @@ -105,7 +105,7 @@ public class TimeBox : TemplatedControl } public static readonly StyledProperty SelectionForegroundBrushProperty = - TextBox.SelectionForegroundBrushProperty.AddOwner(); + TextBox.SelectionForegroundBrushProperty.AddOwner(); public IBrush? SelectionForegroundBrush { @@ -113,7 +113,7 @@ public class TimeBox : TemplatedControl set => SetValue(SelectionForegroundBrushProperty, value); } - public static readonly StyledProperty CaretBrushProperty = TextBox.CaretBrushProperty.AddOwner(); + public static readonly StyledProperty CaretBrushProperty = TextBox.CaretBrushProperty.AddOwner(); public IBrush? CaretBrush { @@ -150,14 +150,15 @@ public class TimeBox : TemplatedControl } public static readonly StyledProperty DragOrientationProperty - = AvaloniaProperty.Register(nameof(DragOrientation), defaultValue: TimeBoxDragOrientation.Horizontal); + = AvaloniaProperty.Register(nameof(DragOrientation), + defaultValue: TimeBoxDragOrientation.Horizontal); public TimeBoxDragOrientation DragOrientation { get => GetValue(DragOrientationProperty); set => SetValue(DragOrientationProperty, value); } - + public static readonly StyledProperty IsTimeLoopProperty = AvaloniaProperty.Register( nameof(IsTimeLoop), defaultBindingMode: BindingMode.TwoWay); @@ -166,7 +167,7 @@ public class TimeBox : TemplatedControl get => GetValue(IsTimeLoopProperty); set => SetValue(IsTimeLoopProperty, value); } - + static TimeBox() { ShowLeadingZeroProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e)); @@ -179,18 +180,19 @@ public class TimeBox : TemplatedControl 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); + _hourText = e.NameScope.Get(PART_HoursTextPresenter); + _minuteText = e.NameScope.Get(PART_MinuteTextPresenter); + _secondText = e.NameScope.Get(PART_SecondTextPresenter); + _milliSecondText = e.NameScope.Get(PART_MillisecondTextPresenter); + _hourBorder = e.NameScope.Get(PART_HourBorder); + _minuteBorder = e.NameScope.Get(PART_MinuteBorder); + _secondBorder = e.NameScope.Get(PART_SecondBorder); + _milliSecondBorder = e.NameScope.Get(PART_MilliSecondBorder); + _hourDragPanel = e.NameScope.Get(PART_HourDragPanel); + _minuteDragPanel = e.NameScope.Get(PART_MinuteDragPanel); + _secondDragPanel = e.NameScope.Get(PART_SecondDragPanel); + _milliSecondDragPanel = e.NameScope.Get(PART_MilliSecondDragPanel); + _presenters[0] = _hourText; _presenters[1] = _minuteText; _presenters[2] = _secondText; @@ -205,12 +207,10 @@ public class TimeBox : TemplatedControl _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"; + _hourText.Text = Time != null ? Time.Value.Hours.ToString() : "0"; + _minuteText.Text = Time != null ? Time.Value.Minutes.ToString() : "0"; + _secondText.Text = Time != null ? Time.Value.Seconds.ToString() : "0"; + _milliSecondText.Text = Time != null ? ClampMilliSecond(Time.Value.Milliseconds).ToString() : "0"; ParseTimeSpan(ShowLeadingZero); PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanels); @@ -264,36 +264,36 @@ public class TimeBox : TemplatedControl string? s = e.Text; if (string.IsNullOrEmpty(s)) return; if (!char.IsNumber(s![0])) return; - if (_currentActiveSectionIndex.HasValue && _presenters[_currentActiveSectionIndex.Value] != null) + if (_currentActiveSectionIndex is null) return; + + int caretIndex = Math.Min(_presenters[_currentActiveSectionIndex.Value].CaretIndex + , _presenters[_currentActiveSectionIndex.Value].Text.Length); + + if (_presenters[_currentActiveSectionIndex.Value].Text is 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(); + string oldText = _presenters[_currentActiveSectionIndex.Value].Text; + string newText = oldText.Length == 0 + ? s + : oldText.Substring(0, caretIndex) + s + oldText.Substring(Math.Min(caretIndex, oldText.Length)); + + // Limit the maximum number of input digits + if (newText.Length > 2) { - _presenters[_currentActiveSectionIndex.Value].Text = s; - _presenters[_currentActiveSectionIndex.Value].MoveCaretHorizontal(); + newText = newText.Substring(0, 2); } - else + + _presenters[_currentActiveSectionIndex.Value].Text = newText; + _presenters[_currentActiveSectionIndex.Value].MoveCaretHorizontal(); + if (_presenters[_currentActiveSectionIndex.Value].CaretIndex == 2 && InputMode == TimeBoxInputMode.Fast) { - _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; - _presenters[_currentActiveSectionIndex.Value].MoveCaretHorizontal(); - if (_presenters[_currentActiveSectionIndex.Value].CaretIndex == 2 && InputMode == TimeBoxInputMode.Fast) - { - MoveToNextSection(_currentActiveSectionIndex.Value); - } + MoveToNextSection(_currentActiveSectionIndex.Value); } } } @@ -304,7 +304,7 @@ public class TimeBox : TemplatedControl _lastDragPoint = _pressedPosition; for (int i = 0; i < 4; ++i) { - if (_borders[i]?.Bounds.Contains(_pressedPosition) ?? false) + if (_borders[i].Bounds.Contains(_pressedPosition)) { _currentActiveSectionIndex = i; } @@ -326,6 +326,8 @@ public class TimeBox : TemplatedControl { EnterSection(_currentActiveSectionIndex.Value); } + + _lastDragPoint = null; } protected override void OnLostFocus(RoutedEventArgs e) @@ -344,6 +346,10 @@ public class TimeBox : TemplatedControl private void OnFormatChange(AvaloniaPropertyChangedEventArgs arg) { + // this function will be call ahead of OnApplyTemplate() if Set ShowLeadingZero in axaml, so that _xxxText could be null + if (_hourText is null || _minuteText is null || _secondText is null || _milliSecondText is null) + return; + bool showLeadingZero = arg.GetNewValue(); ParseTimeSpan(showLeadingZero); } @@ -352,24 +358,28 @@ public class TimeBox : TemplatedControl { IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanels); } - + private void OnTimeChanged(AvaloniaPropertyChangedEventArgs arg) { + // this function will be call ahead of OnApplyTemplate() if bind Time in axaml, so that _xxxText could be null + if (_hourText is null || _minuteText is null || _secondText is null || _milliSecondText is null) + return; + 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; + _hourText.Text = String.Empty; + _minuteText.Text = String.Empty; + _secondText.Text = String.Empty; + _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 = ClampMilliSecond(timeSpan.Value.Milliseconds).ToString(); + _hourText.Text = timeSpan.Value.Hours.ToString(); + _minuteText.Text = timeSpan.Value.Minutes.ToString(); + _secondText.Text = timeSpan.Value.Seconds.ToString(); + _milliSecondText.Text = ClampMilliSecond(timeSpan.Value.Milliseconds).ToString(); ParseTimeSpan(ShowLeadingZero); } } @@ -377,14 +387,6 @@ public class TimeBox : TemplatedControl private void ParseTimeSpan(bool showLeadingZero, bool skipParseFromText = false) { string format = showLeadingZero ? "D2" : ""; - 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) { @@ -401,17 +403,14 @@ public class TimeBox : TemplatedControl _secondText.Text = _values[2].ToString(format); _milliSecondText.Text = _values[3].ToString(format); } + private void OnDragPanelPointerMoved(object sender, PointerEventArgs e) { if (!AllowDrag) return; if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return; var point = e.GetPosition(this); var delta = point - _lastDragPoint; - if (delta is null) - { - return; - } - + if (delta is null) return; int d = GetDelta(delta.Value); if (d > 0) { @@ -446,11 +445,13 @@ public class TimeBox : TemplatedControl _ => 0 }; } + return 0; } private void EnterSection(int index) { + if(index < 0 || index > 3) return; if (!_isShowedCaret[index]) { if (AllowDrag && _dragPanels[index] != null) @@ -470,7 +471,7 @@ public class TimeBox : TemplatedControl private void LeaveSection(int index) { - if (_presenters[index] is null) return; + if(index < 0 || index > 3) return; _presenters[index].ClearSelection(); if (_isShowedCaret[index]) { @@ -484,8 +485,7 @@ public class TimeBox : TemplatedControl private bool MoveToNextSection(int index) { - if (_presenters[index] is null) return false; - if (index == 3) return false; + if(index < 0 || index >= 3) return false; LeaveSection(index); _currentActiveSectionIndex = index + 1; EnterSection(_currentActiveSectionIndex.Value); @@ -494,8 +494,7 @@ public class TimeBox : TemplatedControl private bool MoveToPreviousSection(int index) { - if (_presenters[index] is null) return false; - if (index == 0) return false; + if(index <= 0 || index > 3) return false; LeaveSection(index); _currentActiveSectionIndex = index - 1; EnterSection(_currentActiveSectionIndex.Value); @@ -562,7 +561,7 @@ public class TimeBox : TemplatedControl private void DeleteImplementation(int index) { - if (_presenters[index] is null) return; + if(index < 0 || index > 3) return; var oldText = _presenters[index].Text; if (_presenters[index].SelectionStart != _presenters[index].SelectionEnd) { @@ -582,7 +581,7 @@ public class TimeBox : TemplatedControl _presenters[index].Text = newText; } } - + private bool HandlingCarry(int index, int lowerCarry = 0) { if (index < 0) @@ -615,9 +614,10 @@ public class TimeBox : TemplatedControl _values[index] = 0; } } + return success; } - + private void VerifyTimeValue() { for (int i = 3; i >= 0; --i) @@ -625,37 +625,37 @@ public class TimeBox : TemplatedControl HandlingCarry(i); } } - + private void Increase() { - if(_currentActiveSectionIndex is null)return; - if(_currentActiveSectionIndex.Value == 0) + if (_currentActiveSectionIndex is null) return; + if (_currentActiveSectionIndex.Value == 0) _values[0] += 1; - else if(_currentActiveSectionIndex.Value == 1) + else if (_currentActiveSectionIndex.Value == 1) _values[1] += 1; - else if(_currentActiveSectionIndex.Value == 2) + else if (_currentActiveSectionIndex.Value == 2) _values[2] += 1; - else if(_currentActiveSectionIndex.Value == 3) + else if (_currentActiveSectionIndex.Value == 3) _values[3] += 1; ParseTimeSpan(ShowLeadingZero, true); //SetTimeSpanInternal(); } - + private void Decrease() { - if(_currentActiveSectionIndex is null)return; - if(_currentActiveSectionIndex.Value == 0) + if (_currentActiveSectionIndex is null) return; + if (_currentActiveSectionIndex.Value == 0) _values[0] -= 1; - else if(_currentActiveSectionIndex.Value == 1) + else if (_currentActiveSectionIndex.Value == 1) _values[1] -= 1; - else if(_currentActiveSectionIndex.Value == 2) + else if (_currentActiveSectionIndex.Value == 2) _values[2] -= 1; - else if(_currentActiveSectionIndex.Value == 3) + else if (_currentActiveSectionIndex.Value == 3) _values[3] -= 1; ParseTimeSpan(ShowLeadingZero, true); //SetTimeSpanInternal(); } - + private int ClampMilliSecond(int milliSecond) { while (milliSecond % 100 != milliSecond) From 7bc486792cfa0fbd370a93ba85c0b2753da4ccfa Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 13 Apr 2024 22:47:25 +0800 Subject: [PATCH 4/6] fix: fix various nullable reference issue. --- src/Ursa/Controls/TimeBox.cs | 44 +++++++++++++++++------------------- 1 file changed, 21 insertions(+), 23 deletions(-) diff --git a/src/Ursa/Controls/TimeBox.cs b/src/Ursa/Controls/TimeBox.cs index 57b3a6f..4f7bcb0 100644 --- a/src/Ursa/Controls/TimeBox.cs +++ b/src/Ursa/Controls/TimeBox.cs @@ -5,11 +5,9 @@ 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; @@ -70,11 +68,11 @@ public class TimeBox : TemplatedControl 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[] { 0, 0, 0, 0 }; - private bool[] _isShowedCaret = new[] { false, false, false, false }; + private readonly int[] _values = new[] { 0, 0, 0, 0 }; + private readonly bool[] _isShowedCaret = new[] { false, false, false, false }; private int? _currentActiveSectionIndex; - private bool _isAlreadyDrag = false; - private Point _pressedPosition = new Point(); + private bool _isAlreadyDrag; + private Point _pressedPosition; private Point? _lastDragPoint; public static readonly StyledProperty TimeProperty = AvaloniaProperty.Register( @@ -172,7 +170,7 @@ public class TimeBox : TemplatedControl { ShowLeadingZeroProperty.Changed.AddClassHandler((o, e) => o.OnFormatChange(e)); TimeProperty.Changed.AddClassHandler((o, e) => o.OnTimeChanged(e)); - AllowDragProperty.Changed.AddClassHandler((o, e) => o.OnAllowDragChange(e)); + AllowDragProperty.Changed.AddClassHandler((o, e) => o.OnAllowDragChanged(e)); } #region Overrides @@ -205,7 +203,7 @@ public class TimeBox : TemplatedControl _dragPanels[1] = _minuteDragPanel; _dragPanels[2] = _secondDragPanel; _dragPanels[3] = _milliSecondDragPanel; - IsVisibleProperty.SetValue(AllowDrag, _dragPanels); + IsVisibleProperty.SetValue(AllowDrag, _dragPanels[0], _dragPanels[1], _dragPanels[2], _dragPanels[3]); _hourText.Text = Time != null ? Time.Value.Hours.ToString() : "0"; _minuteText.Text = Time != null ? Time.Value.Minutes.ToString() : "0"; @@ -213,7 +211,7 @@ public class TimeBox : TemplatedControl _milliSecondText.Text = Time != null ? ClampMilliSecond(Time.Value.Milliseconds).ToString() : "0"; ParseTimeSpan(ShowLeadingZero); - PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanels); + PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanels[0], _dragPanels[1], _dragPanels[2], _dragPanels[3]); } protected override void OnKeyDown(KeyEventArgs e) @@ -267,7 +265,7 @@ public class TimeBox : TemplatedControl if (_currentActiveSectionIndex is null) return; int caretIndex = Math.Min(_presenters[_currentActiveSectionIndex.Value].CaretIndex - , _presenters[_currentActiveSectionIndex.Value].Text.Length); + , _presenters[_currentActiveSectionIndex.Value].Text?.Length ?? 0); if (_presenters[_currentActiveSectionIndex.Value].Text is null) { @@ -278,7 +276,7 @@ public class TimeBox : TemplatedControl { _presenters[_currentActiveSectionIndex.Value].DeleteSelection(); _presenters[_currentActiveSectionIndex.Value].ClearSelection(); - string oldText = _presenters[_currentActiveSectionIndex.Value].Text; + string oldText = _presenters[_currentActiveSectionIndex.Value].Text ?? string.Empty; string newText = oldText.Length == 0 ? s : oldText.Substring(0, caretIndex) + s + oldText.Substring(Math.Min(caretIndex, oldText.Length)); @@ -354,9 +352,9 @@ public class TimeBox : TemplatedControl ParseTimeSpan(showLeadingZero); } - private void OnAllowDragChange(AvaloniaPropertyChangedEventArgs args) + private void OnAllowDragChanged(AvaloniaPropertyChangedEventArgs args) { - IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanels); + IsVisibleProperty.SetValue(args.NewValue.Value, _dragPanels[0], _dragPanels[1], _dragPanels[2], _dragPanels[3]); } private void OnTimeChanged(AvaloniaPropertyChangedEventArgs arg) @@ -390,18 +388,18 @@ public class TimeBox : TemplatedControl 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; + _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); + _hourText?.SetValue(TextPresenter.TextProperty,_values[0].ToString(format)); + _minuteText?.SetValue(TextPresenter.TextProperty,_values[1].ToString(format)); + _secondText?.SetValue(TextPresenter.TextProperty,_values[2].ToString(format)); + _milliSecondText?.SetValue(TextPresenter.TextProperty,_values[3].ToString(format)); } private void OnDragPanelPointerMoved(object sender, PointerEventArgs e) @@ -454,7 +452,7 @@ public class TimeBox : TemplatedControl if(index < 0 || index > 3) return; if (!_isShowedCaret[index]) { - if (AllowDrag && _dragPanels[index] != null) + if (AllowDrag) _dragPanels[index].IsVisible = false; _presenters[index].ShowCaret(); _isShowedCaret[index] = true; @@ -479,7 +477,7 @@ public class TimeBox : TemplatedControl _isShowedCaret[index] = false; } - if (AllowDrag && _dragPanels[index] != null) + if (AllowDrag) _dragPanels[index].IsVisible = true; } From 472a48ec123a9e3bcd48fcaf9ccea38841d7e2af Mon Sep 17 00:00:00 2001 From: LiWenhao Date: Sun, 14 Apr 2024 11:20:03 +0800 Subject: [PATCH 5/6] fix: 1s = 100"ms" to 1s = 1000ms --- src/Ursa/Controls/TimeBox.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Ursa/Controls/TimeBox.cs b/src/Ursa/Controls/TimeBox.cs index 4f7bcb0..9cc3283 100644 --- a/src/Ursa/Controls/TimeBox.cs +++ b/src/Ursa/Controls/TimeBox.cs @@ -67,8 +67,9 @@ public class TimeBox : TemplatedControl 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 readonly int[] _limits = new[] { 24, 60, 60, 1000 }; private readonly int[] _values = new[] { 0, 0, 0, 0 }; + private readonly int[] _sectionLength = new[] { 2, 2, 2, 3 }; private readonly bool[] _isShowedCaret = new[] { false, false, false, false }; private int? _currentActiveSectionIndex; private bool _isAlreadyDrag; @@ -208,7 +209,8 @@ public class TimeBox : TemplatedControl _hourText.Text = Time != null ? Time.Value.Hours.ToString() : "0"; _minuteText.Text = Time != null ? Time.Value.Minutes.ToString() : "0"; _secondText.Text = Time != null ? Time.Value.Seconds.ToString() : "0"; - _milliSecondText.Text = Time != null ? ClampMilliSecond(Time.Value.Milliseconds).ToString() : "0"; + //_milliSecondText.Text = Time != null ? ClampMilliSecond(Time.Value.Milliseconds).ToString() : "0"; + _milliSecondText.Text = Time != null ? Time.Value.Milliseconds.ToString() : "0"; ParseTimeSpan(ShowLeadingZero); PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanels[0], _dragPanels[1], _dragPanels[2], _dragPanels[3]); @@ -282,9 +284,9 @@ public class TimeBox : TemplatedControl : oldText.Substring(0, caretIndex) + s + oldText.Substring(Math.Min(caretIndex, oldText.Length)); // Limit the maximum number of input digits - if (newText.Length > 2) + if (newText.Length > _sectionLength[_currentActiveSectionIndex.Value]) { - newText = newText.Substring(0, 2); + newText = newText.Substring(0, _sectionLength[_currentActiveSectionIndex.Value]); } _presenters[_currentActiveSectionIndex.Value].Text = newText; @@ -377,7 +379,8 @@ public class TimeBox : TemplatedControl _hourText.Text = timeSpan.Value.Hours.ToString(); _minuteText.Text = timeSpan.Value.Minutes.ToString(); _secondText.Text = timeSpan.Value.Seconds.ToString(); - _milliSecondText.Text = ClampMilliSecond(timeSpan.Value.Milliseconds).ToString(); + //_milliSecondText.Text = ClampMilliSecond(timeSpan.Value.Milliseconds).ToString(); + _milliSecondText.Text = timeSpan.Value.Milliseconds.ToString(); ParseTimeSpan(ShowLeadingZero); } } @@ -385,6 +388,7 @@ public class TimeBox : TemplatedControl private void ParseTimeSpan(bool showLeadingZero, bool skipParseFromText = false) { string format = showLeadingZero ? "D2" : ""; + string millisecondformat = showLeadingZero ? "D3" : ""; if (!skipParseFromText) { @@ -399,7 +403,7 @@ public class TimeBox : TemplatedControl _hourText?.SetValue(TextPresenter.TextProperty,_values[0].ToString(format)); _minuteText?.SetValue(TextPresenter.TextProperty,_values[1].ToString(format)); _secondText?.SetValue(TextPresenter.TextProperty,_values[2].ToString(format)); - _milliSecondText?.SetValue(TextPresenter.TextProperty,_values[3].ToString(format)); + _milliSecondText?.SetValue(TextPresenter.TextProperty,_values[3].ToString(millisecondformat)); } private void OnDragPanelPointerMoved(object sender, PointerEventArgs e) @@ -549,7 +553,8 @@ public class TimeBox : TemplatedControl { try { - Time = new TimeSpan(0, _values[0], _values[1], _values[2], _values[3] * 10); + //Time = new TimeSpan(0, _values[0], _values[1], _values[2], _values[3] * 10); + Time = new TimeSpan(0, _values[0], _values[1], _values[2], _values[3]); } catch { From 36b01902c7db000e66fa75056b3c9dedb832d4ad Mon Sep 17 00:00:00 2001 From: LiWenhao Date: Tue, 16 Apr 2024 17:23:32 +0800 Subject: [PATCH 6/6] feat: support ctrl + A to select all current section --- src/Ursa/Controls/TimeBox.cs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/Ursa/Controls/TimeBox.cs b/src/Ursa/Controls/TimeBox.cs index 9cc3283..733444b 100644 --- a/src/Ursa/Controls/TimeBox.cs +++ b/src/Ursa/Controls/TimeBox.cs @@ -212,7 +212,7 @@ public class TimeBox : TemplatedControl //_milliSecondText.Text = Time != null ? ClampMilliSecond(Time.Value.Milliseconds).ToString() : "0"; _milliSecondText.Text = Time != null ? Time.Value.Milliseconds.ToString() : "0"; ParseTimeSpan(ShowLeadingZero); - + PointerMovedEvent.AddHandler(OnDragPanelPointerMoved, _dragPanels[0], _dragPanels[1], _dragPanels[2], _dragPanels[3]); } @@ -221,6 +221,13 @@ public class TimeBox : TemplatedControl if (_currentActiveSectionIndex is null) return; var keymap = TopLevel.GetTopLevel(this)?.PlatformSettings?.HotkeyConfiguration; bool Match(List gestures) => gestures.Any(g => g.Matches(e)); + if (keymap is not null && Match(keymap.SelectAll)) + { + _presenters[_currentActiveSectionIndex.Value].SelectionStart = 0; + _presenters[_currentActiveSectionIndex.Value].SelectionEnd = _presenters[_currentActiveSectionIndex.Value].Text?.Length ?? 0; + return; + } + if (e.Key is Key.Enter or Key.Return) { ParseTimeSpan(ShowLeadingZero); @@ -405,7 +412,7 @@ public class TimeBox : TemplatedControl _secondText?.SetValue(TextPresenter.TextProperty,_values[2].ToString(format)); _milliSecondText?.SetValue(TextPresenter.TextProperty,_values[3].ToString(millisecondformat)); } - + private void OnDragPanelPointerMoved(object sender, PointerEventArgs e) { if (!AllowDrag) return; @@ -427,7 +434,7 @@ public class TimeBox : TemplatedControl _lastDragPoint = point; } - + private int GetDelta(Point point) { switch (DragOrientation)