From de0124eb8e61891aa33e51fac80232062d932bbf Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 9 Mar 2024 16:00:51 +0800 Subject: [PATCH 1/6] feat: initialize. --- .../Pages/VerificationCodeDemo.axaml | 13 ++ .../Pages/VerificationCodeDemo.axaml.cs | 13 ++ .../Ursa.Demo/ViewModels/MainViewViewModel.cs | 1 + demo/Ursa.Demo/ViewModels/MenuViewModel.cs | 4 +- .../VerificationCodeDemoViewModel.cs | 8 ++ .../Controls/VerificationCode.axaml | 34 +++++ src/Ursa.Themes.Semi/Controls/_index.axaml | 1 + .../VerificationCode/VerificationCode.cs | 117 ++++++++++++++++++ .../VerificationCodeCollection.cs | 25 ++++ 9 files changed, 215 insertions(+), 1 deletion(-) create mode 100644 demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml create mode 100644 demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs create mode 100644 demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs create mode 100644 src/Ursa.Themes.Semi/Controls/VerificationCode.axaml create mode 100644 src/Ursa/Controls/VerificationCode/VerificationCode.cs create mode 100644 src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml new file mode 100644 index 0000000..ecc21d6 --- /dev/null +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml @@ -0,0 +1,13 @@ + + + + + + + diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs new file mode 100644 index 0000000..6ac89e7 --- /dev/null +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Ursa.Demo.Pages; + +public partial class VerificationCodeDemo : UserControl +{ + public VerificationCodeDemo() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs index 5a0cd77..aa9f90a 100644 --- a/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MainViewViewModel.cs @@ -56,6 +56,7 @@ public class MainViewViewModel : ViewModelBase MenuKeys.MenuKeyTwoTonePathIcon => new TwoTonePathIconDemoViewModel(), MenuKeys.MenuKeyThemeToggler => new ThemeTogglerDemoViewModel(), MenuKeys.MenuKeyToolBar => new ToolBarDemoViewModel(), + MenuKeys.MenuKeyVerificationCode => new VerificationCodeDemoViewModel(), }; } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs index a48238e..1fa6c8a 100644 --- a/demo/Ursa.Demo/ViewModels/MenuViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/MenuViewModel.cs @@ -42,7 +42,8 @@ public class MenuViewModel: ViewModelBase new() { MenuHeader = "Theme Toggler", Key = MenuKeys.MenuKeyThemeToggler, Status = "New" }, new() { MenuHeader = "Timeline", Key = MenuKeys.MenuKeyTimeline, Status = "WIP" }, new() { MenuHeader = "TwoTonePathIcon", Key = MenuKeys.MenuKeyTwoTonePathIcon}, - new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar, Status = "New" } + new() { MenuHeader = "ToolBar", Key = MenuKeys.MenuKeyToolBar, Status = "New" }, + new() { MenuHeader = "Verification Code", Key = MenuKeys.MenuKeyVerificationCode, Status = "New" }, }; } } @@ -81,5 +82,6 @@ public static class MenuKeys public const string MenuKeyTwoTonePathIcon = "TwoTonePathIcon"; public const string MenuKeyThemeToggler = "ThemeToggler"; public const string MenuKeyToolBar = "ToolBar"; + public const string MenuKeyVerificationCode = "VerificationCode"; } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs new file mode 100644 index 0000000..6f106e3 --- /dev/null +++ b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs @@ -0,0 +1,8 @@ +using CommunityToolkit.Mvvm.ComponentModel; + +namespace Ursa.Demo.ViewModels; + +public class VerificationCodeDemoViewModel: ObservableObject +{ + +} \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml new file mode 100644 index 0000000..74e6993 --- /dev/null +++ b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml @@ -0,0 +1,34 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Ursa.Themes.Semi/Controls/_index.axaml b/src/Ursa.Themes.Semi/Controls/_index.axaml index ddb53fd..e79e02f 100644 --- a/src/Ursa.Themes.Semi/Controls/_index.axaml +++ b/src/Ursa.Themes.Semi/Controls/_index.axaml @@ -33,5 +33,6 @@ + diff --git a/src/Ursa/Controls/VerificationCode/VerificationCode.cs b/src/Ursa/Controls/VerificationCode/VerificationCode.cs new file mode 100644 index 0000000..efbc150 --- /dev/null +++ b/src/Ursa/Controls/VerificationCode/VerificationCode.cs @@ -0,0 +1,117 @@ +using System.Windows.Input; +using Avalonia; +using Avalonia.Collections; +using Avalonia.Controls; +using Avalonia.Controls.Metadata; +using Avalonia.Controls.Presenters; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Interactivity; +using Avalonia.LogicalTree; +using Irihi.Avalonia.Shared.Helpers; + +namespace Ursa.Controls; + +[TemplatePart(PART_ItemsControl, typeof(ItemsControl))] +public class VerificationCode: TemplatedControl +{ + public const string PART_ItemsControl = "PART_ItemsControl"; + private ItemsControl? _itemsControl; + private int _currentIndex = 0; + + public static readonly StyledProperty CompleteCommandProperty = AvaloniaProperty.Register( + nameof(CompleteCommand)); + + public ICommand? CompleteCommand + { + get => GetValue(CompleteCommandProperty); + set => SetValue(CompleteCommandProperty, value); + } + + public static readonly StyledProperty CountOfDigitProperty = AvaloniaProperty.Register( + nameof(CountOfDigit)); + + public int CountOfDigit + { + get => GetValue(CountOfDigitProperty); + set => SetValue(CountOfDigitProperty, value); + } + + public static readonly StyledProperty PasswordCharProperty = + AvaloniaProperty.Register( + nameof(PasswordChar)); + + public char PasswordChar + { + get => GetValue(PasswordCharProperty); + set => SetValue(PasswordCharProperty, value); + } + + public static readonly DirectProperty> DigitsProperty = AvaloniaProperty.RegisterDirect>( + nameof(Digits), o => o.Digits, (o, v) => o.Digits = v); + + private AvaloniaList _digits = []; + internal AvaloniaList Digits + { + get => _digits; + set => SetAndRaise(DigitsProperty, ref _digits, value); + } + + static VerificationCode() + { + CountOfDigitProperty.Changed.AddClassHandler((code, args) => code.OnCountOfDigitChanged(args)); + FocusableProperty.OverrideDefaultValue(true); + } + + public VerificationCode() + { + InputMethod.SetIsInputMethodEnabled(this, false); + } + + private void OnCountOfDigitChanged(AvaloniaPropertyChangedEventArgs args) + { + var newValue = args.NewValue.Value; + if (newValue > 0) + { + Digits = new AvaloniaList(Enumerable.Repeat(string.Empty, newValue)); + } + + } + + protected override void OnApplyTemplate(TemplateAppliedEventArgs e) + { + base.OnApplyTemplate(e); + _itemsControl = e.NameScope.Get(PART_ItemsControl); + PointerPressedEvent.AddHandler(OnControlPressed, RoutingStrategies.Tunnel, false, this); + } + + private void OnControlPressed(object sender, PointerPressedEventArgs e) + { + if (e.Source is Control t) + { + var text = t.FindLogicalAncestorOfType(); + if (text != null) + { + _currentIndex = _itemsControl?.IndexFromContainer(text) ?? 0; + } + } + } + + protected override void OnTextInput(TextInputEventArgs e) + { + base.OnTextInput(e); + if (e.Text?.Length == 1 && _currentIndex < CountOfDigit) + { + Digits[_currentIndex] = e.Text; + var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as TextBox; + if (presenter is null) return; + _currentIndex++; + var newPresenter = _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); + presenter.Text = e.Text; + if (_currentIndex == CountOfDigit) + { + CompleteCommand?.Execute(Digits); + } + } + } +} \ No newline at end of file diff --git a/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs new file mode 100644 index 0000000..333af44 --- /dev/null +++ b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs @@ -0,0 +1,25 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Media; + +namespace Ursa.Controls; + +public class VerificationCodeCollection: ItemsControl +{ + protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) + { + return NeedsContainer(item, out recycleKey); + } + + protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) + { + return new TextBox() + { + TextAlignment = TextAlignment.Center, + [InputMethod.IsInputMethodEnabledProperty] = false, + }; + } +} \ No newline at end of file From 27f613f4e494a50c31edf80cc5f489ad1720b476 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 9 Mar 2024 17:20:24 +0800 Subject: [PATCH 2/6] feat: implement verificationcodeitem and corresponding delete operation. --- .../Pages/VerificationCodeDemo.axaml | 7 +- .../Pages/VerificationCodeDemo.axaml.cs | 7 ++ .../VerificationCodeDemoViewModel.cs | 22 +++++- .../Controls/VerificationCode.axaml | 72 ++++++++++++++----- .../VerificationCode/VerificationCode.cs | 64 ++++++++++++----- .../VerificationCodeCollection.cs | 5 +- .../VerificationCodeCompleteEventArgs.cs | 9 +++ .../VerificationCode/VerificationCodeItem.cs | 26 +++++++ 8 files changed, 168 insertions(+), 44 deletions(-) create mode 100644 src/Ursa/Controls/VerificationCode/VerificationCodeCompleteEventArgs.cs create mode 100644 src/Ursa/Controls/VerificationCode/VerificationCodeItem.cs diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml index ecc21d6..437e85b 100644 --- a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml @@ -3,11 +3,14 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:u="https://irihi.tech/ursa" + xmlns:vm="using:Ursa.Demo.ViewModels" + x:DataType="vm:VerificationCodeDemoViewModel" + x:CompileBindings="True" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.VerificationCodeDemo"> - + - + diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs index 6ac89e7..61aecec 100644 --- a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs @@ -1,6 +1,7 @@ using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; +using Ursa.Controls; namespace Ursa.Demo.Pages; @@ -10,4 +11,10 @@ public partial class VerificationCodeDemo : UserControl { InitializeComponent(); } + + private async void VerificationCode_OnComplete(object? sender, VerificationCodeCompleteEventArgs e) + { + var text = string.Join(string.Empty, e.Code); + await MessageBox.ShowOverlayAsync(text); + } } \ No newline at end of file diff --git a/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs index 6f106e3..94e4fff 100644 --- a/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs @@ -1,8 +1,26 @@ -using CommunityToolkit.Mvvm.ComponentModel; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Input; +using Avalonia.Collections; +using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using Ursa.Controls; namespace Ursa.Demo.ViewModels; public class VerificationCodeDemoViewModel: ObservableObject { - + public ICommand CompleteCommand { get; set; } + + public VerificationCodeDemoViewModel() + { + CompleteCommand = new AsyncRelayCommand>(OnComplete); + } + + private async Task OnComplete(IList? obj) + { + if (obj is null) return; + var code = string.Join("", obj); + await MessageBox.ShowOverlayAsync(code); + } } \ No newline at end of file diff --git a/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml index 74e6993..ca051f7 100644 --- a/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml +++ b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml @@ -1,34 +1,68 @@ - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - - - - + + - - - - - - - - diff --git a/src/Ursa/Controls/VerificationCode/VerificationCode.cs b/src/Ursa/Controls/VerificationCode/VerificationCode.cs index efbc150..cc1e9e1 100644 --- a/src/Ursa/Controls/VerificationCode/VerificationCode.cs +++ b/src/Ursa/Controls/VerificationCode/VerificationCode.cs @@ -28,13 +28,13 @@ public class VerificationCode: TemplatedControl set => SetValue(CompleteCommandProperty, value); } - public static readonly StyledProperty CountOfDigitProperty = AvaloniaProperty.Register( - nameof(CountOfDigit)); + public static readonly StyledProperty CountProperty = AvaloniaProperty.Register( + nameof(Count)); - public int CountOfDigit + public int Count { - get => GetValue(CountOfDigitProperty); - set => SetValue(CountOfDigitProperty, value); + get => GetValue(CountProperty); + set => SetValue(CountProperty, value); } public static readonly StyledProperty PasswordCharProperty = @@ -47,19 +47,29 @@ public class VerificationCode: TemplatedControl set => SetValue(PasswordCharProperty, value); } - public static readonly DirectProperty> DigitsProperty = AvaloniaProperty.RegisterDirect>( + public static readonly DirectProperty> DigitsProperty = AvaloniaProperty.RegisterDirect>( nameof(Digits), o => o.Digits, (o, v) => o.Digits = v); - private AvaloniaList _digits = []; - internal AvaloniaList Digits + private IList _digits = []; + internal IList Digits { get => _digits; set => SetAndRaise(DigitsProperty, ref _digits, value); } + + public static readonly RoutedEvent CompleteEvent = + RoutedEvent.Register( + nameof(Complete), RoutingStrategies.Bubble); + + public event EventHandler Complete + { + add => AddHandler(CompleteEvent, value); + remove => RemoveHandler(CompleteEvent, value); + } static VerificationCode() { - CountOfDigitProperty.Changed.AddClassHandler((code, args) => code.OnCountOfDigitChanged(args)); + CountProperty.Changed.AddClassHandler((code, args) => code.OnCountOfDigitChanged(args)); FocusableProperty.OverrideDefaultValue(true); } @@ -73,9 +83,8 @@ public class VerificationCode: TemplatedControl var newValue = args.NewValue.Value; if (newValue > 0) { - Digits = new AvaloniaList(Enumerable.Repeat(string.Empty, newValue)); + Digits = new List(Enumerable.Repeat(string.Empty, newValue)); } - } protected override void OnApplyTemplate(TemplateAppliedEventArgs e) @@ -89,29 +98,48 @@ public class VerificationCode: TemplatedControl { if (e.Source is Control t) { - var text = t.FindLogicalAncestorOfType(); + var text = t.FindLogicalAncestorOfType(); if (text != null) { + text.Focus(); _currentIndex = _itemsControl?.IndexFromContainer(text) ?? 0; } } + e.Handled = true; } protected override void OnTextInput(TextInputEventArgs e) { base.OnTextInput(e); - if (e.Text?.Length == 1 && _currentIndex < CountOfDigit) + if (e.Text?.Length == 1 && _currentIndex < Count) { - Digits[_currentIndex] = e.Text; - var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as TextBox; + + var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as VerificationCodeItem; if (presenter is null) return; - _currentIndex++; - var newPresenter = _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); presenter.Text = e.Text; - if (_currentIndex == CountOfDigit) + Digits[_currentIndex] = e.Text; + _currentIndex++; + _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); + if (_currentIndex == Count) { CompleteCommand?.Execute(Digits); + RaiseEvent(new VerificationCodeCompleteEventArgs(Digits, CompleteEvent)); } } } + + protected override void OnKeyDown(KeyEventArgs e) + { + base.OnKeyDown(e); + if (e.Key == Key.Back && _currentIndex >= 0) + { + var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as VerificationCodeItem; + if (presenter is null) return; + Digits[_currentIndex] = string.Empty; + presenter.Text = string.Empty; + if (_currentIndex == 0) return; + _currentIndex--; + _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); + } + } } \ No newline at end of file diff --git a/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs index 333af44..e403c87 100644 --- a/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs +++ b/src/Ursa/Controls/VerificationCode/VerificationCodeCollection.cs @@ -11,14 +11,13 @@ public class VerificationCodeCollection: ItemsControl { protected override bool NeedsContainerOverride(object? item, int index, out object? recycleKey) { - return NeedsContainer(item, out recycleKey); + return NeedsContainer(item, out recycleKey); } protected override Control CreateContainerForItemOverride(object? item, int index, object? recycleKey) { - return new TextBox() + return new VerificationCodeItem() { - TextAlignment = TextAlignment.Center, [InputMethod.IsInputMethodEnabledProperty] = false, }; } diff --git a/src/Ursa/Controls/VerificationCode/VerificationCodeCompleteEventArgs.cs b/src/Ursa/Controls/VerificationCode/VerificationCodeCompleteEventArgs.cs new file mode 100644 index 0000000..e1422cf --- /dev/null +++ b/src/Ursa/Controls/VerificationCode/VerificationCodeCompleteEventArgs.cs @@ -0,0 +1,9 @@ +using Avalonia.Collections; +using Avalonia.Interactivity; + +namespace Ursa.Controls; + +public class VerificationCodeCompleteEventArgs(IList code, RoutedEvent? @event) : RoutedEventArgs(@event) +{ + public IList Code { get; } = code; +} \ No newline at end of file diff --git a/src/Ursa/Controls/VerificationCode/VerificationCodeItem.cs b/src/Ursa/Controls/VerificationCode/VerificationCodeItem.cs new file mode 100644 index 0000000..081bc7f --- /dev/null +++ b/src/Ursa/Controls/VerificationCode/VerificationCodeItem.cs @@ -0,0 +1,26 @@ +using Avalonia; +using Avalonia.Controls.Primitives; +using Avalonia.Data; + +namespace Ursa.Controls; + +public class VerificationCodeItem: TemplatedControl +{ + public static readonly StyledProperty TextProperty = AvaloniaProperty.Register( + nameof(Text), defaultBindingMode: BindingMode.TwoWay); + + public string Text + { + get => GetValue(TextProperty); + set => SetValue(TextProperty, value); + } + + public static readonly StyledProperty PasswordCharProperty = AvaloniaProperty.Register( + nameof(PasswordChar), defaultBindingMode: BindingMode.TwoWay); + + public char PasswordChar + { + get => GetValue(PasswordCharProperty); + set => SetValue(PasswordCharProperty, value); + } +} \ No newline at end of file From 5c262c50216ef11aeea950d6b14687b0c14e112b Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 9 Mar 2024 17:30:11 +0800 Subject: [PATCH 3/6] feat: add VerificationCodeMode. --- .../Pages/VerificationCodeDemo.axaml | 2 +- .../Pages/VerificationCodeDemo.axaml.cs | 3 ++- .../VerificationCode/VerificationCode.cs | 26 ++++++++++++++++++- .../VerificationCode/VerificationCodeMode.cs | 8 ++++++ 4 files changed, 36 insertions(+), 3 deletions(-) create mode 100644 src/Ursa/Controls/VerificationCode/VerificationCodeMode.cs diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml index 437e85b..68336d5 100644 --- a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml @@ -10,7 +10,7 @@ x:Class="Ursa.Demo.Pages.VerificationCodeDemo"> - + diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs index 61aecec..5933b5c 100644 --- a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml.cs @@ -1,4 +1,5 @@ -using Avalonia; +using System; +using Avalonia; using Avalonia.Controls; using Avalonia.Markup.Xaml; using Ursa.Controls; diff --git a/src/Ursa/Controls/VerificationCode/VerificationCode.cs b/src/Ursa/Controls/VerificationCode/VerificationCode.cs index cc1e9e1..8253898 100644 --- a/src/Ursa/Controls/VerificationCode/VerificationCode.cs +++ b/src/Ursa/Controls/VerificationCode/VerificationCode.cs @@ -47,6 +47,16 @@ public class VerificationCode: TemplatedControl set => SetValue(PasswordCharProperty, value); } + public static readonly StyledProperty ModeProperty = + AvaloniaProperty.Register( + nameof(Mode), defaultValue: VerificationCodeMode.Digit | VerificationCodeMode.Letter); + + public VerificationCodeMode Mode + { + get => GetValue(ModeProperty); + set => SetValue(ModeProperty, value); + } + public static readonly DirectProperty> DigitsProperty = AvaloniaProperty.RegisterDirect>( nameof(Digits), o => o.Digits, (o, v) => o.Digits = v); @@ -113,9 +123,10 @@ public class VerificationCode: TemplatedControl base.OnTextInput(e); if (e.Text?.Length == 1 && _currentIndex < Count) { - var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as VerificationCodeItem; if (presenter is null) return; + char c = e.Text[0]; + if (!Valid(c, this.Mode)) return; presenter.Text = e.Text; Digits[_currentIndex] = e.Text; _currentIndex++; @@ -128,6 +139,19 @@ public class VerificationCode: TemplatedControl } } + private bool Valid(char c, VerificationCodeMode mode) + { + bool isDigit = char.IsDigit(c); + bool isLetter = char.IsLetter(c); + return mode switch + { + VerificationCodeMode.Digit => isDigit, + VerificationCodeMode.Letter => isLetter, + VerificationCodeMode.Digit | VerificationCodeMode.Letter => isDigit || isLetter, + _ => true + }; + } + protected override void OnKeyDown(KeyEventArgs e) { base.OnKeyDown(e); diff --git a/src/Ursa/Controls/VerificationCode/VerificationCodeMode.cs b/src/Ursa/Controls/VerificationCode/VerificationCodeMode.cs new file mode 100644 index 0000000..6caf485 --- /dev/null +++ b/src/Ursa/Controls/VerificationCode/VerificationCodeMode.cs @@ -0,0 +1,8 @@ +namespace Ursa.Controls; + +[Flags] +public enum VerificationCodeMode +{ + Letter = 1, + Digit = 2, +} \ No newline at end of file From bfaee9051fbdc16b18329b9046cbb33c59114103 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 9 Mar 2024 17:32:27 +0800 Subject: [PATCH 4/6] feat: add one more demo. --- demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml | 1 + 1 file changed, 1 insertion(+) diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml index 68336d5..dfd61e6 100644 --- a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml @@ -11,6 +11,7 @@ + From 99cae144e4db58d5b27117a9fc17bfb73770132b Mon Sep 17 00:00:00 2001 From: rabbitism Date: Sat, 9 Mar 2024 17:38:51 +0800 Subject: [PATCH 5/6] feat: support left right navigation. --- src/Ursa/Controls/VerificationCode/VerificationCode.cs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/Ursa/Controls/VerificationCode/VerificationCode.cs b/src/Ursa/Controls/VerificationCode/VerificationCode.cs index 8253898..5e963bd 100644 --- a/src/Ursa/Controls/VerificationCode/VerificationCode.cs +++ b/src/Ursa/Controls/VerificationCode/VerificationCode.cs @@ -165,5 +165,15 @@ public class VerificationCode: TemplatedControl _currentIndex--; _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); } + else if(e.Key == Key.Left && _currentIndex > 0) + { + _currentIndex--; + _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); + } + else if(e.Key == Key.Right && _currentIndex < Count) + { + _currentIndex++; + _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); + } } } \ No newline at end of file From 78697523309489af314225d5fc05d130f909a621 Mon Sep 17 00:00:00 2001 From: rabbitism Date: Mon, 11 Mar 2024 17:02:08 +0800 Subject: [PATCH 6/6] feat: update focus logic. --- .../Pages/VerificationCodeDemo.axaml | 3 +- .../VerificationCodeDemoViewModel.cs | 7 ++- .../Controls/VerificationCode.axaml | 56 +++++++++++-------- .../VerificationCode/VerificationCode.cs | 24 ++++---- 4 files changed, 51 insertions(+), 39 deletions(-) diff --git a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml index dfd61e6..43e4ac8 100644 --- a/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml +++ b/demo/Ursa.Demo/Pages/VerificationCodeDemo.axaml @@ -4,13 +4,14 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:u="https://irihi.tech/ursa" xmlns:vm="using:Ursa.Demo.ViewModels" + xmlns:system="clr-namespace:System;assembly=System.Runtime" x:DataType="vm:VerificationCodeDemoViewModel" x:CompileBindings="True" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Ursa.Demo.Pages.VerificationCodeDemo"> - + diff --git a/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs index 94e4fff..5de9705 100644 --- a/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs +++ b/demo/Ursa.Demo/ViewModels/VerificationCodeDemoViewModel.cs @@ -1,4 +1,5 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Collections; @@ -8,13 +9,15 @@ using Ursa.Controls; namespace Ursa.Demo.ViewModels; -public class VerificationCodeDemoViewModel: ObservableObject +public partial class VerificationCodeDemoViewModel: ObservableObject { public ICommand CompleteCommand { get; set; } + [ObservableProperty] private List? _error; public VerificationCodeDemoViewModel() { CompleteCommand = new AsyncRelayCommand>(OnComplete); + Error = [new Exception("Invalid verification code")]; } private async Task OnComplete(IList? obj) diff --git a/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml index ca051f7..ae38a54 100644 --- a/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml +++ b/src/Ursa.Themes.Semi/Controls/VerificationCode.axaml @@ -10,16 +10,19 @@ - + + + + + BorderThickness="{TemplateBinding BorderThickness}" + CornerRadius="{TemplateBinding CornerRadius}"> + + - + @@ -47,21 +56,24 @@ - + - - - - - - - - - - - - + + + + + + + + + + + + + + + diff --git a/src/Ursa/Controls/VerificationCode/VerificationCode.cs b/src/Ursa/Controls/VerificationCode/VerificationCode.cs index 5e963bd..0a82576 100644 --- a/src/Ursa/Controls/VerificationCode/VerificationCode.cs +++ b/src/Ursa/Controls/VerificationCode/VerificationCode.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Primitives; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.LogicalTree; +using Avalonia.Utilities; using Irihi.Avalonia.Shared.Helpers; namespace Ursa.Controls; @@ -108,12 +109,16 @@ public class VerificationCode: TemplatedControl { if (e.Source is Control t) { - var text = t.FindLogicalAncestorOfType(); - if (text != null) + /* + var item = t.FindLogicalAncestorOfType(); + if (item != null) { - text.Focus(); - _currentIndex = _itemsControl?.IndexFromContainer(text) ?? 0; + item.Focus(); + _currentIndex = _itemsControl?.IndexFromContainer(item) ?? 0; } + */ + _currentIndex = MathUtilities.Clamp(_currentIndex, 0, Count - 1); + _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); } e.Handled = true; } @@ -157,6 +162,7 @@ public class VerificationCode: TemplatedControl base.OnKeyDown(e); if (e.Key == Key.Back && _currentIndex >= 0) { + _currentIndex = MathUtilities.Clamp(_currentIndex, 0, Count - 1); var presenter = _itemsControl?.ContainerFromIndex(_currentIndex) as VerificationCodeItem; if (presenter is null) return; Digits[_currentIndex] = string.Empty; @@ -165,15 +171,5 @@ public class VerificationCode: TemplatedControl _currentIndex--; _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); } - else if(e.Key == Key.Left && _currentIndex > 0) - { - _currentIndex--; - _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); - } - else if(e.Key == Key.Right && _currentIndex < Count) - { - _currentIndex++; - _itemsControl?.ContainerFromIndex(_currentIndex)?.Focus(); - } } } \ No newline at end of file